From d66788eb943f05db898dfa477d59d002ba315095 Mon Sep 17 00:00:00 2001 From: wsm <8454518+new-twice@user.noreply.gitee.com> Date: Mon, 17 Aug 2026 07:24:40 +0000 Subject: [PATCH] =?UTF-8?q?!130=20chore:=20gitignore=20=E5=BF=BD=E7=95=A5?= =?UTF-8?q?=20weekly-summary=20=E7=9B=AE=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: gitignore 忽略 weekly-summary 目录 * feat(bpm): 流程回归支持驳回路径驱动 * feat(bpm): 4个流程happy path回归测试全部跑通 * feat(bpm): 新增流程回归测试框架(连运行后端全路径驱动+自动清理) --- .gitignore | 1 + .../InspectCloseoutFlowTest.java | 12 +- .../flowregression/FlowRegressionBase.java | 403 ++++++++++++++++++ .../flowregression/FlowRegressionProps.java | 71 +++ .../InspectCloseoutRegressionTest.java | 50 +++ .../InspectImproveRegressionTest.java | 76 ++++ .../flowregression/ResourceRegistry.java | 95 +++++ .../XiSpeakFeedbackRegressionTest.java | 69 +++ .../flowregression/XiSpeakRegressionTest.java | 66 +++ pom.xml | 4 +- sql-changelog/README.md | 1 + ...20260817_习讲话反馈流程process_key对齐.sql | 9 + 12 files changed, 850 insertions(+), 7 deletions(-) create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionBase.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionProps.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectCloseoutRegressionTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectImproveRegressionTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/ResourceRegistry.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakFeedbackRegressionTest.java create mode 100644 jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakRegressionTest.java create mode 100644 sql-changelog/dml/20260817_习讲话反馈流程process_key对齐.sql diff --git a/.gitignore b/.gitignore index 1973d68..f0e31e7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ *.iml rebel.xml localDocs +weekly-summary ## backend **/target diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java index 25bb69c..6362670 100644 --- a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/dqinspecttask/InspectCloseoutFlowTest.java @@ -105,20 +105,20 @@ class InspectCloseoutFlowTest { } @Test - void getJJDeptLdWorkerIdList_shouldReturnUserList() { + void getJJDeptLdList_shouldReturnUserList() { List users = List.of("worker1"); - when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker")) + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld")) .thenReturn(users); - assertEquals(users, flow.getJJDeptLdWorkerIdList()); + assertEquals(users, flow.getJJDeptLdList()); } @Test - void getJJDeptLdWorkerIdListLength_shouldReturnCount() { - when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-jj-worker")) + void getJJDeptLdListLength_shouldReturnCount() { + when(iSysBaseAPI.getUsersListByDeptIdAndRoleIdLocalApi("dept-jj", "role-ld")) .thenReturn(List.of("user1", "user2")); - assertEquals(2, flow.getJJDeptLdWorkerIdListLength()); + assertEquals(2, flow.getJJDeptLdListLength()); } @Test diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionBase.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionBase.java new file mode 100644 index 0000000..c8e706e --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionBase.java @@ -0,0 +1,403 @@ +package org.jeecg.modules.supervision.flowregression; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.jeecg.common.util.RestUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * 流程回归测试基类:纯 HTTP 驱动本机运行中的后端(不启 Spring 上下文)。 + * 审批人动态发现——每步查询当前任务 assignee,以该用户名 autoLogin 后驱动。 + */ +public abstract class FlowRegressionBase { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + private final Map tokenCache = new HashMap<>(); + protected final ResourceRegistry registry = new ResourceRegistry(); + protected boolean passed = true; + + @BeforeEach + void resetState() { + passed = true; + } + + /** + * 用例包装:失败时置 passed=false 保留测试数据供排查,成功则清理。 + * 不能用 TestWatcher —— 它的 testFailed 在 @AfterEach 之后才触发。 + */ + protected void runTestCase(Runnable body) { + try { + body.run(); + passed = true; + } catch (Throwable t) { + passed = false; + throw t; + } + } + + @AfterEach + void cleanup() { + if (passed) { + cleanupHttp(); + try { + registry.cleanupJdbc(); + log.info("[回归] 测试通过,测试数据已清理"); + } catch (Exception e) { + log.error("[回归] DB 清理失败,请手动清理", e); + } + } else { + registry.printResidue(); + } + } + + /** 通过 HTTP 清理 Flowable 流程实例 + task_task + 审批意见 */ + private void cleanupHttp() { + for (String procId : registry.getProcessInstIds()) { + try { + post("/tasktask/taskTask/deleteSingleProcess", + new JSONObject().fluentPut("processInstId", procId), null, FlowRegressionProps.INITIATOR); + log.info("[回归] 已删除流程实例 procId: {}", procId); + } catch (Exception e) { + log.error("[回归] 删除流程实例失败 procId: {}", procId, e); + } + } + } + + // ================= HTTP 基础 ================= + + protected JSONObject get(String path, JSONObject query, String username) { + ResponseEntity resp = RestUtil.request(FlowRegressionProps.BASE_URL + path, + HttpMethod.GET, headers(username), query, null, JSONObject.class); + return resp == null ? null : resp.getBody(); + } + + protected JSONObject post(String path, JSONObject query, JSONObject body, String username) { + ResponseEntity resp = RestUtil.request(FlowRegressionProps.BASE_URL + path, + HttpMethod.POST, headers(username), query, body, JSONObject.class); + return resp == null ? null : resp.getBody(); + } + + private HttpHeaders headers(String username) { + HttpHeaders h = new HttpHeaders(); + h.setContentType(MediaType.APPLICATION_JSON); + h.set("X-Access-Token", token(username)); + return h; + } + + protected String token(String username) { + return tokenCache.computeIfAbsent(username, this::login); + } + + /** 网关自动登录,跳过密码校验 */ + protected String login(String username) { + HttpHeaders h = new HttpHeaders(); + h.setContentType(MediaType.APPLICATION_JSON); + ResponseEntity resp = RestUtil.request(FlowRegressionProps.BASE_URL + "/sys/autoLogin", + HttpMethod.POST, h, new JSONObject().fluentPut("username", username), null, JSONObject.class); + JSONObject body = resp == null ? null : resp.getBody(); + JSONObject result = body == null ? null : body.getJSONObject("result"); + String tk = result == null ? null : result.getString("token"); + if (tk == null || tk.isEmpty()) { + throw new IllegalStateException("autoLogin 失败, username=" + username + ", resp=" + body); + } + return tk; + } + + // ================= 发起流程 ================= + + protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, String initiator) { + return startFlow(fd, businessId, new JSONObject().fluentPut("business_id", businessId), null, initiator); + } + + protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, JSONObject jsonData, String initiator) { + return startFlow(fd, businessId, jsonData, null, initiator); + } + + /** + * deptHandlerName:习讲话反馈等流程首节点按 task_task.deptHandlerName 评估,需在发起请求里带上。 + */ + protected String startFlow(FlowRegressionProps.FlowDescriptor fd, String businessId, JSONObject jsonData, + String deptHandlerName, String initiator) { + JSONObject taskTask = new JSONObject(); + taskTask.put("triggerType", 1); + taskTask.put("flowCode", fd.flowCode); + taskTask.put("flowName", fd.processKey); + taskTask.put("formUrl", fd.formUrl); + taskTask.put("businessTablename", "task_task"); + taskTask.put("businessId", businessId); + taskTask.put("jsonData", jsonData == null ? new JSONObject().fluentPut("business_id", businessId) : jsonData); + if (deptHandlerName != null) { + taskTask.put("deptHandlerName", deptHandlerName); + } + JSONObject req = new JSONObject(); + req.put("taskTask", taskTask); + JSONObject resp = post("/tasktask/taskTask/flow-schedules", null, req, initiator); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("发起流程失败, flowCode=" + fd.flowCode + ", resp=" + resp); + } + String procId = queryProcessInstIdByBusinessId(businessId, initiator); + if (procId == null) { + throw new IllegalStateException("未查询到流程实例, businessId=" + businessId); + } + registry.registerProcess(procId); + registry.registerBusiness(fd.businessTable, businessId); + log.info("[回归] 流程已发起, flowCode: {}, businessId: {}, procId: {}", fd.flowCode, businessId, procId); + return procId; + } + + private String queryProcessInstIdByBusinessId(String businessId, String initiator) { + JSONObject resp = get("/tasktask/taskTask/queryByBusinessId", + new JSONObject().fluentPut("businessId", businessId), initiator); + if (resp == null) { + return null; + } + JSONObject result = resp.getJSONObject("result"); + JSONArray arr = result == null ? null : result.getJSONArray("records"); + if (arr == null || arr.isEmpty()) { + return null; + } + for (int i = arr.size() - 1; i >= 0; i--) { + JSONObject tt = arr.getJSONObject(i); + if (tt.getString("processInstId") != null) { + return tt.getString("processInstId"); + } + } + return null; + } + + // ================= 流程变量 / 任务指派 ================= + + /** 设置流程变量(POST /act/process/setProcessVariable) */ + protected void setProcessVariable(String procInstId, String varField, Object varVal, String username) { + JSONObject body = new JSONObject(); + body.put("processInstanceId", procInstId); + body.put("varField", varField); + body.put("varVal", varVal); + JSONObject resp = post("/act/process/setProcessVariable", null, body, username); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("设置流程变量失败, varField=" + varField + ", resp=" + resp); + } + } + + /** 指派任务办理人(GET /act/processInstance/reassign) */ + protected void assignTask(String taskId, String userName, String operator) { + JSONObject resp = get("/act/processInstance/reassign", + new JSONObject().fluentPut("taskId", taskId).fluentPut("userName", userName), operator); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("指派任务失败, taskId=" + taskId + ", userName=" + userName + ", resp=" + resp); + } + } + + /** + * 等待流程自动推进稳定:连续 stableRounds 次查询活跃任务集合不变视为稳定。 + * 部分流程节点配置了自动提交(首节点自动完成、后续节点级联),发起后需先等它跑完再驱动。 + */ + protected List waitSettled(String procInstId, int stableRounds) { + Set last = null; + int stable = 0; + for (int i = 0; i < 30; i++) { + List tasks = currentTasks(procInstId); + if (tasks.isEmpty()) { + return tasks; + } + Set current = taskIds(tasks); + if (current.equals(last)) { + stable++; + if (stable >= stableRounds) { + return tasks; + } + } else { + stable = 0; + last = current; + } + try { + Thread.sleep(200); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + throw new IllegalStateException("流程未稳定, procId=" + procInstId); + } + + // ================= 当前任务查询 ================= + + protected List currentTasks(String procInstId) { + JSONObject resp = get("/act/task/currentNodeInfo", + new JSONObject().fluentPut("procInstId", procInstId), FlowRegressionProps.INITIATOR); + JSONObject result = resp == null ? null : resp.getJSONObject("result"); + List tasks = new ArrayList<>(); + if (result != null) { + JSONArray arr = result.getJSONArray("currentTasks"); + if (arr != null) { + for (int i = 0; i < arr.size(); i++) { + JSONObject t = arr.getJSONObject(i); + tasks.add(new ActiveTask(t.getString("taskId"), t.getString("taskDefinitionKey"), + t.getString("taskName"), t.getString("assignee"))); + } + } + } + return tasks; + } + + protected boolean isFinished(String procInstId) { + JSONObject resp = get("/act/task/currentNodeInfo", + new JSONObject().fluentPut("procInstId", procInstId), FlowRegressionProps.INITIATOR); + JSONObject result = resp == null ? null : resp.getJSONObject("result"); + return result != null && Boolean.TRUE.equals(result.getBoolean("finished")); + } + + protected void assertFinished(String procInstId) { + assertTrue(isFinished(procInstId), "流程未按预期结束, procId=" + procInstId + " 当前任务=" + currentTasks(procInstId)); + } + + /** 驱动流程直到当前活跃任务全部位于指定节点(兼容自动推进);若已结束则返回 */ + protected void driveUntil(String procInstId, String taskDefKey) { + for (int i = 0; i < 20 && !isFinished(procInstId); i++) { + List tasks = currentTasks(procInstId); + if (!tasks.isEmpty() && tasks.stream().allMatch(t -> taskDefKey.equals(t.taskDefKey))) { + return; + } + completeCurrentNode(procInstId); + } + if (!isFinished(procInstId)) { + throw new IllegalStateException("驱动未到达目标节点 " + taskDefKey + ", procId=" + procInstId + + " 当前任务=" + currentTasks(procInstId)); + } + } + + // ================= 审批动作 ================= + + protected void approve(String taskId, String assignee, String remarks) { + JSONObject body = new JSONObject(); + body.put("taskId", taskId); + body.put("processModel", "2"); + body.put("remarks", remarks == null ? "同意" : remarks); + JSONObject resp = post("/act/task/processComplete", null, body, assignee); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("审批失败, taskId=" + taskId + ", assignee=" + assignee + ", resp=" + resp); + } + } + + protected void reject(String taskId, String assignee, String rejectNodeCode, String remarks) { + JSONObject body = new JSONObject(); + body.put("taskId", taskId); + body.put("rejectModelNode", rejectNodeCode); + body.put("remarks", remarks == null ? "驳回" : remarks); + JSONObject resp = post("/act/task/processComplete", null, body, assignee); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("驳回失败, taskId=" + taskId + ", assignee=" + assignee + ", resp=" + resp); + } + } + + // ================= 驱动:办完当前节点全部任务直到节点推进 ================= + + /** + * 办完当前活跃节点上的全部任务(兼容多实例会签/或签)。 + * 兼容部分流程节点自动推进:任务可能在查询与办理之间被自动完成, + * 遇失效任务(task is null)跳过重查;等待短暂时间让自动推进稳定后再驱动。 + * 若流程已结束返回空列表。 + */ + protected List completeCurrentNode(String procInstId) { + for (int guard = 0; guard < 20; guard++) { + List tasks = currentTasks(procInstId); + if (tasks.isEmpty()) { + return tasks; + } + log.info("[回归] 当前活跃任务: {}", tasks); + boolean anyCompleted = false; + for (ActiveTask t : tasks) { + if (t.assignee == null || t.assignee.isEmpty()) { + throw new IllegalStateException("任务无办理人(可能是候选组待签收), taskId=" + t.taskId + + ", taskDefKey=" + t.taskDefKey + ", taskName=" + t.taskName); + } + try { + log.info("[回归] 办理节点任务, taskDefKey: {}, taskName: {}, assignee: {}, taskId: {}", + t.taskDefKey, t.taskName, t.assignee, t.taskId); + approve(t.taskId, t.assignee, "同意"); + anyCompleted = true; + } catch (IllegalStateException e) { + // 任务可能已被自动推进完成/失效,跳过,下一轮重新查询 + log.info("[回归] 任务已失效(可能被自动推进), 跳过重查, taskId: {}, err: {}", t.taskId, e.getMessage()); + } + } + sleepQuietly(300); + List after = currentTasks(procInstId); + if (after.isEmpty()) { + return after; + } + // 节点已推进(活跃任务集变化)则返回给外层继续驱动 + if (!taskIds(after).equals(taskIds(tasks)) || !anyCompleted) { + return after; + } + } + throw new IllegalStateException("完成当前节点超限, procId=" + procInstId); + } + + private void sleepQuietly(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + /** 断言当前活跃任务恰好在指定节点集合,返回任务列表 */ + protected List assertCurrentNode(String procInstId, String... taskDefKeys) { + List tasks = currentTasks(procInstId); + Set expected = new HashSet<>(List.of(taskDefKeys)); + Set actual = new HashSet<>(); + for (ActiveTask t : tasks) { + actual.add(t.taskDefKey); + } + if (!actual.equals(expected)) { + fail("节点不符, 期望=" + expected + ", 实际=" + actual + ", tasks=" + tasks); + } + return tasks; + } + + private Set taskIds(List tasks) { + Set ids = new HashSet<>(); + for (ActiveTask t : tasks) { + ids.add(t.taskId); + } + return ids; + } + + /** 当前活跃任务 */ + public static class ActiveTask { + public final String taskId; + public final String taskDefKey; + public final String taskName; + public final String assignee; + + public ActiveTask(String taskId, String taskDefKey, String taskName, String assignee) { + this.taskId = taskId; + this.taskDefKey = taskDefKey; + this.taskName = taskName; + this.assignee = assignee; + } + + @Override + public String toString() { + return "{taskId=" + taskId + ", def=" + taskDefKey + ", name=" + taskName + ", assignee=" + assignee + "}"; + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionProps.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionProps.java new file mode 100644 index 0000000..5c7f272 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/FlowRegressionProps.java @@ -0,0 +1,71 @@ +package org.jeecg.modules.supervision.flowregression; + +/** + * 流程回归测试常量配置。 + * 服务器地址/库连接针对本地开发环境(test_src),各流程 flowCode↔process_key 映射 + * 来自 test_src.ext_act_process_form / ext_act_process,组织 id 来自 application-flow.yml。 + */ +public final class FlowRegressionProps { + + private FlowRegressionProps() { + } + + /** 后端地址(context-path 为 /jeecg-boot),需本地已运行 */ + public static final String BASE_URL = "http://localhost:8080/jeecg-boot"; + + /** 清理用本地 test_src 连接 */ + public static final String JDBC_URL = "jdbc:mysql://127.0.0.1:3306/test_src?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false"; + public static final String JDBC_USER = "root"; + public static final String JDBC_PASSWORD = "root"; + + /** 流程发起人(各流程 1.1 节点 assignee = applyUserId = 发起人) */ + public static final String INITIATOR = "admin"; + + // ---- 组织 id(application-flow.yml flow-biz.org) ---- + public static final String DEPT_DQ = "2044677628281720834"; + public static final String DEPT_JJ = "2054466800692432898"; + public static final String DEPT_SLD = "2044677562460508161"; + public static final String DEPT_BG = "2044677604785229826"; + + public static final String ROLE_LD = "2044680793306591234"; + public static final String ROLE_JJ_WORKER = "2047511967494213633"; + public static final String ROLE_SLD_JWSJ = "2063902766943363073"; + public static final String ROLE_SDWSJ = "2062796937607499777"; + public static final String ROLE_SDW = "2044676455280570370"; + + // ---- 4 个流程描述符 ---- + /** 巡视整改提升 */ + public static final FlowDescriptor IMPROVE = new FlowDescriptor( + "inspect_flow_create_01", "process_1780563034974", + "dj/inspectimprove/components/DjInspectImproveBPMForm", "dq_inspect_task"); + + /** 巡视整改销号 */ + public static final FlowDescriptor CLOSEOUT = new FlowDescriptor( + "dq_delete_01", "partymatter_close", + "dj/inspectcloseout/components/InspectCloseoutBPMForm", "dq_inspect_task"); + + /** 习讲话审批 */ + public static final FlowDescriptor XI_SPEAK = new FlowDescriptor( + "dev_bg_xi_speak_001", "bg_xispeak_process", + "bg/xispeak/components/BgXiSpeakBPMForm?showFeedbackInfo=1&showJiJianFields=1", "bg_xi_speak"); + + /** 习讲话经办人反馈(process_key 已对齐 BPMN id) */ + public static final FlowDescriptor XI_SPEAK_FB = new FlowDescriptor( + "bg_xi_speak_fb_01", "process_1778315114392", + "bg/xispeak/components/BgXiSpeakFeedbackBPMForm", "bg_xi_speak"); + + /** 单个流程的描述与业务表信息 */ + public static final class FlowDescriptor { + public final String flowCode; + public final String processKey; + public final String formUrl; + public final String businessTable; + + public FlowDescriptor(String flowCode, String processKey, String formUrl, String businessTable) { + this.flowCode = flowCode; + this.processKey = processKey; + this.formUrl = formUrl; + this.businessTable = businessTable; + } + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectCloseoutRegressionTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectCloseoutRegressionTest.java new file mode 100644 index 0000000..21d3743 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectCloseoutRegressionTest.java @@ -0,0 +1,50 @@ +package org.jeecg.modules.supervision.flowregression; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 巡视整改销号流程回归测试。 + * flowCode=dq_delete_01 → process_key=partymatter_close + * 节点:1.1 责任部门负责人分配/办结 →(办结分支 is_end=1 跳过 1.2/1.3)→ 1.4 党群经办人 → 1.5 党群领导 + * → 1.6 纪检部经办人 → 1.7 纪检部领导 → 1.8 分管所领导 → 1.9 纪委书记 → 1.10 党委书记 → 结束 + */ +public class InspectCloseoutRegressionTest extends FlowRegressionBase { + + /** 办结 happy path:1.1 后 is_end=1 直跳 1.4 */ + @Test + void happyPath() { + runTestCase(() -> { + String businessId = createCloseoutBusiness(); + String procId = startFlow(FlowRegressionProps.CLOSEOUT, businessId, FlowRegressionProps.INITIATOR); + setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR); + List settled = waitSettled(procId, 3); + log.info("[回归] 自动推进稳定后活跃任务: {}", settled); + for (int i = 0; i < 20 && !isFinished(procId); i++) { + List tasks = completeCurrentNode(procId); + log.info("[回归] 节点推进后活跃任务: {}", tasks); + } + assertFinished(procId); + }); + } + + private String createCloseoutBusiness() { + String id = "REGX" + System.currentTimeMillis(); + JSONObject body = new JSONObject(); + body.put("id", id); + // 整改任务必须挂在具体问题节点下 + body.put("problemId", "2085316815648165890"); + body.put("measureResDept", FlowRegressionProps.DEPT_DQ); + body.put("measureResLeader", "dq_ld_01"); + body.put("chargeLeaderId", "sz"); + body.put("dqInspectProgressList", new JSONArray()); + JSONObject resp = post("/dqinspecttask/dqInspectTask/add", null, body, FlowRegressionProps.INITIATOR); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("创建巡视整改销号任务失败, resp=" + resp); + } + return id; + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectImproveRegressionTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectImproveRegressionTest.java new file mode 100644 index 0000000..5a9499b --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/InspectImproveRegressionTest.java @@ -0,0 +1,76 @@ +package org.jeecg.modules.supervision.flowregression; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 巡视整改提升流程回归测试。 + * flowCode=inspect_flow_create_01 → process_key=process_1780563034974 + * 节点:1.1 发起→1.2 党群负责人→1.3 责任部门领导分配→(经办人 1.4)→1.5 部门领导→1.6 党群经办人→1.7 党群负责人→结束 + */ +public class InspectImproveRegressionTest extends FlowRegressionBase { + + /** 无经办人(办结)happy path:1.1-1.3 自动推进后直跳 1.5 */ + @Test + void happyPath() { + runTestCase(() -> { + String businessId = createImproveBusiness(); + String procId = startFlow(FlowRegressionProps.IMPROVE, businessId, FlowRegressionProps.INITIATOR); + // 强制走"办结"路径:1.3 后网关 is_end=1 → 直跳 1.5,避免 NULL 经办人节点 + setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR); + List settled = waitSettled(procId, 3); + log.info("[回归] 自动推进稳定后活跃任务: {}", settled); + for (int i = 0; i < 15 && !isFinished(procId); i++) { + List tasks = completeCurrentNode(procId); + log.info("[回归] 节点推进后活跃任务: {}", tasks); + } + assertFinished(procId); + }); + } + + /** 驳回路径:1.6 党群经办人审查 驳回到 1.5 责任部门领导审批,再重走到结束 */ + @Test + void rejectPath_atNode16_backTo15() { + runTestCase(() -> { + String businessId = createImproveBusiness(); + String procId = startFlow(FlowRegressionProps.IMPROVE, businessId, FlowRegressionProps.INITIATOR); + setProcessVariable(procId, "is_end", "1", FlowRegressionProps.INITIATOR); + // 办结路径:自动推进 + 驱动 1.1-1.3 → 1.6 + driveUntil(procId, "Task_14226iy"); + List tasks = assertCurrentNode(procId, "Task_14226iy"); + ActiveTask task16 = tasks.get(0); + log.info("[回归] 1.6 驳回至 1.5, taskId: {}, assignee: {}", task16.taskId, task16.assignee); + reject(task16.taskId, task16.assignee, "Task_0mxtg05", "驳回测试"); + // 驳回后回到 1.5,重新驱动到结束 + for (int i = 0; i < 15 && !isFinished(procId); i++) { + List after = completeCurrentNode(procId); + log.info("[回归] 驳回后节点推进: {}", after); + } + assertFinished(procId); + }); + } + + private String createImproveBusiness() { + String id = "REGT" + System.currentTimeMillis(); + JSONObject body = new JSONObject(); + body.put("id", id); + // 整改任务必须挂在具体问题节点下(DqInspectTaskServiceImpl.validateProblemId) + body.put("problemId", "2085316815648165890"); + body.put("measureResDept", FlowRegressionProps.DEPT_DQ); + body.put("measureResLeader", "dq_ld_01"); + body.put("isNeedAppro", "1"); + body.put("supDeptleaderid", "dq_ld_01"); + body.put("chargeLeaderId", "dq_ld_01"); + body.put("dqInspectProgressList", new JSONArray()); + JSONObject resp = post("/dqinspecttask/dqInspectTask/add", null, body, FlowRegressionProps.INITIATOR); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("创建巡视整改任务失败, resp=" + resp); + } + return id; + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/ResourceRegistry.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/ResourceRegistry.java new file mode 100644 index 0000000..1cf7aee --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/ResourceRegistry.java @@ -0,0 +1,95 @@ +package org.jeecg.modules.supervision.flowregression; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * 登记本次回归测试创建的资源;通过后由基类清理(Flowable/task_task 走 HTTP + * deleteSingleProcess,此处清理业务表 + 抄送 + 流程关联表残留),失败保留供排查。 + */ +public class ResourceRegistry { + + private final List processInstIds = new ArrayList<>(); + /** 业务主表 table -> ids */ + private final Map> businessMain = new LinkedHashMap<>(); + /** 业务子表 subTable -> mainIds(按 main_id 删) */ + private final Map> businessSub = new LinkedHashMap<>(); + + public void registerProcess(String processInstId) { + if (processInstId != null && !processInstId.isEmpty()) { + processInstIds.add(processInstId); + } + } + + public void registerBusiness(String table, String id) { + businessMain.computeIfAbsent(table, k -> new ArrayList<>()).add(id); + } + + public void registerSub(String subTable, String mainId) { + businessSub.computeIfAbsent(subTable, k -> new ArrayList<>()).add(mainId); + } + + public List getProcessInstIds() { + return processInstIds; + } + + /** 清理 DB 业务残留:流程关联表 → 抄送 → 子表 → 主表 */ + public void cleanupJdbc() { + try (Connection conn = DriverManager.getConnection(FlowRegressionProps.JDBC_URL, + FlowRegressionProps.JDBC_USER, FlowRegressionProps.JDBC_PASSWORD)) { + conn.setAutoCommit(false); + try { + deleteByIn(conn, "ext_act_flow_data", "process_inst_id", processInstIds); + deleteByIn(conn, "ext_act_task_cc", "proc_inst_id", processInstIds); + // deleteSingleProcess 对 task_task 是逻辑删除(@TableLogic),这里物理清掉 + deleteByIn(conn, "task_task", "process_inst_id", processInstIds); + for (Map.Entry> e : businessSub.entrySet()) { + deleteByIn(conn, e.getKey(), "main_id", e.getValue()); + } + for (Map.Entry> e : businessMain.entrySet()) { + deleteByIn(conn, e.getKey(), "id", e.getValue()); + } + conn.commit(); + } catch (SQLException e) { + conn.rollback(); + throw e; + } + } catch (Exception e) { + throw new IllegalStateException("测试数据清理失败", e); + } + } + + private void deleteByIn(Connection conn, String table, String col, List values) throws SQLException { + if (values == null || values.isEmpty()) { + return; + } + StringBuilder sb = new StringBuilder("DELETE FROM ").append(table) + .append(" WHERE ").append(col).append(" IN ("); + for (int i = 0; i < values.size(); i++) { + sb.append("?"); + if (i < values.size() - 1) { + sb.append(","); + } + } + sb.append(")"); + try (PreparedStatement ps = conn.prepareStatement(sb.toString())) { + for (int i = 0; i < values.size(); i++) { + ps.setString(i + 1, values.get(i)); + } + ps.executeUpdate(); + } + } + + public void printResidue() { + System.out.println("[回归] 测试失败,保留以下数据以便排查:"); + System.out.println("[回归] processInstIds = " + processInstIds); + System.out.println("[回归] businessMain = " + businessMain); + System.out.println("[回归] businessSub = " + businessSub); + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakFeedbackRegressionTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakFeedbackRegressionTest.java new file mode 100644 index 0000000..9b9cf6d --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakFeedbackRegressionTest.java @@ -0,0 +1,69 @@ +package org.jeecg.modules.supervision.flowregression; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 习讲话经办人反馈流程回归测试。 + * flowCode=bg_xi_speak_fb_01 → process_key=xispeak经办人反馈流程(test_src 中文 key) + * 节点:1.1 部门经办人反馈完成情况 →1.3 部门负责人审批→1.4 督办经办人核查 + * →1.5 督办部门负责人审批→1.6 纪检经办人监察→1.7 纪检负责人审批→结束 + */ +public class XiSpeakFeedbackRegressionTest extends FlowRegressionBase { + + private static final String DD_DEPT = "2044677731423850498"; + + @Test + void happyPath() { + runTestCase(() -> { + String businessId = createXiSpeakBusiness(); + // 1.1 多实例取 approve_info.implUserNameList,1.3 取 detail.approverName(saveSubApprove 不设 approverName,走 edit) + JSONObject approveInfo = new JSONObject(); + JSONObject detail = new JSONObject(); + detail.put("isEnd", 1); + detail.put("deptId", DD_DEPT); + detail.put("implWorker", "dd_ld_01"); + detail.put("implUserNameList", new JSONArray().fluentAdd("dd_ld_01")); + detail.put("approverName", "dd_ld_02"); + approveInfo.put(DD_DEPT, detail); + JSONObject editBody = new JSONObject(); + editBody.put("id", businessId); + editBody.put("approveInfo", approveInfo); + post("/bg/xispeak/bgXiSpeak/edit", null, editBody, FlowRegressionProps.INITIATOR); + + JSONObject jsonData = new JSONObject(); + jsonData.put("business_id", businessId); + // 1.1 多实例按 task_task.deptHandlerName 评估,须在发起请求里带上 + String procId = startFlow(FlowRegressionProps.XI_SPEAK_FB, businessId, jsonData, "dd_ld_01", + FlowRegressionProps.INITIATOR); + List settled = waitSettled(procId, 3); + log.info("[回归] 自动推进稳定后活跃任务: {}", settled); + for (int i = 0; i < 15 && !isFinished(procId); i++) { + List tasks = completeCurrentNode(procId); + log.info("[回归] 节点推进后活跃任务: {}", tasks); + } + assertFinished(procId); + }); + } + + private String createXiSpeakBusiness() { + String id = "REGFB" + System.currentTimeMillis(); + JSONObject body = new JSONObject(); + body.put("id", id); + body.put("secret_level", 0); + body.put("need_sdw_approval", 0); + body.put("impl_dept", DD_DEPT); + body.put("responsible_person", "dd_ld_01"); + body.put("impl_measures", "回归测试反馈"); + body.put("completion_status", 0); + body.put("completed_stage", 0); + JSONObject resp = post("/bg/xispeak/bgXiSpeak/add", null, body, FlowRegressionProps.INITIATOR); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("创建习讲话反馈事项失败, resp=" + resp); + } + return id; + } +} diff --git a/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakRegressionTest.java b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakRegressionTest.java new file mode 100644 index 0000000..ec11347 --- /dev/null +++ b/jeecg-module-supervision/src/test/java/org/jeecg/modules/supervision/flowregression/XiSpeakRegressionTest.java @@ -0,0 +1,66 @@ +package org.jeecg.modules.supervision.flowregression; + +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * 习讲话审批流程回归测试。 + * flowCode=dev_bg_xi_speak_001 → process_key=bg_xispeak_process + * 收集线:1.1 督办经办人发起→1.2 督办负责人→(needSdw=0 跳过 1.3)→1.4 收集部门负责人分配 + * →(isEnd=1 直 1.6 审批)→1.7 收集督办发起人确认 + * 反馈线:1.8 反馈部门经办人→1.11 督办经办人核查→1.12 督办部门负责人审批 + * →1.13 纪检经办人监察→1.14 纪检负责人审批→1.15 督办部门经办人确认(归档,抄送)→结束 + */ +public class XiSpeakRegressionTest extends FlowRegressionBase { + + private static final String DD_DEPT = "2044677731423850498"; + + @Test + void happyPath() { + runTestCase(() -> { + String businessId = createXiSpeakBusiness(); + JSONObject jsonData = new JSONObject(); + jsonData.put("business_id", businessId); + jsonData.put("needSdwApproval", 0); + jsonData.put("feedback_right_now", 0); + jsonData.put("sdwLeaderList", new JSONArray()); + jsonData.put("implDept", DD_DEPT); + String procId = startFlow(FlowRegressionProps.XI_SPEAK, businessId, jsonData, FlowRegressionProps.INITIATOR); + // 写部门审批明细:isEnd=1 收集完成直 1.6,implWorker 指定经办人 + JSONObject subApprove = new JSONObject(); + subApprove.put("mainId", businessId); + subApprove.put("deptId", DD_DEPT); + subApprove.put("isEnd", 1); + subApprove.put("implWorker", "dd_ld_01"); + post("/bg/xispeak/bgXiSpeak/saveSubApprove", null, subApprove, FlowRegressionProps.INITIATOR); + List settled = waitSettled(procId, 3); + log.info("[回归] 自动推进稳定后活跃任务: {}", settled); + for (int i = 0; i < 20 && !isFinished(procId); i++) { + List tasks = completeCurrentNode(procId); + log.info("[回归] 节点推进后活跃任务: {}", tasks); + } + assertFinished(procId); + }); + } + + private String createXiSpeakBusiness() { + String id = "REGXSP" + System.currentTimeMillis(); + JSONObject body = new JSONObject(); + body.put("id", id); + body.put("secret_level", 0); + body.put("need_sdw_approval", 0); + body.put("impl_dept", DD_DEPT); + body.put("responsible_person", "dd_ld_01"); + body.put("impl_measures", "回归测试措施"); + body.put("completion_status", 0); + body.put("completed_stage", 0); + JSONObject resp = post("/bg/xispeak/bgXiSpeak/add", null, body, FlowRegressionProps.INITIATOR); + if (resp == null || !Boolean.TRUE.equals(resp.getBoolean("success"))) { + throw new IllegalStateException("创建习讲话事项失败, resp=" + resp); + } + return id; + } +} diff --git a/pom.xml b/pom.xml index dfbffae..c1bc6a5 100644 --- a/pom.xml +++ b/pom.xml @@ -39,6 +39,8 @@ 3.8.0 + + true 17 UTF-8 @@ -551,7 +553,7 @@ org.apache.maven.plugins maven-surefire-plugin - true + ${skipTests} diff --git a/sql-changelog/README.md b/sql-changelog/README.md index c12c9ed..07f5863 100644 --- a/sql-changelog/README.md +++ b/sql-changelog/README.md @@ -36,6 +36,7 @@ YYYYMMDD_描述.sql | 2026-08-12 | DML | `20260812_习讲话反馈流程完整流程更新.sql` | 习讲话经办人反馈流程(xispeak经办人反馈流程)完整流程更新(process_key含中文,需 -f 强制导出),全新库可建、已有库可UPDATE | | 2026-08-12 | DML | `20260812_习讲话审批流程完整流程更新.sql` | 习讲话审批流程(bg_xispeak_process)完整流程更新,全新库可建、已有库可UPDATE | | 2026-08-13 | DDL | `20260813_反馈表新增检查情况监督意见措施数量字段.sql` | dq_inspect_progress 新增 inspection_status/supervisory_opinion/measure_count 三列,保存纪检反馈字段 | +| 2026-08-17 | DML | `20260817_习讲话反馈流程process_key对齐.sql` | 习讲话经办人反馈流程 process_key 对齐为 BPMN id(process_1778315114392),修复按 process_key 无法启动问题 | ## 使用方式 diff --git a/sql-changelog/dml/20260817_习讲话反馈流程process_key对齐.sql b/sql-changelog/dml/20260817_习讲话反馈流程process_key对齐.sql new file mode 100644 index 0000000..6cce8bd --- /dev/null +++ b/sql-changelog/dml/20260817_习讲话反馈流程process_key对齐.sql @@ -0,0 +1,9 @@ +-- 习讲话经办人反馈流程 process_key 对齐 +-- 背景:该流程 ext_act_process.process_key 曾为中文(xispeak经办人反馈流程), +-- 与部署的 BPMN (process_1778315114392)不一致,导致 startWorkflow +-- 按 process_key 查不到已部署定义、流程无法发起("立即发起流程失败")。 +-- 对齐为 BPMN id 后,按 flowCode 发起即可正常启动。 +UPDATE ext_act_process +SET process_key = 'process_1778315114392' +WHERE id = '2058711133876629506' + AND process_key <> 'process_1778315114392';