package jnpf.bizcommon.audit.service.entry;
|
|
import jnpf.util.JsonUtil;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.stereotype.Component;
|
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.ConcurrentHashMap;
|
|
/**
|
* 「操作入口」解析:用户是从哪个菜单点进来的。
|
*
|
* <p><b>为什么不按 modelId 反查</b>:一个表单模型可以挂多个菜单——实测 modelId
|
* {@code 846320770681526725} 对应 4 个菜单(待取样沉降菌/浮游菌/悬浮粒子/表面微生物测试)。
|
* 反查只能取其中一个,写进审计就是<b>记错事实</b>。所以入口必须由请求携带
|
* (前端全局拦截器注入 {@code Jnpf-Menu-Id} → 平台 AUDIT-P1 补丁塞进 {@code auditCtx.menuId}),
|
* 本类只负责把 id 翻译成当时的名字。
|
*
|
* <p>缓存口径与 {@code AuditFormRegistry} 同族:只缓存确定性结果(空串 = 确定地查不到),
|
* 瞬时故障不缓存(L061 ①)——一次连接抖动若被永久负缓存,该菜单从此到重启前都没有名字。
|
*
|
* <p><b>返回 null 的三种成因由调用方区分</b>:menuId 为空(请求没带)、确定地查不到、瞬时故障。
|
* 本类只保证「查不到就返回 null,绝不编一个名字」;「没带」与「带了但查不到」写成两种
|
* {@code entrySource} 留痕,是编排侧(Task 6)的事——共用一个表示就会遮住一处漏记。
|
*/
|
@Slf4j
|
@Component
|
public class AuditEntryResolver {
|
|
/** 入口类型:菜单。预留 FORM / FLOW 等,用于解释 entry_id 指向哪张表。 */
|
public static final String TYPE_MENU = "MENU";
|
|
private final Map<String, CacheValue> cache = new ConcurrentHashMap<String, CacheValue>();
|
|
@Autowired
|
private JdbcTemplate jdbcTemplate;
|
|
@Value("${audit.layer0.metadata-cache-ttl-ms:300000}")
|
private long ttlMs = 300_000L;
|
|
/** 菜单名;menuId 为空、查不到、或瞬时故障时返回 null(调用方据此写 entrySource)。 */
|
public String resolveMenuName(String menuId) {
|
if (menuId == null || menuId.trim().isEmpty()) {
|
return null;
|
}
|
String key = menuId.trim();
|
CacheValue cached = cache.get(key);
|
if (cached != null && ttlMs > 0L
|
&& System.currentTimeMillis() - cached.createdAt < ttlMs) {
|
return cached.value.isEmpty() ? null : cached.value;
|
}
|
String name;
|
try {
|
// 排除已删菜单:菜单被删后 f_id 仍是可追溯的事实,但它的名字不该再被当成"当时的入口名"
|
// 写进新记录。f_delete_mark 在 PG 里是 integer,未删除时为 NULL 或 0。
|
name = jdbcTemplate.queryForObject(
|
"SELECT f_full_name FROM base_module WHERE f_id = ? "
|
+ "AND (f_delete_mark IS NULL OR f_delete_mark <> 1) LIMIT 1",
|
String.class, key);
|
} catch (org.springframework.dao.EmptyResultDataAccessException e) {
|
// 确定性:这个 menuId 没有菜单行,再查一万次也一样
|
put(key, "");
|
return null;
|
} catch (Throwable t) {
|
log.warn("[AUDIT-L0] 反查菜单名失败(瞬时,不缓存)menuId={} err={}", key, t.toString());
|
return null;
|
}
|
String normalized = name == null ? "" : name.trim();
|
put(key, normalized);
|
return normalized.isEmpty() ? null : normalized;
|
}
|
|
/** Resolve only the closest marked ancestor of the actual entry menu. */
|
public String resolveParentName(String menuId, String requestedParentId) {
|
if (menuId == null || requestedParentId == null
|
|| menuId.trim().isEmpty() || requestedParentId.trim().isEmpty()) {
|
return null;
|
}
|
try {
|
List<Map<String, Object>> rows = closestMarkedParent(menuId.trim());
|
if (rows.isEmpty() || !requestedParentId.trim().equals(String.valueOf(rows.get(0).get("f_id")))) {
|
return null;
|
}
|
Object name = rows.get(0).get("f_full_name");
|
return name == null ? null : String.valueOf(name);
|
} catch (RuntimeException ex) {
|
log.warn("[AUDIT] Parent module lookup failed menuId={} err={}", menuId,
|
ex.getClass().getSimpleName());
|
return null;
|
}
|
}
|
|
/** A workflow task identifies a template, while the current route may be the generic inbox. */
|
public String resolveFlowParentName(String taskId, String requestedMenuId, String sourceModelId) {
|
if (taskId == null || taskId.trim().isEmpty()) {
|
return null;
|
}
|
try {
|
List<Map<String, Object>> tasks = jdbcTemplate.queryForList(
|
"SELECT f_template_id, f_flow_id FROM workflow_task WHERE f_id = ?", taskId.trim());
|
if (tasks.isEmpty() || tasks.get(0).get("f_template_id") == null) {
|
return null;
|
}
|
String templateId = String.valueOf(tasks.get(0).get("f_template_id"));
|
if (sourceModelId != null && !sourceModelId.trim().isEmpty()) {
|
Object flowId = tasks.get(0).get("f_flow_id");
|
if (flowId == null || jdbcTemplate.queryForList(
|
"SELECT f_id FROM workflow_node WHERE f_flow_id = ? AND f_form_id = ? LIMIT 1",
|
String.valueOf(flowId), sourceModelId.trim()).isEmpty()) {
|
return null;
|
}
|
}
|
List<Map<String, Object>> menus = jdbcTemplate.queryForList(
|
"SELECT f_id, f_property_json FROM base_module WHERE f_type = 9"
|
+ " AND f_enabled_mark = 1 AND (f_delete_mark IS NULL OR f_delete_mark <> 1)");
|
List<String> matches = new ArrayList<>();
|
for (Map<String, Object> menu : menus) {
|
Object property = menu.get("f_property_json");
|
if (property == null) {
|
continue;
|
}
|
try {
|
Map<String, Object> values = JsonUtil.stringToMap(String.valueOf(property));
|
if (values != null && templateId.equals(String.valueOf(values.get("moduleId")))) {
|
matches.add(String.valueOf(menu.get("f_id")));
|
}
|
} catch (RuntimeException ignored) {
|
// A malformed menu configuration cannot establish workflow ownership.
|
}
|
}
|
String menuId = requestedMenuId != null && matches.contains(requestedMenuId.trim())
|
? requestedMenuId.trim() : matches.size() == 1 ? matches.get(0) : null;
|
if (menuId == null) {
|
return null;
|
}
|
List<Map<String, Object>> parents = closestMarkedParent(menuId);
|
Object name = parents.isEmpty() ? null : parents.get(0).get("f_full_name");
|
return name == null ? null : String.valueOf(name);
|
} catch (RuntimeException ex) {
|
log.warn("[AUDIT] Workflow parent module lookup failed taskId={} err={}", taskId,
|
ex.getClass().getSimpleName());
|
return null;
|
}
|
}
|
|
private List<Map<String, Object>> closestMarkedParent(String menuId) {
|
String sql = "WITH RECURSIVE ancestors AS ("
|
+ " SELECT f_id, f_parent_id, f_system_id, f_category, f_type,"
|
+ " f_is_parent_biz_module, f_full_name, 0 AS depth FROM base_module"
|
+ " WHERE f_id = ? AND f_enabled_mark = 1"
|
+ " AND (f_delete_mark IS NULL OR f_delete_mark <> 1)"
|
+ " UNION ALL"
|
+ " SELECT parent.f_id, parent.f_parent_id, parent.f_system_id, parent.f_category,"
|
+ " parent.f_type, parent.f_is_parent_biz_module, parent.f_full_name, child.depth + 1"
|
+ " FROM ancestors child JOIN base_module parent ON parent.f_id = child.f_parent_id"
|
+ " AND parent.f_system_id = child.f_system_id"
|
+ " AND parent.f_category = child.f_category"
|
+ " WHERE child.depth < 32 AND parent.f_enabled_mark = 1"
|
+ " AND (parent.f_delete_mark IS NULL OR parent.f_delete_mark <> 1)"
|
+ ") SELECT f_id, f_full_name FROM ancestors"
|
+ " WHERE depth > 0 AND f_type = 1 AND f_is_parent_biz_module = 1"
|
+ " ORDER BY depth LIMIT 1";
|
return jdbcTemplate.queryForList(sql, menuId);
|
}
|
|
private void put(String key, String value) {
|
if (ttlMs > 0L) {
|
cache.put(key, new CacheValue(value));
|
}
|
}
|
|
private static final class CacheValue {
|
private final String value;
|
private final long createdAt = System.currentTimeMillis();
|
|
private CacheValue(String value) {
|
this.value = value;
|
}
|
}
|
}
|