[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:
@@ -32,6 +32,7 @@ import java.util.List;
|
|||||||
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
|
* 应用内审批(2026-09 第三步,原"刻意不提供审批动作"立场由该方案修订:登录身份由调用方
|
||||||
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
|
* 应用按其 CAS 会话保证并以 operatorId 传入,本端按任务办理人逐字复核,审计链不断):
|
||||||
* - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办
|
* - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办
|
||||||
|
* - GET /oapi/flow/tasks/by-biz?schId=&bizType=&bizId=&operatorId= → data=当前人在该单据的待办(0/1/N)
|
||||||
* - GET /oapi/flow/instances?schId=&bizType=&status=&page=&size= → data=分页办件
|
* - GET /oapi/flow/instances?schId=&bizType=&status=&page=&size= → data=分页办件
|
||||||
* - POST /oapi/flow/task/{taskId}/approve?schId=&operatorId=&operatorName=(body: opinion/edgeName)
|
* - POST /oapi/flow/task/{taskId}/approve?schId=&operatorId=&operatorName=(body: opinion/edgeName)
|
||||||
* - POST /oapi/flow/task/{taskId}/reject?schId=&operatorId=&operatorName=(body: opinion 必填)
|
* - POST /oapi/flow/task/{taskId}/reject?schId=&operatorId=&operatorName=(body: opinion 必填)
|
||||||
@@ -158,6 +159,20 @@ public class OapiFlowController {
|
|||||||
return flowService.pageTodoTasks(schId, assigneeId, bizType, page, size);
|
return flowService.pageTodoTasks(schId, assigneeId, bizType, page, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按业务单据查当前人待办(v2.1 业务详情卡片数据源):与 tasks/todo 互补——那个是"人→单据",
|
||||||
|
* 这个是"单据→我的待办"。0/1/N 三种计数都由 HTTP 200 + tasks 数组表达,办件不存在与
|
||||||
|
* 他人待办同样出空(不用于探测)。operatorId 由子应用后端从 CAS 会话取,前端不可传。
|
||||||
|
*/
|
||||||
|
@GetMapping("/tasks/by-biz")
|
||||||
|
public OapiModels.ByBizTasksVo tasksByBiz(
|
||||||
|
@RequestParam("schId") Long schId,
|
||||||
|
@RequestParam("bizType") String bizType,
|
||||||
|
@RequestParam("bizId") String bizId,
|
||||||
|
@RequestParam("operatorId") Integer operatorId) {
|
||||||
|
return flowService.tasksByBiz(schId, bizType, bizId, operatorId);
|
||||||
|
}
|
||||||
|
|
||||||
/** 办件分页(oapi):bizType/status 可选过滤 */
|
/** 办件分页(oapi):bizType/status 可选过滤 */
|
||||||
@GetMapping("/instances")
|
@GetMapping("/instances")
|
||||||
public BasePageResult<OapiModels.InstanceItemVo> instances(
|
public BasePageResult<OapiModels.InstanceItemVo> instances(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.yunhai.flowcenter.model;
|
|||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -266,4 +267,45 @@ public final class OapiModels {
|
|||||||
/** 多出口节点办理时所选出口名(单出口可省) */
|
/** 多出口节点办理时所选出口名(单出口可省) */
|
||||||
private String edgeName;
|
private String edgeName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按业务单据定位的当前人待办(tasks/by-biz 出参,v2.1 应用内审批卡片数据源)。
|
||||||
|
* 无办件与"有办件但本人无待办"同为 tasks=[] + HTTP 200,出参不区分二者(防探测)。
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public static class ByBizTasksVo {
|
||||||
|
/** 办件 id;无办件时 null */
|
||||||
|
private Long instanceId;
|
||||||
|
private String bizType;
|
||||||
|
private String bizId;
|
||||||
|
/** 办件状态;无办件时 null */
|
||||||
|
private Integer status;
|
||||||
|
/**
|
||||||
|
* 办件当前活跃节点——**节点 id** 逗号串,原文照抄 {@code flw_instance.current_nodes},
|
||||||
|
* 与 {@code instances}/{@code progress} 同口径(不是节点名,渲染人读名称请取
|
||||||
|
* {@code tasks[].nodeName})。无办件时 null。
|
||||||
|
*/
|
||||||
|
private String currentNodes;
|
||||||
|
/** 当前人 PENDING 任务,按 taskId 升序;无待办为空数组 */
|
||||||
|
private List<ByBizTaskVo> tasks = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** by-biz 单条待办:只含办理该任务必需的定位信息,不透出他人办理人/意见 */
|
||||||
|
@Data
|
||||||
|
public static class ByBizTaskVo {
|
||||||
|
private Long taskId;
|
||||||
|
private String nodeId;
|
||||||
|
private String nodeName;
|
||||||
|
private String arriveTime;
|
||||||
|
/** 出口列表;单出口节点为空数组(与 FlowQueryService.detail 的 outlets 口径一致) */
|
||||||
|
private List<ByBizOutletVo> outlets = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** by-biz 出口(多出口节点办理时须选其一,透传给 approve 的 edgeName) */
|
||||||
|
@Data
|
||||||
|
public static class ByBizOutletVo {
|
||||||
|
private String name;
|
||||||
|
private String toNodeId;
|
||||||
|
private String toNodeName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,79 @@ public class OapiFlowService {
|
|||||||
return toPageResult(p.getTotalElements(), items, page, size);
|
return toPageResult(p.getTotalElements(), items, page, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按业务单据查当前人待办(v2.1 应用内审批的业务详情卡片数据源):
|
||||||
|
* 补齐 tasks/todo 缺的那一侧——todo 是"人 → 单据列表",本方法是"单据 → 我的待办",
|
||||||
|
* 子应用据此判断"当前人是否该办件的待办人",不必再拉全量待办在前端过滤。
|
||||||
|
* <p>治理口径:
|
||||||
|
* <ul>
|
||||||
|
* <li>只返回 {@code assigneeId=operatorId} 且 status=PENDING 的任务;他人待办与"办件不存在"
|
||||||
|
* 一律出空数组(HTTP 200),出参不区分二者,避免用本端点探测别人的办件;</li>
|
||||||
|
* <li>租户收敛在定位查询本身({@code findBySchIdAndBizTypeAndBizId}),bizType 不匹配即空;</li>
|
||||||
|
* <li>{@code outlets} 只为当前人的任务计算,且出边 >1 才透出(与
|
||||||
|
* {@code FlowQueryService.detail} 的办理面板口径一致——单出口无需选择,不给前端多余分支);</li>
|
||||||
|
* <li>不透出他人办理人、意见或定义内部 id;taskId 是办理必需的运行时身份,保留。</li>
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
public OapiModels.ByBizTasksVo tasksByBiz(Long schId, String bizType, String bizId, Integer operatorId) {
|
||||||
|
if (schId == null || schId <= 0 || operatorId == null || operatorId <= 0) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:schId/operatorId(均须为正)");
|
||||||
|
}
|
||||||
|
String cleanBizType = emptyToNull(bizType);
|
||||||
|
String cleanBizId = emptyToNull(bizId);
|
||||||
|
if (cleanBizType == null || cleanBizId == null) {
|
||||||
|
throw new BizException(ErrorCode.PARAM_ERROR, "缺少必要参数:bizType/bizId");
|
||||||
|
}
|
||||||
|
OapiModels.ByBizTasksVo vo = new OapiModels.ByBizTasksVo();
|
||||||
|
vo.setBizType(cleanBizType);
|
||||||
|
vo.setBizId(cleanBizId);
|
||||||
|
FlwInstance instance = instanceRepository
|
||||||
|
.findBySchIdAndBizTypeAndBizId(schId, cleanBizType, cleanBizId).orElse(null);
|
||||||
|
if (instance == null) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
vo.setInstanceId(instance.getId());
|
||||||
|
vo.setStatus(instance.getStatus());
|
||||||
|
vo.setCurrentNodes(instance.getCurrentNodes());
|
||||||
|
List<FlwTask> mine = taskRepository
|
||||||
|
.findByInstanceIdAndStatus(instance.getId(), FlwEnums.TaskStatus.PENDING.getCode()).stream()
|
||||||
|
.filter(t -> operatorId.equals(t.getAssigneeId()))
|
||||||
|
.sorted(java.util.Comparator.comparing(FlwTask::getId))
|
||||||
|
.collect(java.util.stream.Collectors.toList());
|
||||||
|
if (mine.isEmpty()) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
FlwDef def = defRepository.findById(instance.getDefId()).orElse(null);
|
||||||
|
FlowDefModel model = def == null ? null : flowEngine.parseModel(def);
|
||||||
|
for (FlwTask t : mine) {
|
||||||
|
vo.getTasks().add(toByBizTaskVo(t, model));
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OapiModels.ByBizTaskVo toByBizTaskVo(FlwTask t, FlowDefModel model) {
|
||||||
|
OapiModels.ByBizTaskVo vo = new OapiModels.ByBizTaskVo();
|
||||||
|
vo.setTaskId(t.getId());
|
||||||
|
vo.setNodeId(t.getNodeId());
|
||||||
|
vo.setNodeName(t.getNodeName());
|
||||||
|
vo.setArriveTime(t.getArriveTime() == null ? null : t.getArriveTime().toString());
|
||||||
|
if (model == null) {
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
List<FlowDefModel.Edge> outs = model.outEdges(t.getNodeId());
|
||||||
|
if (outs.size() > 1) {
|
||||||
|
for (FlowDefModel.Edge e : outs) {
|
||||||
|
FlowDefModel.Node to = model.node(e.to);
|
||||||
|
OapiModels.ByBizOutletVo outlet = new OapiModels.ByBizOutletVo();
|
||||||
|
outlet.setName(e.name);
|
||||||
|
outlet.setToNodeId(e.to);
|
||||||
|
outlet.setToNodeName(to == null || to.name == null || to.name.isEmpty() ? e.to : to.name);
|
||||||
|
vo.getOutlets().add(outlet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vo;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 同意(oapi,应用内审批):operatorId 由调用方应用按其登录态传入,本端按任务办理人
|
* 同意(oapi,应用内审批):operatorId 由调用方应用按其登录态传入,本端按任务办理人
|
||||||
* 复核({@code task.assigneeId == operatorId},不满足→业务码拒绝,应用签名不替代"谁在办")。
|
* 复核({@code task.assigneeId == operatorId},不满足→业务码拒绝,应用签名不替代"谁在办")。
|
||||||
|
|||||||
@@ -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