feat(msgcenter): 卡片直批——MsgApprovalProxyService 定位待办办 approve/reject(B3 §2):身份全取会话防代批、bizId=办件 id 匹配 instanceId、0/>1 匹配明确文案、业务拒绝原文透出、不可达降级不阻塞
This commit is contained in:
@@ -20,6 +20,12 @@ public class MsgCenterProperties {
|
||||
/** UserMsg 服务基址;空 = 未接入(代理降级返回空,前端铃铛维持原表现) */
|
||||
private String usermsgBaseUrl = "";
|
||||
|
||||
/**
|
||||
* flow-center 服务基址(卡片直批 B3 出站,/oapi/flow/**);空 = 走服务目录
|
||||
* flowcenter(PlatformService 枚举兜底)。不可达时直批降级提示,不阻塞消息卡片。
|
||||
*/
|
||||
private String flowcenterBaseUrl = "";
|
||||
|
||||
/**
|
||||
* 浏览器可达的 UserMsg WS 端点(实时推送握手地址);空 = 不下发 WS 地址,
|
||||
* 前端维持"打开页面拉一次 + 操作后手动刷新"的既有表现。
|
||||
|
||||
@@ -2,10 +2,13 @@ package com.yunhai.userhome.controller.api;
|
||||
|
||||
import com.yunhai.platform.utils.model.BoaUser;
|
||||
import com.yunhai.userhome.config.Config;
|
||||
import com.yunhai.userhome.model.MsgHandleRequest;
|
||||
import com.yunhai.userhome.service.MsgApprovalProxyService;
|
||||
import com.yunhai.userhome.service.MsgCenterProxyService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -24,6 +27,7 @@ import java.util.Map;
|
||||
public class MessageCenterController {
|
||||
|
||||
private final MsgCenterProxyService proxy;
|
||||
private final MsgApprovalProxyService approvalProxy;
|
||||
|
||||
/** schId=0 时推断:主学校 → 主归属组(bureau)→ 0(对齐 MessageController 旧口径) */
|
||||
private Integer resolveSchId(BoaUser user) {
|
||||
@@ -73,4 +77,16 @@ public class MessageCenterController {
|
||||
public void readAll(BoaUser authUserWithDept) {
|
||||
proxy.readAll(authUserWithDept.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片直批(B3 §2):同意/拒绝消息对应的审批待办。
|
||||
* 红线:operatorId/schId/operatorName 一律取当前会话,绝不接受前端传参(防越权代批)。
|
||||
*
|
||||
* @return {ok, message}——ok=false 时 message 为可直接展示的文案(已处理/多节点/业务拒绝/不可达降级)
|
||||
*/
|
||||
@PostMapping("msgCenter/handle")
|
||||
public Map<String, Object> handle(BoaUser authUserWithDept, @RequestBody MsgHandleRequest request) {
|
||||
return approvalProxy.handle(authUserWithDept.getId(), resolveSchId(authUserWithDept),
|
||||
authUserWithDept.getName(), request.getMsgId(), request.getAction(), request.getOpinion());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yunhai.userhome.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 卡片直批入参(B3 §2)。只含消息定位与动作——operatorId/schId/operatorName
|
||||
* 一律服务端从当前会话取,绝不从前端接收(防越权代批)。
|
||||
*/
|
||||
@Data
|
||||
public class MsgHandleRequest {
|
||||
|
||||
private Long msgId;
|
||||
|
||||
/** approve | reject */
|
||||
private String action;
|
||||
|
||||
/** 办理意见:拒绝必填,同意可选 */
|
||||
private String opinion;
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.yunhai.platform.openapi.core.TargetSpec;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.userhome.config.MsgCenterProperties;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 卡片直批代理(方案 §2,B3):消息卡片上的「同意/拒绝」在 UserHome 服务端完成
|
||||
* 消息定位 → flow-center 待办匹配 → 办理 → 标已读 全链路。
|
||||
* <ul>
|
||||
* <li><b>红线</b>:operatorId / schId / operatorName 一律由控制器从当前 CAS 会话取
|
||||
* (BoaUser),绝不接受前端传参——防越权代批;flow-center 侧再按任务办理人逐字复核,双层防护。</li>
|
||||
* <li>taskId 定位(§2.3):我的待办(tasks/todo,assigneeId=登录人)里按 bizId 精确匹配。
|
||||
* 实测契约:flow-center 待办消息 bizId=办件 id(instanceId),故匹配键 = instanceId,
|
||||
* bizId(业务记录 id)作为通用回落;bizType 过滤键用消息 jumpApp(=流程业务类型,
|
||||
* 消息 bizType「flow.task」是消息分类,与待办的 bizType 不同命名空间,勿混)。</li>
|
||||
* <li>匹配 0 条=已处理或已失效;>1 条(同办件多出口/多节点)=请前往详情页办理(一期不做歧义猜选)。</li>
|
||||
* <li>出站走 {@link YschOpenCall} 签名直调,目标 flowcenter(服务目录/枚举兜底,
|
||||
* flowcenter-base-url 显式覆盖优先)。flow-center 不可达降级为提示,不影响消息卡片其余功能。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class MsgApprovalProxyService {
|
||||
|
||||
private static final String TARGET_FLOWCENTER = "flowcenter";
|
||||
|
||||
/** 我的待办分页拉取上限:5 页 × 100 条(todo 量级远小于此,防御性封顶) */
|
||||
private static final int TODO_MAX_PAGES = 5;
|
||||
private static final int TODO_PAGE_SIZE = 100;
|
||||
|
||||
private final YschOpenCall openCall;
|
||||
private final MsgCenterProxyService msgProxy;
|
||||
private final MsgCenterProperties properties;
|
||||
|
||||
public MsgApprovalProxyService(YschOpenCall openCall, MsgCenterProxyService msgProxy,
|
||||
MsgCenterProperties properties) {
|
||||
this.openCall = openCall;
|
||||
this.msgProxy = msgProxy;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 办理消息对应的审批待办。action=approve|reject;reject 意见必填。
|
||||
*
|
||||
* @return {ok, message}——ok=false 时 message 为可直接展示的用户文案
|
||||
*/
|
||||
public Map<String, Object> handle(Integer userId, Integer schId, String operatorName,
|
||||
Long msgId, String action, String opinion) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
boolean approve = "approve".equals(action);
|
||||
if (!approve && !"reject".equals(action)) {
|
||||
return fail(out, "不支持的操作类型");
|
||||
}
|
||||
if (!approve && !StringUtils.hasText(opinion)) {
|
||||
return fail(out, "拒绝必须填写意见");
|
||||
}
|
||||
|
||||
JsonNode msg = msgProxy.findMessage(userId, msgId);
|
||||
if (msg == null) {
|
||||
return fail(out, "消息不存在或已失效");
|
||||
}
|
||||
String bizId = text(msg, "bizId");
|
||||
if (!StringUtils.hasText(bizId)) {
|
||||
return fail(out, "该消息不含可办理的业务标识");
|
||||
}
|
||||
String flowBizType = text(msg, "jumpApp");
|
||||
|
||||
List<JsonNode> matched = matchTodoTasks(schId, userId, flowBizType, bizId.trim());
|
||||
if (matched == null) {
|
||||
// flow-center 不可达 ≠ 已处理:降级提示,不误导用户
|
||||
return fail(out, "流程中心暂不可用,请稍后重试");
|
||||
}
|
||||
if (matched.isEmpty()) {
|
||||
return fail(out, "该待办已处理或已失效");
|
||||
}
|
||||
if (matched.size() > 1) {
|
||||
return fail(out, "同一办件匹配到多个待办节点,请前往详情页办理");
|
||||
}
|
||||
long taskId = matched.get(0).path("taskId").asLong();
|
||||
|
||||
try {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("schId", String.valueOf(schId == null ? 0 : schId));
|
||||
query.put("operatorId", String.valueOf(userId));
|
||||
query.put("operatorName", operatorName == null ? "" : operatorName);
|
||||
Map<String, Object> body = new HashMap<>();
|
||||
body.put("opinion", opinion);
|
||||
postFlowcenter("/oapi/flow/task/" + taskId + "/" + action, query, body);
|
||||
} catch (DException e) {
|
||||
// flow-center 业务码拒绝(非办理人/多出口需 edgeName 等)——原文透出给用户
|
||||
log.info("[msgcenter] 直批被 flow-center 业务拒绝 msgId={} taskId={}:{}", msgId, taskId, e.getMessage());
|
||||
return fail(out, e.getMessage());
|
||||
} catch (Exception e) {
|
||||
log.warn("[msgcenter] flow-center 直批不可达(降级提示)msgId={}:{}", msgId, e.toString());
|
||||
return fail(out, "流程中心暂不可用,请稍后重试");
|
||||
}
|
||||
|
||||
msgProxy.read(userId, java.util.Collections.singletonList(msgId));
|
||||
out.put("ok", true);
|
||||
out.put("message", "办理成功");
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 我的待办中精确匹配目标办件:先按 flowBizType(=消息 jumpApp)过滤(未声明则不过滤),
|
||||
* 再按 instanceId(flow-center 待办消息约定)或 bizId(业务方自发消息约定)匹配。
|
||||
*
|
||||
* @return 匹配行(可能为空);flow-center 不可达返回 null(区别于「已处理」的 0 匹配)
|
||||
*/
|
||||
private List<JsonNode> matchTodoTasks(Integer schId, Integer userId, String flowBizType, String bizId) {
|
||||
List<JsonNode> matched = new ArrayList<>();
|
||||
for (int page = 1; page <= TODO_MAX_PAGES; page++) {
|
||||
List<JsonNode> content;
|
||||
try {
|
||||
content = fetchTodoPage(schId, userId, flowBizType, page);
|
||||
} catch (Exception e) {
|
||||
log.warn("[msgcenter] flow-center 待办查询不可达 msgId 定位失败:{}", e.toString());
|
||||
return null;
|
||||
}
|
||||
for (JsonNode task : content) {
|
||||
String taskBizType = text(task, "bizType");
|
||||
if (StringUtils.hasText(flowBizType) && !flowBizType.equals(taskBizType)) {
|
||||
continue;
|
||||
}
|
||||
String instanceId = task.has("instanceId") && task.get("instanceId").isNumber()
|
||||
? String.valueOf(task.get("instanceId").asLong()) : null;
|
||||
if (bizId.equals(instanceId) || bizId.equals(text(task, "bizId"))) {
|
||||
matched.add(task);
|
||||
}
|
||||
}
|
||||
if (content.size() < TODO_PAGE_SIZE) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
private List<JsonNode> fetchTodoPage(Integer schId, Integer userId, String flowBizType, int page) {
|
||||
Map<String, String> query = new LinkedHashMap<>();
|
||||
query.put("schId", String.valueOf(schId == null ? 0 : schId));
|
||||
query.put("assigneeId", String.valueOf(userId));
|
||||
if (StringUtils.hasText(flowBizType)) {
|
||||
query.put("bizType", flowBizType);
|
||||
}
|
||||
query.put("page", String.valueOf(page));
|
||||
query.put("size", String.valueOf(TODO_PAGE_SIZE));
|
||||
JsonNode data = getFlowcenter("/oapi/flow/tasks/todo", query);
|
||||
if (data != null && data.path("content").isArray()) {
|
||||
List<JsonNode> rows = new ArrayList<>();
|
||||
data.get("content").forEach(rows::add);
|
||||
return rows;
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/** 显式 base-url → ad-hoc TargetSpec(对齐 usermsg 通道形态);否则按名走服务目录/枚举兜底 */
|
||||
private JsonNode getFlowcenter(String path, Map<String, String> query) {
|
||||
String base = properties.getFlowcenterBaseUrl();
|
||||
if (StringUtils.hasText(base)) {
|
||||
return openCall.get(TargetSpec.fromUrl(base.trim()), path, query, JsonNode.class);
|
||||
}
|
||||
return openCall.get(TARGET_FLOWCENTER, path, query, JsonNode.class);
|
||||
}
|
||||
|
||||
private JsonNode postFlowcenter(String path, Map<String, String> query, Object body) {
|
||||
String base = properties.getFlowcenterBaseUrl();
|
||||
if (StringUtils.hasText(base)) {
|
||||
return openCall.post(TargetSpec.fromUrl(base.trim()), path, query, body, JsonNode.class);
|
||||
}
|
||||
return openCall.post(TARGET_FLOWCENTER, path, query, body, JsonNode.class);
|
||||
}
|
||||
|
||||
private static Map<String, Object> fail(Map<String, Object> out, String message) {
|
||||
out.put("ok", false);
|
||||
out.put("message", message);
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String text(JsonNode node, String field) {
|
||||
return node != null && node.has(field) && !node.get(field).isNull() ? node.get(field).asText() : null;
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,9 @@ userhome:
|
||||
# 浏览器可达的 UserMsg WS 端点(实时推送)。空 = 不下发(前端维持手动刷新兜底);
|
||||
# 生产经网关暴露:wss://msg.yunhaixingtong.com/usermsg/ws/msg
|
||||
usermsg-ws-url: ws://localhost:8080/ws/msg
|
||||
# flow-center 服务基址(卡片直批 B3 出站,/oapi/flow/**)。空 = 走服务目录 flowcenter
|
||||
# (PlatformService 枚举兜底);不可达时直批降级提示,不阻塞消息卡片
|
||||
flowcenter-base-url: http://localhost:8085
|
||||
# appKey → 应用显示名(消息行应用列/铃铛标签补全;未命中回退 msgType 文案)
|
||||
app-names:
|
||||
100009: 通知公告
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.yunhai.userhome.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.openapi.core.TargetSpec;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.userhome.config.MsgCenterProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyMap;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 卡片直批代理单测(B3 §2):taskId 匹配三态(0/1/>1)、
|
||||
* <b>红线=身份只从会话参数出站</b>(operatorId/schId/operatorName 来自控制器入参,前端只有
|
||||
* msgId/action/opinion)、业务拒绝原文透出、flow-center 不可达降级。
|
||||
* 契约冻结(源码实测):flow-center 待办消息 bizId=办件 id → 匹配键 instanceId;
|
||||
* bizType 过滤键=消息 jumpApp(流程业务类型),非消息分类 flow.task。
|
||||
*/
|
||||
@DisplayName("MsgApprovalProxyService - 卡片直批")
|
||||
class MsgApprovalProxyServiceTest {
|
||||
|
||||
private static final String FLOWCENTER_BASE = "http://flowcenter-service:10008/flow-center";
|
||||
|
||||
private YschOpenCall openCall;
|
||||
private MsgCenterProxyService msgProxy;
|
||||
private MsgCenterProperties properties;
|
||||
private MsgApprovalProxyService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
openCall = mock(YschOpenCall.class);
|
||||
msgProxy = mock(MsgCenterProxyService.class);
|
||||
properties = new MsgCenterProperties();
|
||||
properties.setFlowcenterBaseUrl(FLOWCENTER_BASE);
|
||||
service = new MsgApprovalProxyService(openCall, msgProxy, properties);
|
||||
}
|
||||
|
||||
private static JsonNode json(String s) throws Exception {
|
||||
return new ObjectMapper().readTree(s);
|
||||
}
|
||||
|
||||
/** flow-center 发的待办消息(notifyTaskCreated 契约):bizId=办件 id、jumpApp=流程业务类型 */
|
||||
private JsonNode flowTaskMessage() throws Exception {
|
||||
return json("{\"msgId\":5,\"msgType\":2,\"bizType\":\"flow.task\","
|
||||
+ "\"bizId\":\"123\",\"jumpApp\":\"notice-approval\",\"isRead\":0}");
|
||||
}
|
||||
|
||||
private void stubTodo(String content) throws Exception {
|
||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/flow/tasks/todo"), anyMap(), eq(JsonNode.class)))
|
||||
.thenReturn(json("{\"total\":1,\"content\":[" + content + "]}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectWithoutOpinion_rejectedBeforeAnyOutbound() {
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "reject", " ");
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("拒绝必须填写意见", out.get("message"));
|
||||
verifyNoInteractions(openCall, msgProxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidAction_rejected() {
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "delegate", null);
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("不支持的操作类型", out.get("message"));
|
||||
verifyNoInteractions(openCall, msgProxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void messageNotFound_stopsWithoutFlowcenterCall() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(null);
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("消息不存在或已失效", out.get("message"), "含他人消息/已删除——统一文案防探测");
|
||||
verifyNoInteractions(openCall);
|
||||
}
|
||||
|
||||
@Test
|
||||
void todoZeroMatch_reportProcessed() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/flow/tasks/todo"), anyMap(), eq(JsonNode.class)))
|
||||
.thenReturn(json("{\"total\":0,\"content\":[]}"));
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("该待办已处理或已失效", out.get("message"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void flowcenterUnreachable_distinguishedFromZeroMatch() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
when(openCall.get(any(TargetSpec.class), eq("/oapi/flow/tasks/todo"), anyMap(), eq(JsonNode.class)))
|
||||
.thenThrow(new RuntimeException("connect timeout"));
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("流程中心暂不可用,请稍后重试", out.get("message"), "不可达 ≠ 已处理,降级提示不误导");
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleMatch_approve_postsSessionIdentity_andMarksRead() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
stubTodo("{\"taskId\":77,\"instanceId\":123,\"bizType\":\"notice-approval\","
|
||||
+ "\"bizId\":\"9001\",\"assigneeId\":201}");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(JsonNode.class)))
|
||||
.thenReturn(null);
|
||||
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", "同意");
|
||||
|
||||
assertTrue((Boolean) out.get("ok"));
|
||||
assertEquals("办理成功", out.get("message"));
|
||||
|
||||
// 红线断言:出站身份 = 会话入参(schId=3 / operatorId=201 / operatorName=张三),前端无身份入参
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> postQuery = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).post(any(TargetSpec.class), eq("/oapi/flow/task/77/approve"),
|
||||
postQuery.capture(), any(), eq(JsonNode.class));
|
||||
assertEquals("3", postQuery.getValue().get("schId"));
|
||||
assertEquals("201", postQuery.getValue().get("operatorId"));
|
||||
assertEquals("张三", postQuery.getValue().get("operatorName"));
|
||||
|
||||
ArgumentCaptor<List<Long>> readIds = ArgumentCaptor.forClass(List.class);
|
||||
verify(msgProxy).read(eq(201), readIds.capture());
|
||||
assertTrue(readIds.getValue().contains(5L), "办理成功后标记已读");
|
||||
}
|
||||
|
||||
@Test
|
||||
void todoFilteredByJumpApp_notMessageBizType() throws Exception {
|
||||
// bizType 过滤键 = 消息 jumpApp(流程业务类型 notice-approval),不是消息分类 flow.task
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
stubTodo("{\"taskId\":77,\"instanceId\":123,\"bizType\":\"notice-approval\",\"bizId\":\"9001\"}");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(JsonNode.class)))
|
||||
.thenReturn(null);
|
||||
|
||||
service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<Map<String, String>> todoQuery = ArgumentCaptor.forClass(Map.class);
|
||||
verify(openCall).get(any(TargetSpec.class), eq("/oapi/flow/tasks/todo"), todoQuery.capture(),
|
||||
eq(JsonNode.class));
|
||||
assertEquals("notice-approval", todoQuery.getValue().get("bizType"), "消息分类 flow.task 与待办 bizType 不同命名空间");
|
||||
assertEquals("201", todoQuery.getValue().get("assigneeId"), "待办按登录人维度拉取");
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiMatch_sameInstanceMultipleNodes_goDetail() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
stubTodo("{\"taskId\":77,\"instanceId\":123,\"bizType\":\"notice-approval\"},"
|
||||
+ "{\"taskId\":78,\"instanceId\":123,\"bizType\":\"notice-approval\"}");
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("同一办件匹配到多个待办节点,请前往详情页办理", out.get("message"));
|
||||
verify(openCall, never()).post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(JsonNode.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void businessReject_passthroughServerMessage() throws Exception {
|
||||
when(msgProxy.findMessage(201, 5L)).thenReturn(flowTaskMessage());
|
||||
stubTodo("{\"taskId\":77,\"instanceId\":123,\"bizType\":\"notice-approval\"}");
|
||||
when(openCall.post(any(TargetSpec.class), anyString(), anyMap(), any(), eq(JsonNode.class)))
|
||||
.thenThrow(new DException("当前用户不是该任务办理人"));
|
||||
|
||||
Map<String, Object> out = service.handle(201, 3, "张三", 5L, "approve", null);
|
||||
|
||||
assertFalse((Boolean) out.get("ok"));
|
||||
assertEquals("当前用户不是该任务办理人", out.get("message"), "业务拒绝原文透出(flow-center 逐字复核)");
|
||||
verify(msgProxy, never()).read(any(), any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user