package jnpf.bizcommon.audit.service.entry; 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.Map; import java.util.concurrent.ConcurrentHashMap; /** * 「操作入口」解析:用户是从哪个菜单点进来的。 * *

为什么不按 modelId 反查:一个表单模型可以挂多个菜单——实测 modelId * {@code 846320770681526725} 对应 4 个菜单(待取样沉降菌/浮游菌/悬浮粒子/表面微生物测试)。 * 反查只能取其中一个,写进审计就是记错事实。所以入口必须由请求携带 * (前端全局拦截器注入 {@code Jnpf-Menu-Id} → 平台 AUDIT-P1 补丁塞进 {@code auditCtx.menuId}), * 本类只负责把 id 翻译成当时的名字。 * *

缓存口径与 {@code AuditFormRegistry} 同族:只缓存确定性结果(空串 = 确定地查不到), * 瞬时故障不缓存(L061 ①)——一次连接抖动若被永久负缓存,该菜单从此到重启前都没有名字。 * *

返回 null 的三种成因由调用方区分: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 cache = new ConcurrentHashMap(); @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; } 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; } } }