Files
UserhomeService/src/main/java/com/yunhai/userhome/service/MsgApprovalProxyService.java
T

195 lines
9.0 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 条=已处理或已失效;&gt;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;
}
}