刘光辉
2 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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;
        }
    }
}