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 待办匹配 → 办理 → 标已读 全链路。 * */ @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 handle(Integer userId, Integer schId, String operatorName, Long msgId, String action, String opinion) { Map 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 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 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 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 matchTodoTasks(Integer schId, Integer userId, String flowBizType, String bizId) { List matched = new ArrayList<>(); for (int page = 1; page <= TODO_MAX_PAGES; page++) { List 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 fetchTodoPage(Integer schId, Integer userId, String flowBizType, int page) { Map 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 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 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 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 fail(Map 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; } }