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;
}
}
}