From 52c27d88bbd2943b747fa52025ec08e69c27b8db Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 19 Oct 2026 20:09:29 +0800 Subject: [PATCH] =?UTF-8?q?[fix]=20=E5=8A=9E=E7=90=86=E9=A1=B5=E4=B8=9A?= =?UTF-8?q?=E5=8A=A1=E4=BF=A1=E6=81=AF=E8=B5=B0=20summaryFields=20+=20biz?= =?UTF-8?q?=5Furl=20=E7=BB=9D=E5=AF=B9=E5=8C=96=EF=BC=88BizUrlResolver?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 详情出参新增 bizUrlAbs:biz_url 裸相对路径按 applyAppCode(by-code)→applyAppKey(by-key) 查应用注册表 pc_url 拼 base#path(口径同 UserHome 消息中心 jumpUrl),查不到为 null - 缓存 5min/未命中 60s,不阻断详情接口;biz_url 原值保留给 bare 态父页面接管 --- .../yunhai/flowcenter/model/FlowModels.java | 358 +++++++++--------- .../flowcenter/service/BizUrlResolver.java | 120 ++++++ .../flowcenter/service/FlowQueryService.java | 3 + 3 files changed, 305 insertions(+), 176 deletions(-) create mode 100644 src/main/java/com/yunhai/flowcenter/service/BizUrlResolver.java diff --git a/src/main/java/com/yunhai/flowcenter/model/FlowModels.java b/src/main/java/com/yunhai/flowcenter/model/FlowModels.java index ce0ac64..21803b9 100644 --- a/src/main/java/com/yunhai/flowcenter/model/FlowModels.java +++ b/src/main/java/com/yunhai/flowcenter/model/FlowModels.java @@ -1,176 +1,182 @@ -package com.yunhai.flowcenter.model; - -import lombok.Data; - -import java.util.List; -import java.util.Map; - -/** - * 用户面/管理面 VO(/api/flow/**)。 - */ -public final class FlowModels { - - private FlowModels() { - } - - /** 我的办件列表行 */ - @Data - public static class InstanceItem { - private Long instanceId; - private String bizType; - private String bizId; - private String bizTitle; - private Integer status; - private String currentNodes; - private String submitterName; - private String submitTime; - private String finishTime; - private String defName; - private Integer defVersion; - } - - /** 待办/已办列表行 */ - @Data - public static class TaskItem { - private Long taskId; - private Long instanceId; - private String bizTitle; - private String bizType; - private String bizId; - private Integer instanceStatus; - private String nodeId; - private String nodeName; - private String assigneeName; - /** - * 提交人姓名(办件发起人快照)。列表列"提交人"以此为准,勿与 assigneeName(当前办理人)混用。 - */ - private String submitterName; - private Integer status; - private String opinion; - private String arriveTime; - private String handleTime; - private String deadline; - private Integer urgeCount; - /** - * 已办专用:本人对该办件的累计办理次数(同一办件多节点办理/退回重办只出一行, - * 次数用这个字段兜住,前端提示"共办理 N 次");待办口径恒 null。 - */ - private Integer doneCount; - } - - /** 抄送我的列表行(NOTIFY 落库行,flw_cc) */ - @Data - public static class CcItem { - private Long ccId; - private Long instanceId; - private String bizType; - private String bizId; - private String bizTitle; - private Integer instanceStatus; - private String nodeId; - private String nodeName; - private Integer userId; - private String userName; - private String sentTime; - } - - /** 办件详情(办理页/详情页数据源) */ - @Data - public static class InstanceDetail { - private Long instanceId; - private Long schId; - private String defCode; - private Integer defVersion; - private String bizType; - private String bizId; - private String bizTitle; - private String bizUrl; - /** 业务摘要 JSON(办理页区块渲染;无富文本正文——概览 §十边界) */ - private String bizSnapshot; - private String formDataJson; - private Integer status; - private String currentNodes; - private Integer submitterId; - private String submitterName; - private String submitTime; - private String finishTime; - private Integer version; - private String nodeSummary; - private List> trace; - private List> branches; - /** 当前用户可否办理(存在本人的待办任务) */ - private boolean myPendingTask; - private List myPendingTaskIds; - /** - * 本人待办所在节点的出口列表(多出口办理按钮,[{name,toNodeId,toNodeName}]); - * 单出口/无待办为空数组——前端据此决定是否显示去向选择器。 - */ - private List> outlets; - /** 可退回目标(主线已办审批节点去重,排除当前节点)[{nodeId,nodeName}] */ - private List> returnTargets; - /** 本人待办可否移交(恒=有本人待办,展示用) */ - private boolean canTransfer; - } - - /** 流程示意图出参(用户面 /diagram 与管理面 /admin/def/diagram 共用,方案 §3.1) */ - @Data - public static class DiagramVo { - private String flowGuid; - private Long defId; - private Long instanceId; - private String name; - private Integer version; - private Integer status; - /** 实际出图口径:dept|user|auto(后端归一化后回显) */ - private String mode; - private List nodes; - private List edges; - /** - * 布局单源(2026-10-03 三视图统一):nodeId → {x,y,w,h}(内容区坐标,min 归零)。 - * 查看端(前端 SVG / PNG)一律以此为准;layout(设计器画布坐标)已随画布退役删除。 - */ - private Map> positions; - } - - /** 示意图节点(label/subLabel 已按视图解析好,前端只画不解析;state 仅 instance 口径有值) */ - @Data - public static class NodeVo { - private String id; - private String type; - private String label; - private String subLabel; - private String deptKey; - private String assigneeType; - private String countersign; - private Integer deadlineHours; - /** - * 运行时状态(仅 instanceId 口径):done 已办过 / active 当前在办 / pending 未到 / - * rejected 被拒绝 / 其他为 null。flowGuid 口径恒 null。 - */ - private String state; - } - - /** 示意图连边(name=出口文案,多出口节点显示为连线标签) */ - @Data - public static class EdgeVo { - private String from; - private String to; - private String name; - } - - /** - * 流程定义源码(2026-09-18,「图示页-源码」抽屉数据源):只读展示/复制 contentJson, - * 可见性口径与 {@link DiagramVo} 完全一致(instanceId 走办件可见性守卫,flowGuid 只给最高已发布版)。 - */ - @Data - public static class DefSourceVo { - private Long defId; - private String flowGuid; - private String code; - private String name; - private Integer version; - private Integer status; - /** 流程定义 JSON(nodes/edges/settings/layout,运行时格式) */ - private String contentJson; - } -} +package com.yunhai.flowcenter.model; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +/** + * 用户面/管理面 VO(/api/flow/**)。 + */ +public final class FlowModels { + + private FlowModels() { + } + + /** 我的办件列表行 */ + @Data + public static class InstanceItem { + private Long instanceId; + private String bizType; + private String bizId; + private String bizTitle; + private Integer status; + private String currentNodes; + private String submitterName; + private String submitTime; + private String finishTime; + private String defName; + private Integer defVersion; + } + + /** 待办/已办列表行 */ + @Data + public static class TaskItem { + private Long taskId; + private Long instanceId; + private String bizTitle; + private String bizType; + private String bizId; + private Integer instanceStatus; + private String nodeId; + private String nodeName; + private String assigneeName; + /** + * 提交人姓名(办件发起人快照)。列表列"提交人"以此为准,勿与 assigneeName(当前办理人)混用。 + */ + private String submitterName; + private Integer status; + private String opinion; + private String arriveTime; + private String handleTime; + private String deadline; + private Integer urgeCount; + /** + * 已办专用:本人对该办件的累计办理次数(同一办件多节点办理/退回重办只出一行, + * 次数用这个字段兜住,前端提示"共办理 N 次");待办口径恒 null。 + */ + private Integer doneCount; + } + + /** 抄送我的列表行(NOTIFY 落库行,flw_cc) */ + @Data + public static class CcItem { + private Long ccId; + private Long instanceId; + private String bizType; + private String bizId; + private String bizTitle; + private Integer instanceStatus; + private String nodeId; + private String nodeName; + private Integer userId; + private String userName; + private String sentTime; + } + + /** 办件详情(办理页/详情页数据源) */ + @Data + public static class InstanceDetail { + private Long instanceId; + private Long schId; + private String defCode; + private Integer defVersion; + private String bizType; + private String bizId; + private String bizTitle; + private String bizUrl; + /** + * 业务详情的**绝对地址**(bizUrl 为裸相对路径,由 applyAppCode 查应用注册表 pc_url 拼出, + * 口径同消息中心跳转);无法解析为 null ⇒ 前端不渲染"查看业务详情"入口。 + * 保留 bizUrl 原值不动:嵌入/去壳态由父页面按自己路由接管,绝对地址只服务流内直开。 + */ + private String bizUrlAbs; + /** 业务摘要 JSON(办理页区块渲染;无富文本正文——概览 §十边界) */ + private String bizSnapshot; + private String formDataJson; + private Integer status; + private String currentNodes; + private Integer submitterId; + private String submitterName; + private String submitTime; + private String finishTime; + private Integer version; + private String nodeSummary; + private List> trace; + private List> branches; + /** 当前用户可否办理(存在本人的待办任务) */ + private boolean myPendingTask; + private List myPendingTaskIds; + /** + * 本人待办所在节点的出口列表(多出口办理按钮,[{name,toNodeId,toNodeName}]); + * 单出口/无待办为空数组——前端据此决定是否显示去向选择器。 + */ + private List> outlets; + /** 可退回目标(主线已办审批节点去重,排除当前节点)[{nodeId,nodeName}] */ + private List> returnTargets; + /** 本人待办可否移交(恒=有本人待办,展示用) */ + private boolean canTransfer; + } + + /** 流程示意图出参(用户面 /diagram 与管理面 /admin/def/diagram 共用,方案 §3.1) */ + @Data + public static class DiagramVo { + private String flowGuid; + private Long defId; + private Long instanceId; + private String name; + private Integer version; + private Integer status; + /** 实际出图口径:dept|user|auto(后端归一化后回显) */ + private String mode; + private List nodes; + private List edges; + /** + * 布局单源(2026-10-03 三视图统一):nodeId → {x,y,w,h}(内容区坐标,min 归零)。 + * 查看端(前端 SVG / PNG)一律以此为准;layout(设计器画布坐标)已随画布退役删除。 + */ + private Map> positions; + } + + /** 示意图节点(label/subLabel 已按视图解析好,前端只画不解析;state 仅 instance 口径有值) */ + @Data + public static class NodeVo { + private String id; + private String type; + private String label; + private String subLabel; + private String deptKey; + private String assigneeType; + private String countersign; + private Integer deadlineHours; + /** + * 运行时状态(仅 instanceId 口径):done 已办过 / active 当前在办 / pending 未到 / + * rejected 被拒绝 / 其他为 null。flowGuid 口径恒 null。 + */ + private String state; + } + + /** 示意图连边(name=出口文案,多出口节点显示为连线标签) */ + @Data + public static class EdgeVo { + private String from; + private String to; + private String name; + } + + /** + * 流程定义源码(2026-09-18,「图示页-源码」抽屉数据源):只读展示/复制 contentJson, + * 可见性口径与 {@link DiagramVo} 完全一致(instanceId 走办件可见性守卫,flowGuid 只给最高已发布版)。 + */ + @Data + public static class DefSourceVo { + private Long defId; + private String flowGuid; + private String code; + private String name; + private Integer version; + private Integer status; + /** 流程定义 JSON(nodes/edges/settings/layout,运行时格式) */ + private String contentJson; + } +} diff --git a/src/main/java/com/yunhai/flowcenter/service/BizUrlResolver.java b/src/main/java/com/yunhai/flowcenter/service/BizUrlResolver.java new file mode 100644 index 0000000..3316323 --- /dev/null +++ b/src/main/java/com/yunhai/flowcenter/service/BizUrlResolver.java @@ -0,0 +1,120 @@ +package com.yunhai.flowcenter.service; + +import com.fasterxml.jackson.databind.JsonNode; +import com.yunhai.platform.openapi.base.OpenApiAppClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * 办件 biz_url 绝对化(办件详情/办理页"查看业务详情")。 + * + *

bizUrl 自 2026-09-26 起是**裸相对路径**(不含域名/ctx/#,见 UserMsgNotifier 跳转策略):落地基址由 + * 接收方按"页面归属"解析。消息链路(UserHome 消息中心)已按 jumpApp→application_info.pc_url 拼; + * 流程中心自己是另一类接收方——用户在流内直接点"查看业务详情"时没有宿主页面可借基址,必须自己解析, + * 否则裸路径会落到 flow-center 自己的 origin 上(实测 /list/detail?serid=78 404)。 + * + *

解析口径与消息中心逐字一致({@code MsgCenterProxyService#jumpUrl}):绝对地址直通 → 否则 + * 按办件快照的 applyAppCode 查应用注册表 pc_url(老数据没有 app_code 时用 applyAppKey 兜底) + * → 丢 fragment、去尾斜杠 → {@code base + "#" + path}。查不到 pc_url 返回 null(前端据此不渲染入口),**不猜**。 + * + *

缓存:pc_url 属低频变更的注册表数据,按应用标识缓存 5 分钟;查无缓存 60 秒(防脏值刷重复出站)。 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class BizUrlResolver { + + private static final long HIT_TTL_MS = 5 * 60_000L; + private static final long MISS_TTL_MS = 60_000L; + + private final OpenApiAppClient appClient; + + private final Map cache = new ConcurrentHashMap<>(); + + /** + * @param appCode 页面归属应用(办件 apply_app_code,如 notice) + * @param appKey 验签身份 app_key(老数据 app_code 为空时的兜底) + * @param bizUrl 办件快照里的 biz_url(裸相对路径,或历史存量绝对地址) + * @return 绝对地址;绝对地址原样返回,无法解析返回 null + */ + public String resolve(String appCode, String appKey, String bizUrl) { + if (bizUrl == null || bizUrl.trim().isEmpty()) { + return null; + } + String url = bizUrl.trim(); + if (isAbsolute(url)) { + return url; + } + String pcUrl = null; + if (hasText(appCode)) { + pcUrl = pcUrl("code:" + appCode.trim(), appClient::getAppByCode); + } + if (pcUrl == null && hasText(appKey)) { + pcUrl = pcUrl("key:" + appKey.trim(), appClient::getAppByKey); + } + if (pcUrl == null) { + return null; + } + StringBuilder base = new StringBuilder(pcUrl); + int hash = base.indexOf("#"); + if (hash >= 0) { + base.setLength(hash); + } + while (base.length() > 0 && base.charAt(base.length() - 1) == '/') { + base.setLength(base.length() - 1); + } + if (base.length() == 0) { + return null; + } + return base + "#" + (url.startsWith("/") ? url : "/" + url); + } + + private static boolean isAbsolute(String url) { + return url.startsWith("http://") || url.startsWith("https://"); + } + + private static boolean hasText(String v) { + return v != null && !v.trim().isEmpty(); + } + + private String pcUrl(String cacheKey, Function fetcher) { + long now = System.currentTimeMillis(); + CacheEntry cached = cache.get(cacheKey); + if (cached != null && cached.expireAt > now) { + return cached.pcUrl; + } + String pcUrl = null; + try { + JsonNode app = fetcher.apply(cacheKey.substring(cacheKey.indexOf(':') + 1)); + if (app != null && app.hasNonNull("pcUrl")) { + String v = app.get("pcUrl").asText(null); + if (hasText(v)) { + pcUrl = v.trim(); + } + } + if (pcUrl == null) { + log.warn("[flow-center] biz_url 归属应用无 pc_url,跳转入口隐藏:{}", cacheKey); + } + } catch (Exception e) { + // 不可达按 miss 缓存,避免办件详情页每请求打一次 UserBase + log.warn("[flow-center] biz_url 归属应用查址失败(UserBase 不可达?){}:{}", cacheKey, e.toString()); + } + cache.put(cacheKey, new CacheEntry(pcUrl, now + (pcUrl == null ? MISS_TTL_MS : HIT_TTL_MS))); + return pcUrl; + } + + private static final class CacheEntry { + private final String pcUrl; + private final long expireAt; + + private CacheEntry(String pcUrl, long expireAt) { + this.pcUrl = pcUrl; + this.expireAt = expireAt; + } + } +} diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java b/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java index 2e4051e..dafd001 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowQueryService.java @@ -48,6 +48,7 @@ public class FlowQueryService { private final FlwDefRepository defRepository; private final FlowDeptAdminService deptAdminService; private final FlowEngine flowEngine; + private final BizUrlResolver bizUrlResolver; public BasePageResult pageMy(Long schId, Integer submitterId, Integer status, String appKey, Integer page, Integer size) { @@ -152,6 +153,8 @@ public class FlowQueryService { detail.setBizId(instance.getBizId()); detail.setBizTitle(instance.getBizTitle()); detail.setBizUrl(instance.getBizUrl()); + detail.setBizUrlAbs(bizUrlResolver.resolve( + instance.getApplyAppCode(), instance.getApplyAppKey(), instance.getBizUrl())); detail.setBizSnapshot(instance.getBizSnapshot()); detail.setFormDataJson(instance.getFormDataJson()); detail.setStatus(instance.getStatus());