feat(oapi): 应用内审批 oapi——待办/办件查询 + 办理(operator 复核)+ 待办跳转 bizUrl 优先
build / build-push (push) Successful in 1m29s
build / build-push (push) Successful in 1m29s
- GET /oapi/flow/tasks/todo?schId&assigneeId&bizType&page&size(出参含 biz 定位/快照/版本)
- GET /oapi/flow/instances?schId&bizType&status&page&size(查询不限办理人)
- POST /oapi/flow/task/{taskId}/approve|reject?schId&operatorId&operatorName:
operatorId 按任务办理人逐字复核(非办理人→业务码拒绝不 500),拒绝必填意见,
opinion 经事件链透传到回调 payload;审批后回调/待办减一自动复用
- 待办通知 jumpPath:办件 bizUrl 优先(空回落 /task/{taskId})
契约 IT +3(todo 过滤分页/instances 状态过滤/办理三态+payload 断言),
单测 UserMsgNotifierBizUrlTest 两态;全量 99 单测 + 11 契约 IT 绿。
This commit is contained in:
@@ -16,6 +16,8 @@ import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
@@ -242,6 +244,172 @@ class OapiFlowContractIT extends FlowTestSupport {
|
||||
assertEquals(10, instanceRepository.findById(instanceId).get().getStatus());
|
||||
}
|
||||
|
||||
// ===== 应用内审批(第三步):tasks/todo + instances + approve/reject =====
|
||||
|
||||
/** 待办分页(bizType 过滤 + 出参含 biz 定位/快照/版本);查询不限办理人(办理才校验) */
|
||||
@Test
|
||||
void todoTasks_pagedAndFilteredByBizType() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t20", "待办查询", linearDef("ybb", "审核"));
|
||||
String noticeBody = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"notice\",\"bizId\":\"td-1\",\"bizTitle\":\"通知审核\","
|
||||
+ "\"bizUrl\":\"http://front/#/notice/1\","
|
||||
+ "\"bizSnapshot\":{\"title\":\"通知审核\",\"summary\":\"摘要\"},"
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
String itemBody = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"td-2\",\"bizTitle\":\"事项审核\","
|
||||
+ "\"submitterId\":202,\"submitterName\":\"李四\"}";
|
||||
unwrap(client.post("/oapi/flow/apply?schId=1", noticeBody, JsonNode.class).getBody());
|
||||
unwrap(client.post("/oapi/flow/apply?schId=1", itemBody, JsonNode.class).getBody());
|
||||
|
||||
// 全量待办
|
||||
ResponseEntity<JsonNode> all = client.get(
|
||||
"/oapi/flow/tasks/todo?schId=1&assigneeId=101", JsonNode.class);
|
||||
assertEquals(200, all.getStatusCodeValue());
|
||||
assertEquals(0, all.getBody().get("code").asInt());
|
||||
JsonNode allData = unwrap(all.getBody());
|
||||
assertEquals(2, allData.get("total").asInt());
|
||||
JsonNode firstRow = allData.get("content").get(0);
|
||||
assertNotNull(firstRow.get("taskId").asLong());
|
||||
assertNotNull(firstRow.get("instanceId").asLong());
|
||||
assertNotNull(firstRow.get("instanceVersion"));
|
||||
assertEquals("notice", firstRow.get("bizType").asText());
|
||||
assertEquals("td-1", firstRow.get("bizId").asText());
|
||||
assertTrue(firstRow.get("bizSnapshot").asText().contains("摘要"));
|
||||
assertEquals("审核", firstRow.get("nodeName").asText());
|
||||
assertEquals(101, firstRow.get("assigneeId").asInt());
|
||||
|
||||
// bizType 过滤
|
||||
ResponseEntity<JsonNode> onlyItem = client.get(
|
||||
"/oapi/flow/tasks/todo?schId=1&assigneeId=101&bizType=svc_item", JsonNode.class);
|
||||
JsonNode itemData = unwrap(onlyItem.getBody());
|
||||
assertEquals(1, itemData.get("total").asInt());
|
||||
assertEquals("td-2", itemData.get("content").get(0).get("bizId").asText());
|
||||
}
|
||||
|
||||
/** 办件分页(bizType/status 过滤;查询不做办理人校验) */
|
||||
@Test
|
||||
void instances_pagedByBizTypeAndStatus() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t21", "办件查询", linearDef("ybb", "审核"));
|
||||
unwrap(client.post("/oapi/flow/apply?schId=1", "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"notice\",\"bizId\":\"in-1\",\"bizTitle\":\"办件一\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}", JsonNode.class).getBody());
|
||||
unwrap(client.post("/oapi/flow/apply?schId=1", "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"in-2\",\"bizTitle\":\"办件二\","
|
||||
+ "\"submitterId\":202,\"submitterName\":\"李四\"}", JsonNode.class).getBody());
|
||||
|
||||
JsonNode byType = unwrap(client.get(
|
||||
"/oapi/flow/instances?schId=1&bizType=notice", JsonNode.class).getBody());
|
||||
assertEquals(1, byType.get("total").asInt());
|
||||
assertEquals("办件一", byType.get("content").get(0).get("bizTitle").asText());
|
||||
|
||||
JsonNode approving = unwrap(client.get(
|
||||
"/oapi/flow/instances?schId=1&status=10", JsonNode.class).getBody());
|
||||
assertEquals(2, approving.get("total").asInt());
|
||||
JsonNode finished = unwrap(client.get(
|
||||
"/oapi/flow/instances?schId=1&status=20", JsonNode.class).getBody());
|
||||
assertEquals(0, finished.get("total").asInt());
|
||||
}
|
||||
|
||||
/** 办理(第三步 T-C1b):非办理人业务码拒绝不 500;同意推进+opinion 进回调 payload;拒绝必填意见 */
|
||||
@Test
|
||||
void approveAndReject_operatorChecked_opinionPropagates() throws Exception {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t22", "办理", linearDef("ybb", "审核"));
|
||||
// 本地回调接收 sink(探活 POST + 事件推送都回 200),直接捕获 payload 断言
|
||||
List<String> receivedPayloads = java.util.Collections.synchronizedList(new java.util.ArrayList<>());
|
||||
com.sun.net.httpserver.HttpServer sink = com.sun.net.httpserver.HttpServer.create(
|
||||
new java.net.InetSocketAddress(java.net.InetAddress.getLoopbackAddress(), 0), 0);
|
||||
sink.createContext("/", exchange -> {
|
||||
java.io.ByteArrayOutputStream buf = new java.io.ByteArrayOutputStream();
|
||||
byte[] chunk = new byte[4096];
|
||||
int n;
|
||||
while ((n = exchange.getRequestBody().read(chunk)) > 0) {
|
||||
buf.write(chunk, 0, n);
|
||||
}
|
||||
receivedPayloads.add(new String(buf.toByteArray(), java.nio.charset.StandardCharsets.UTF_8));
|
||||
exchange.sendResponseHeaders(200, -1);
|
||||
exchange.close();
|
||||
});
|
||||
sink.start();
|
||||
try {
|
||||
String regBody = "{\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"callbackUrl\":\"http://localhost:" + sink.getAddress().getPort() + "/cb\"}";
|
||||
assertEquals(0, client.post("/oapi/flow/callback/reg?schId=1", regBody, JsonNode.class)
|
||||
.getBody().get("code").asInt(), "回调注册(指向本地 sink)应探活通过");
|
||||
Long instanceId = unwrap(client.post("/oapi/flow/apply?schId=1", "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"ap-1\",\"bizTitle\":\"办理件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}", JsonNode.class).getBody())
|
||||
.get("instanceId").asLong();
|
||||
Long taskId = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId();
|
||||
|
||||
// 缺 operatorName → 业务码拒绝
|
||||
ResponseEntity<JsonNode> noName = client.post(
|
||||
"/oapi/flow/task/" + taskId + "/approve?schId=1&operatorId=101", "{}", JsonNode.class);
|
||||
assertNotEquals(0, noName.getBody().get("code").asInt(), "缺 operatorName 应被拒绝");
|
||||
|
||||
// 非办理人(999)→ 业务码拒绝(code!=0,HTTP 200),任务仍在待办
|
||||
ResponseEntity<JsonNode> forged = client.post(
|
||||
"/oapi/flow/task/" + taskId + "/approve?schId=1&operatorId=999&operatorName=路人",
|
||||
"{\"opinion\":\"越权\"}", JsonNode.class);
|
||||
assertEquals(200, forged.getStatusCodeValue());
|
||||
assertNotEquals(0, forged.getBody().get("code").asInt(), "非办理人应被业务码拒绝");
|
||||
assertEquals(1, taskRepository.findByInstanceIdAndStatus(instanceId, 0).size(),
|
||||
"越权后任务仍在待办");
|
||||
|
||||
// 办理人(101)同意 → 任务 1、单节点单人会签放行 → 办件 20;opinion 进回调 payload
|
||||
ResponseEntity<JsonNode> ok = client.post(
|
||||
"/oapi/flow/task/" + taskId + "/approve?schId=1&operatorId=101&operatorName=甲",
|
||||
"{\"opinion\":\"同意,情况属实\"}", JsonNode.class);
|
||||
assertEquals(0, ok.getBody().get("code").asInt(), "办理人同意应成功");
|
||||
assertEquals(1, taskRepository.findById(taskId).get().getStatus());
|
||||
assertEquals(20, instanceRepository.findById(instanceId).get().getStatus());
|
||||
assertTrue(awaitPayloadContains(receivedPayloads, "同意,情况属实"),
|
||||
"opinion 应透传到回调 payload");
|
||||
|
||||
// 第二件:拒绝无意见 → 业务码拒绝;带意见 → 任务 2、办件 30
|
||||
Long id2 = unwrap(client.post("/oapi/flow/apply?schId=1", "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"ap-2\",\"bizTitle\":\"驳回件\","
|
||||
+ "\"submitterId\":202,\"submitterName\":\"李四\"}", JsonNode.class).getBody())
|
||||
.get("instanceId").asLong();
|
||||
Long taskId2 = taskRepository.findByInstanceIdAndStatus(id2, 0).get(0).getId();
|
||||
ResponseEntity<JsonNode> noOpinion = client.post(
|
||||
"/oapi/flow/task/" + taskId2 + "/reject?schId=1&operatorId=101&operatorName=甲",
|
||||
"{}", JsonNode.class);
|
||||
assertNotEquals(0, noOpinion.getBody().get("code").asInt(), "拒绝必须填写意见");
|
||||
ResponseEntity<JsonNode> rejected = client.post(
|
||||
"/oapi/flow/task/" + taskId2 + "/reject?schId=1&operatorId=101&operatorName=甲",
|
||||
"{\"opinion\":\"材料不全\"}", JsonNode.class);
|
||||
assertEquals(0, rejected.getBody().get("code").asInt());
|
||||
assertEquals(2, taskRepository.findById(taskId2).get().getStatus());
|
||||
assertEquals(30, instanceRepository.findById(id2).get().getStatus());
|
||||
assertTrue(awaitPayloadContains(receivedPayloads, "材料不全"),
|
||||
"拒绝意见也应透传到回调 payload");
|
||||
} finally {
|
||||
sink.stop(0);
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询等异步回调推送(AFTER_COMMIT + 推送池),最长约 5s */
|
||||
private boolean awaitPayloadContains(List<String> payloads, String needle) throws InterruptedException {
|
||||
for (int i = 0; i < 25; i++) {
|
||||
for (String p : payloads) {
|
||||
if (p.contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Thread.sleep(200);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private JsonNode unwrap(JsonNode body) {
|
||||
return body.get("data");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user