[v2.1] A 应用内审批地基:oapi 增 GET /tasks/by-biz——按单据查当前人待办(0/1/N 均 200 空数组不泄露,校 schId/bizType/assignee 三 match,IT 11 例)
build / build-push (push) Successful in 2m2s
build / build-push (push) Successful in 2m2s
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
package com.yunhai.flowcenter.oapi;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.service.OapiFlowService;
|
||||
import com.yunhai.flowcenter.support.FlowTestSupport;
|
||||
import com.yunhai.flowcenter.support.OapiSignedClient;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* GET /oapi/flow/tasks/by-biz 契约(v2.1 应用内审批·业务详情卡片数据源)。
|
||||
* 核心是 0/1/N 三种计数语义与"不探测他人办件":办件不存在、错校、错 bizType、他人待办一律
|
||||
* HTTP 200 + tasks=[],出参不区分(区分即等于告诉调用方"这个单据上有个待办,只是不是你的")。
|
||||
* 注意:本类必须单类独跑(平台 IT 判据)。
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
class OapiFlowTasksByBizTest extends FlowTestSupport {
|
||||
|
||||
private static final String APP_KEY = "100018";
|
||||
private static final String APP_SECRET = "svc-hall-dev-secret-100018";
|
||||
private static final String BIZ = "notice";
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private OapiFlowService flowService;
|
||||
|
||||
private OapiSignedClient client;
|
||||
|
||||
@TestConfiguration
|
||||
static class StubConfig {
|
||||
@Bean
|
||||
@Primary
|
||||
public com.yunhai.flowcenter.stub.StubUserClient stubUserClient() {
|
||||
return new com.yunhai.flowcenter.stub.StubUserClient();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cleanDb();
|
||||
client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
}
|
||||
|
||||
/** 代发起一件并回带 instanceId(bizId 唯一,供 by-biz 定位) */
|
||||
private Long applyOne(FlwDef def, String bizId, String title) {
|
||||
ResponseEntity<JsonNode> resp = client.post("/oapi/flow/apply?schId=1", "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"" + BIZ + "\",\"bizId\":\"" + bizId + "\",\"bizTitle\":\"" + title + "\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}", JsonNode.class);
|
||||
assertEquals(0, resp.getBody().get("code").asInt(), "代发起应成功:" + resp.getBody());
|
||||
return resp.getBody().get("data").get("instanceId").asLong();
|
||||
}
|
||||
|
||||
private JsonNode byBiz(String bizId, int operatorId) {
|
||||
return byBiz(SCH, BIZ, bizId, operatorId);
|
||||
}
|
||||
|
||||
private JsonNode byBiz(long schId, String bizType, String bizId, int operatorId) {
|
||||
ResponseEntity<JsonNode> resp = client.get("/oapi/flow/tasks/by-biz?schId=" + schId
|
||||
+ "&bizType=" + bizType + "&bizId=" + bizId + "&operatorId=" + operatorId, JsonNode.class);
|
||||
assertEquals(200, resp.getStatusCodeValue(), "by-biz 恒 HTTP 200(空结果不报错)");
|
||||
assertEquals(0, resp.getBody().get("code").asInt(), "业务码须为成功:" + resp.getBody());
|
||||
return resp.getBody().get("data");
|
||||
}
|
||||
|
||||
/** 签名强制:无签名头 401(与其余 /oapi 端点同纪律) */
|
||||
@Test
|
||||
void requiresAppSignature_401() {
|
||||
assertEquals(401, client.getRaw(
|
||||
"/oapi/flow/tasks/by-biz?schId=1&bizType=notice&bizId=x&operatorId=101").getStatusCodeValue());
|
||||
}
|
||||
|
||||
/** 缺参/非正数 → 业务码拒绝(身份参数不允许"可选"降级成越权查询) */
|
||||
@Test
|
||||
void badParams_rejected() {
|
||||
assertNotEqualsZero(client.get("/oapi/flow/tasks/by-biz?schId=1&bizType=notice&bizId=x",
|
||||
JsonNode.class).getBody(), "缺 operatorId 应拒绝");
|
||||
assertNotEqualsZero(client.get("/oapi/flow/tasks/by-biz?schId=1&bizId=x&operatorId=101",
|
||||
JsonNode.class).getBody(), "缺 bizType 应拒绝");
|
||||
assertNotEqualsZero(client.get("/oapi/flow/tasks/by-biz?schId=0&bizType=notice&bizId=x&operatorId=101",
|
||||
JsonNode.class).getBody(), "schId<=0 应拒绝");
|
||||
assertNotEqualsZero(client.get("/oapi/flow/tasks/by-biz?schId=1&bizType=notice&bizId=x&operatorId=0",
|
||||
JsonNode.class).getBody(), "operatorId<=0 应拒绝");
|
||||
}
|
||||
|
||||
/** 0 条·无办件:instanceId/status/currentNodes 均 null,tasks 空数组(不是 null,前端不必判空指针) */
|
||||
@Test
|
||||
void noInstance_empty() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
saveAndPublishDef("bb1", "无办件", linearDef("ybb", "审核"));
|
||||
JsonNode data = byBiz("ghost-1", 101);
|
||||
assertTrue(data.get("instanceId").isNull(), "无办件 instanceId 须 null");
|
||||
assertTrue(data.get("status").isNull(), "无办件 status 须 null");
|
||||
assertTrue(data.get("currentNodes").isNull(), "无办件 currentNodes 须 null");
|
||||
assertEquals(0, data.get("tasks").size());
|
||||
assertEquals("notice", data.get("bizType").asText(), "入参 bizType 原样回显");
|
||||
assertEquals("ghost-1", data.get("bizId").asText(), "入参 bizId 原样回显");
|
||||
}
|
||||
|
||||
/** 0 条·办件存在但本人无待办(已办结):仍 200 + 空,且带出办件状态供卡片渲染终态 */
|
||||
@Test
|
||||
void finishedInstance_emptyWithStatus() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("bb2", "已办结", linearDef("ybb", "审核"));
|
||||
Long instanceId = applyOne(def, "done-1", "已办结通知");
|
||||
Long taskId = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId();
|
||||
assertEquals(0, client.post("/oapi/flow/task/" + taskId
|
||||
+ "/approve?schId=1&operatorId=101&operatorName=甲", "{}", JsonNode.class)
|
||||
.getBody().get("code").asInt(), "办理人同意应成功");
|
||||
JsonNode data = byBiz("done-1", 101);
|
||||
assertEquals(0, data.get("tasks").size(), "办结后本人无待办");
|
||||
assertEquals(20, data.get("status").asInt(), "终态可展示:回带办件状态");
|
||||
assertEquals(instanceId, data.get("instanceId").asLong());
|
||||
}
|
||||
|
||||
/** 1 条·单出口:outlets 空数组(与 FlowQueryService.detail 同口径,单出口不给前端多余分支) */
|
||||
@Test
|
||||
void oneTask_singleOutlet_noOutlets() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("bb3", "单出口", linearDef("ybb", "审核"));
|
||||
Long instanceId = applyOne(def, "one-1", "单出口通知");
|
||||
Long taskId = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId();
|
||||
JsonNode data = byBiz("one-1", 101);
|
||||
assertEquals(1, data.get("tasks").size());
|
||||
JsonNode task = data.get("tasks").get(0);
|
||||
assertEquals(taskId, task.get("taskId").asLong());
|
||||
assertEquals("n1", task.get("nodeId").asText());
|
||||
assertEquals("审核", task.get("nodeName").asText());
|
||||
assertNotNull(task.get("arriveTime").asText());
|
||||
assertEquals(0, task.get("outlets").size(), "单出口不透出 outlets");
|
||||
assertEquals(10, data.get("status").asInt(), "审批中");
|
||||
// currentNodes 是办件上的节点 **id** 逗号串(与 /oapi/flow/instances、progress 同一原文口径,
|
||||
// 本端点不做名字化——改名会让同名字段在两个 oapi 里语义分叉)。人读节点名取 tasks[].nodeName。
|
||||
assertEquals("n1", data.get("currentNodes").asText());
|
||||
}
|
||||
|
||||
/** 1 条·多出口:outlets 逐字给出 name/toNodeId/toNodeName,供卡片"通过"弹窗选出口 */
|
||||
@Test
|
||||
void oneTask_multiOutlet_exposesOutlets() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
mapDept("kb", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("bb4", "多出口", multiOutletDef("ybb", "kb"));
|
||||
Long instanceId = applyOne(def, "mo-1", "多出口通知");
|
||||
JsonNode data = byBiz("mo-1", 101);
|
||||
assertEquals(instanceId, data.get("instanceId").asLong(), "多出口件同样回带办件定位");
|
||||
JsonNode task = data.get("tasks").get(0);
|
||||
assertEquals("入口审核", task.get("nodeName").asText());
|
||||
JsonNode outlets = task.get("outlets");
|
||||
assertEquals(2, outlets.size(), "n1 出边 2 条须全部透出");
|
||||
assertEquals("转乙科", outlets.get(0).get("name").asText());
|
||||
assertEquals("n2", outlets.get(0).get("toNodeId").asText());
|
||||
assertEquals("复核", outlets.get(0).get("toNodeName").asText());
|
||||
assertEquals("通过", outlets.get(1).get("name").asText());
|
||||
assertEquals("end", outlets.get(1).get("toNodeId").asText());
|
||||
}
|
||||
|
||||
/** N 条:并行两支都指派同一人 → tasks 两条、按 taskId 升序;卡片据此引导列表/任务页,不猜选 */
|
||||
@Test
|
||||
void multiTask_returnsAllSorted() {
|
||||
mapDept("ka", 101, "甲");
|
||||
mapDept("kb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("bb5", "并行同人", parallelDef("ka", "kb"));
|
||||
Long instanceId = applyOne(def, "mt-1", "多待办通知");
|
||||
List<Long> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0)
|
||||
.stream().map(com.yunhai.flowcenter.entity.FlwTask::getId)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
assertEquals(2, pending.size(), "前置数据:本人两条并行待办");
|
||||
JsonNode data = byBiz("mt-1", 101);
|
||||
JsonNode tasks = data.get("tasks");
|
||||
assertEquals(2, tasks.size(), "N 条全返回,不合并、不猜选");
|
||||
long first = tasks.get(0).get("taskId").asLong();
|
||||
long second = tasks.get(1).get("taskId").asLong();
|
||||
assertTrue(first < second, "须按 taskId 升序:" + first + " -> " + second);
|
||||
}
|
||||
|
||||
/** 他人待办 → 空(关键防探测:并行两支分属两人时,非办理人查同一 bizId 得到 tasks=[]) */
|
||||
@Test
|
||||
void otherAssignee_empty() {
|
||||
mapDept("ka", 101, "甲");
|
||||
mapDept("kb", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("bb6", "并行他人", parallelDef("ka", "kb"));
|
||||
Long instanceId = applyOne(def, "ot-1", "他人待办通知");
|
||||
assertEquals(2, taskRepository.findByInstanceIdAndStatus(instanceId, 0).size(),
|
||||
"前置数据:两条待办分属 101/102");
|
||||
JsonNode asOutsider = byBiz("ot-1", 999);
|
||||
assertEquals(0, asOutsider.get("tasks").size(), "非办理人 → 空");
|
||||
assertNotNull(asOutsider.get("instanceId"), "但办件定位字段照常回带");
|
||||
assertEquals(1, byBiz("ot-1", 101).get("tasks").size(), "办理人本人查得到");
|
||||
// 出参不得泄露他人办理人
|
||||
assertFalse(byBiz("ot-1", 101).toString().contains("102"), "不得透出他人 assigneeId");
|
||||
assertFalse(byBiz("ot-1", 101).toString().contains("乙"), "不得透出他人姓名");
|
||||
}
|
||||
|
||||
/** 错学校 / 错 bizType → 空(租户与业务类型收敛在定位查询本身) */
|
||||
@Test
|
||||
void wrongSchoolOrBizType_empty() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("bb7", "跨租户", linearDef("ybb", "审核"));
|
||||
applyOne(def, "ws-1", "跨租户通知");
|
||||
JsonNode otherSchool = byBiz(2L, BIZ, "ws-1", 101);
|
||||
assertEquals(0, otherSchool.get("tasks").size(), "错学校 → 空");
|
||||
assertTrue(otherSchool.get("instanceId").isNull(), "错学校不得回带 instanceId");
|
||||
JsonNode otherType = byBiz(SCH, "svc_item", "ws-1", 101);
|
||||
assertEquals(0, otherType.get("tasks").size(), "错 bizType → 空");
|
||||
assertTrue(otherType.get("instanceId").isNull(), "错 bizType 不得回带 instanceId");
|
||||
}
|
||||
|
||||
/** 定义缺失(def 行被清理)→ 任务照常返回,outlets 降级空数组,不 500 */
|
||||
@Test
|
||||
void missingDef_degradesWithoutOutlets() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("bb8", "定义缺失", linearDef("ybb", "审核"));
|
||||
Long instanceId = applyOne(def, "md-1", "定义缺失通知");
|
||||
Long taskId = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId();
|
||||
defRepository.delete(def);
|
||||
JsonNode task = byBiz("md-1", 101).get("tasks").get(0);
|
||||
assertEquals(0, task.get("outlets").size(), "无定义时 outlet 计算降级空");
|
||||
assertEquals(taskId, task.get("taskId").asLong(), "任务本身照常返回");
|
||||
}
|
||||
|
||||
/** 服务层直调:空白 bizId 拒绝(URL 上 bizId= 空串不得退化成"查全部") */
|
||||
@Test
|
||||
void service_blankBizParams_rejected() {
|
||||
assertThrows(BizException.class, () -> flowService.tasksByBiz(1L, "notice", " ", 101));
|
||||
assertThrows(BizException.class, () -> flowService.tasksByBiz(1L, null, "1", 101));
|
||||
assertThrows(BizException.class, () -> flowService.tasksByBiz(null, "notice", "1", 101));
|
||||
}
|
||||
|
||||
private static void assertNotEqualsZero(JsonNode body, String message) {
|
||||
org.junit.jupiter.api.Assertions.assertNotEquals(Integer.valueOf(0),
|
||||
Integer.valueOf(body.get("code").asInt()), message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user