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;
|
|
/**
|
* 「操作入口」解析:用户是从哪个菜单点进来的。
|
*
|
* <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;
|
}
|
|
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;
|
}
|
}
|
}
|