[fix] 办理页业务信息走 summaryFields + biz_url 绝对化(BizUrlResolver)
build / build-push (push) Successful in 1m43s
build / build-push (push) Successful in 1m43s
- 详情出参新增 bizUrlAbs:biz_url 裸相对路径按 applyAppCode(by-code)→applyAppKey(by-key) 查应用注册表 pc_url 拼 base#path(口径同 UserHome 消息中心 jumpUrl),查不到为 null - 缓存 5min/未命中 60s,不阻断详情接口;biz_url 原值保留给 bare 态父页面接管
This commit is contained in:
@@ -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<Map<String, Object>> trace;
|
||||
private List<Map<String, Object>> branches;
|
||||
/** 当前用户可否办理(存在本人的待办任务) */
|
||||
private boolean myPendingTask;
|
||||
private List<Long> myPendingTaskIds;
|
||||
/**
|
||||
* 本人待办所在节点的出口列表(多出口办理按钮,[{name,toNodeId,toNodeName}]);
|
||||
* 单出口/无待办为空数组——前端据此决定是否显示去向选择器。
|
||||
*/
|
||||
private List<Map<String, Object>> outlets;
|
||||
/** 可退回目标(主线已办审批节点去重,排除当前节点)[{nodeId,nodeName}] */
|
||||
private List<Map<String, Object>> 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<NodeVo> nodes;
|
||||
private List<EdgeVo> edges;
|
||||
/**
|
||||
* 布局单源(2026-10-03 三视图统一):nodeId → {x,y,w,h}(内容区坐标,min 归零)。
|
||||
* 查看端(前端 SVG / PNG)一律以此为准;layout(设计器画布坐标)已随画布退役删除。
|
||||
*/
|
||||
private Map<String, Map<String, Integer>> 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<Map<String, Object>> trace;
|
||||
private List<Map<String, Object>> branches;
|
||||
/** 当前用户可否办理(存在本人的待办任务) */
|
||||
private boolean myPendingTask;
|
||||
private List<Long> myPendingTaskIds;
|
||||
/**
|
||||
* 本人待办所在节点的出口列表(多出口办理按钮,[{name,toNodeId,toNodeName}]);
|
||||
* 单出口/无待办为空数组——前端据此决定是否显示去向选择器。
|
||||
*/
|
||||
private List<Map<String, Object>> outlets;
|
||||
/** 可退回目标(主线已办审批节点去重,排除当前节点)[{nodeId,nodeName}] */
|
||||
private List<Map<String, Object>> 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<NodeVo> nodes;
|
||||
private List<EdgeVo> edges;
|
||||
/**
|
||||
* 布局单源(2026-10-03 三视图统一):nodeId → {x,y,w,h}(内容区坐标,min 归零)。
|
||||
* 查看端(前端 SVG / PNG)一律以此为准;layout(设计器画布坐标)已随画布退役删除。
|
||||
*/
|
||||
private Map<String, Map<String, Integer>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 绝对化(办件详情/办理页"查看业务详情")。
|
||||
*
|
||||
* <p>bizUrl 自 2026-09-26 起是**裸相对路径**(不含域名/ctx/#,见 UserMsgNotifier 跳转策略):落地基址由
|
||||
* 接收方按"页面归属"解析。消息链路(UserHome 消息中心)已按 jumpApp→application_info.pc_url 拼;
|
||||
* 流程中心自己是另一类接收方——用户在流内直接点"查看业务详情"时没有宿主页面可借基址,必须自己解析,
|
||||
* 否则裸路径会落到 flow-center 自己的 origin 上(实测 /list/detail?serid=78 404)。
|
||||
*
|
||||
* <p>解析口径与消息中心逐字一致({@code MsgCenterProxyService#jumpUrl}):绝对地址直通 → 否则
|
||||
* 按办件快照的 applyAppCode 查应用注册表 pc_url(老数据没有 app_code 时用 applyAppKey 兜底)
|
||||
* → 丢 fragment、去尾斜杠 → {@code base + "#" + path}。查不到 pc_url 返回 null(前端据此不渲染入口),**不猜**。
|
||||
*
|
||||
* <p>缓存: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<String, CacheEntry> 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<String, JsonNode> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FlowModels.InstanceItem> 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());
|
||||
|
||||
Reference in New Issue
Block a user