!130 chore: gitignore 忽略 weekly-summary 目录

* chore: gitignore 忽略 weekly-summary 目录
* feat(bpm): 流程回归支持驳回路径驱动
* feat(bpm): 4个流程happy path回归测试全部跑通
* feat(bpm): 新增流程回归测试框架(连运行后端全路径驱动+自动清理)
This commit is contained in:
wsm
2026-08-17 07:24:40 +00:00
parent a5e4eebc71
commit d66788eb94
12 changed files with 850 additions and 7 deletions
+1
View File
@@ -3,6 +3,7 @@
*.iml
rebel.xml
localDocs
weekly-summary
## backend
**/target
@@ -105,20 +105,20 @@ class InspectCloseoutFlowTest {
}
@Test
void getJJDeptLdWorkerIdList_shouldReturnUserList() {
void getJJDeptLdList_shouldReturnUserList() {
List<String> 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
@@ -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<String, String> 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<JSONObject> 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<JSONObject> 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<JSONObject> 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<ActiveTask> waitSettled(String procInstId, int stableRounds) {
Set<String> last = null;
int stable = 0;
for (int i = 0; i < 30; i++) {
List<ActiveTask> tasks = currentTasks(procInstId);
if (tasks.isEmpty()) {
return tasks;
}
Set<String> 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<ActiveTask> 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<ActiveTask> 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<ActiveTask> 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<ActiveTask> completeCurrentNode(String procInstId) {
for (int guard = 0; guard < 20; guard++) {
List<ActiveTask> 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<ActiveTask> 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<ActiveTask> assertCurrentNode(String procInstId, String... taskDefKeys) {
List<ActiveTask> tasks = currentTasks(procInstId);
Set<String> expected = new HashSet<>(List.of(taskDefKeys));
Set<String> actual = new HashSet<>();
for (ActiveTask t : tasks) {
actual.add(t.taskDefKey);
}
if (!actual.equals(expected)) {
fail("节点不符, 期望=" + expected + ", 实际=" + actual + ", tasks=" + tasks);
}
return tasks;
}
private Set<String> taskIds(List<ActiveTask> tasks) {
Set<String> 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 + "}";
}
}
}
@@ -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";
// ---- 组织 idapplication-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;
}
}
}
@@ -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 path1.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<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 20 && !isFinished(procId); i++) {
List<ActiveTask> 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;
}
}
@@ -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 path1.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<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 15 && !isFinished(procId); i++) {
List<ActiveTask> 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<ActiveTask> 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<ActiveTask> 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;
}
}
@@ -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<String> processInstIds = new ArrayList<>();
/** 业务主表 table -> ids */
private final Map<String, List<String>> businessMain = new LinkedHashMap<>();
/** 业务子表 subTable -> mainIds(按 main_id 删) */
private final Map<String, List<String>> 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<String> 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<String, List<String>> e : businessSub.entrySet()) {
deleteByIn(conn, e.getKey(), "main_id", e.getValue());
}
for (Map.Entry<String, List<String>> 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<String> 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);
}
}
@@ -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.implUserNameList1.3 取 detail.approverNamesaveSubApprove 不设 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<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 15 && !isFinished(procId); i++) {
List<ActiveTask> 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;
}
}
@@ -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.6implWorker 指定经办人
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<ActiveTask> settled = waitSettled(procId, 3);
log.info("[回归] 自动推进稳定后活跃任务: {}", settled);
for (int i = 0; i < 20 && !isFinished(procId); i++) {
List<ActiveTask> 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;
}
}
+3 -1
View File
@@ -39,6 +39,8 @@
<properties>
<jeecgboot.version>3.8.0</jeecgboot.version>
<!-- 默认跳过单测,需显式 -DskipTests=false 开启(回归测试等场景) -->
<skipTests>true</skipTests>
<!-- JDK版本支持17和1.8 -->
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -551,7 +553,7 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
<skipTests>${skipTests}</skipTests>
</configuration>
</plugin>
<!-- 避免font文件的二进制文件格式压缩破坏 -->
+1
View File
@@ -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 idprocess_1778315114392),修复按 process_key 无法启动问题 |
## 使用方式
@@ -0,0 +1,9 @@
-- 习讲话经办人反馈流程 process_key 对齐
-- 背景:该流程 ext_act_process.process_key 曾为中文(xispeak经办人反馈流程),
-- 与部署的 BPMN <process id>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';