刘光辉
15 小时以前 34981c30a78e8bbd7791131059a9210f9928b62c
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package jnpf.bizcommon.audit.service.value;
 
import com.alibaba.fastjson.JSON;
import jnpf.bizcommon.audit.entity.model.AuditFieldDiffViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Component;
 
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
 
/**
 * 把 {@code userSelect} 控件存的用户 id 解析成「真实姓名/账号」。
 *
 * <p><b>为什么需要它</b>:{@code userSelect} 的 valueType 是 {@code reference}(见
 * {@link jnpf.bizcommon.audit.service.AuditFormRegistry} 的 valueType 映射),而
 * {@link jnpf.bizcommon.audit.service.AuditFieldDiffViewService} 对没人认领的 reference 一律回落成
 * 「名称暂不可用」。此前只有字典解析器和请验单专用解析器,用户 id 无人认领——2026-08-02
 * 实测删除标题 {@code ys0001/ys0001修改下备注/名称暂不可用},最后那段就是提交人。
 * <b>这是既有缺陷而不是删除场景引入的</b>:以前标题只勾了编号所以没暴露,D6 让列表勾选
 * (含提交人)进标题才第一次现形。
 *
 * <p><b>展示格式跟随前端</b>:{@code realName/account}(如「管理员/admin」)。平台自己在
 * {@code FormInfoUtils}/{@code OnlineDevInfoUtils} 等十几处都是这个拼法,审计里换个写法
 * 只会让人怀疑是不是另一个人。带上账号是审计的刚需——重名的两个人靠姓名分不开。
 *
 * <p><b>不过滤 f_delete_mark / f_enabled_mark</b>:审计追的是「当时是谁」。人离职了、账号
 * 停用了、用户被删了,事件里那个 id 指向的仍然是同一个人,这时候把名字换成「名称暂不可用」
 * 恰恰是把最需要追溯的那条记录变成不可追溯。租户边界仍然照约束——那是数据隔离,不是历史。
 *
 * <p>与 {@link LimsQingyandanFieldValueResolver} 的分工:那个是表专用解析器({@code @Order}
 * 更靠前),{@code lims_qingyandan.qingyan_ren} 由它按自己的格式解析完,本解析器看到
 * display 已经有值就不再插手({@code displaysResolved}),两者不会打架。
 */
@Slf4j
@Component
@Order(JnpfUserFieldValueResolver.ORDER)
public class JnpfUserFieldValueResolver implements AuditFieldValueResolver {
 
    /** 通用兜底解析器,排在表专用(100)与字典(200)之后。 */
    public static final int ORDER = 300;
 
    private static final String UNAVAILABLE = "名称暂不可用";
 
    /** 认领的控件类型(小写比对)。只认人员选择器,部门/角色/岗位各有各的名称表,不在本类范围。 */
    private static final String USER_COMPONENT = "userselect";
 
    private final UserLookup lookup;
 
    @Autowired
    public JnpfUserFieldValueResolver(JdbcTemplate jdbcTemplate) {
        this(new JdbcUserLookup(jdbcTemplate));
    }
 
    JnpfUserFieldValueResolver(UserLookup lookup) {
        this.lookup = lookup;
    }
 
    @Override
    public boolean supports(String targetTable) {
        return true;   // userSelect 是平台控件,哪张业务表都可能有
    }
 
    @Override
    public void resolve(AuditFieldValueContext context, List<AuditFieldDiffViewVO> fields) {
        if (context == null || blank(context.getTenantId()) || fields == null || fields.isEmpty()) {
            return;
        }
        Set<String> userIds = new LinkedHashSet<>();
        for (AuditFieldDiffViewVO field : fields) {
            if (!isUserField(field) || displaysResolved(field)) {
                continue;
            }
            userIds.addAll(parseValues(field.getOldData()));
            userIds.addAll(parseValues(field.getNewData()));
        }
        if (userIds.isEmpty()) {
            return;
        }
 
        // 整批一次 IN 查询(SPI 约定:层 0 跑在 MQ 消费线程上,逐 id 查会拖垮消费速率)
        Map<String, String> names = lookup.names(context.getTenantId().trim(), userIds);
        for (AuditFieldDiffViewVO field : fields) {
            if (!isUserField(field)) {
                continue;
            }
            if (field.getOldDisplay() == null) {
                field.setOldDisplay(display(field.getOldData(), names));
            }
            if (field.getNewDisplay() == null) {
                field.setNewDisplay(display(field.getNewData(), names));
            }
        }
    }
 
    /**
     * 判据是<b>控件类型</b>而不是字段名:字段名是业务起的({@code tijiao_ren}/{@code fuhe_ren}/
     * {@code qingyan_ren}…),穷举不完;{@code jnpfKey} 才是「这一列存的是用户 id」的权威声明。
     */
    private static boolean isUserField(AuditFieldDiffViewVO field) {
        return field != null && field.getComponentType() != null
                && USER_COMPONENT.equals(field.getComponentType().trim().toLowerCase(Locale.ROOT));
    }
 
    /** 两侧要么本来就空、要么已被前序解析器填过,就不必再查库。 */
    private static boolean displaysResolved(AuditFieldDiffViewVO field) {
        return (blank(field.getOldData()) || field.getOldDisplay() != null)
                && (blank(field.getNewData()) || field.getNewDisplay() != null);
    }
 
    private static String display(String raw, Map<String, String> names) {
        if (blank(raw)) {
            return "";
        }
        List<String> ids = parseValues(raw);
        if (ids.isEmpty()) {
            return UNAVAILABLE;
        }
        List<String> result = new ArrayList<>(ids.size());
        for (String id : ids) {
            String name = names.get(id);
            result.add(blank(name) ? UNAVAILABLE : name);
        }
        return String.join("、", result);
    }
 
    /** 多选(multiple=true)的 userSelect 存的是 JSON 数组,单选存裸 id。 */
    @SuppressWarnings("unchecked")
    private static List<String> parseValues(String raw) {
        if (blank(raw)) {
            return Collections.emptyList();
        }
        String trimmed = raw.trim();
        if (!trimmed.startsWith("[")) {
            return Collections.singletonList(trimmed);
        }
        try {
            Object parsed = JSON.parse(trimmed);
            if (!(parsed instanceof Collection)) {
                return Collections.emptyList();
            }
            List<String> ids = new ArrayList<>();
            for (Object value : (Collection<Object>) parsed) {
                if (value != null && !String.valueOf(value).trim().isEmpty()) {
                    ids.add(String.valueOf(value).trim());
                }
            }
            return ids;
        } catch (Throwable ignored) {
            return Collections.emptyList();
        }
    }
 
    /**
     * 前端展示格式即 {@code realName/account}(如「管理员/admin」);缺一边时退化成还剩的
     * 那一边,不留孤零零的斜杠。
     *
     * <p>放在外层类而不是 {@link JdbcUserLookup} 里,是为了让探针够得着——格式本身就是
     * 本次修复的验收标准之一(「删除标题里提交人显示管理员/admin」),埋在 JDBC 实现里
     * 就只能靠端到端才验得出来。
     */
    static String userDisplay(String realName, String account) {
        if (blank(realName)) {
            return blank(account) ? null : account.trim();
        }
        return blank(account) ? realName.trim() : realName.trim() + "/" + account.trim();
    }
 
    private static boolean blank(String value) {
        return value == null || value.trim().isEmpty();
    }
 
    interface UserLookup {
        Map<String, String> names(String tenantId, Set<String> ids);
    }
 
    /** 表名和列名固定;id 与租户全部参数化。 */
    private static final class JdbcUserLookup implements UserLookup {
        private final NamedParameterJdbcTemplate jdbc;
 
        private JdbcUserLookup(JdbcTemplate jdbcTemplate) {
            this.jdbc = new NamedParameterJdbcTemplate(jdbcTemplate);
        }
 
        @Override
        public Map<String, String> names(String tenantId, Set<String> ids) {
            if (ids.isEmpty()) {
                return Collections.emptyMap();
            }
            try {
                MapSqlParameterSource params = new MapSqlParameterSource()
                        .addValue("ids", ids)
                        .addValue("tenantId", tenantId);
                List<Map<String, Object>> rows = jdbc.queryForList(
                        "SELECT f_id, f_real_name, f_account FROM base_user "
                                + "WHERE f_id IN (:ids) "
                                + "AND COALESCE(NULLIF(f_tenant_id, ''), '0') = :tenantId",
                        params);
                Map<String, String> result = new LinkedHashMap<>();
                for (Map<String, Object> row : rows) {
                    String id = text(row.get("f_id"));
                    String display = userDisplay(text(row.get("f_real_name")),
                            text(row.get("f_account")));
                    if (id != null && display != null) {
                        result.put(id, display);
                    }
                }
                return result;
            } catch (RuntimeException ex) {
                log.warn("[AUDIT-QUERY] 用户名称批量解析失败 count={} err={}",
                        ids.size(), ex.getClass().getSimpleName());
                return Collections.emptyMap();
            }
        }
 
        private static String text(Object value) {
            if (value == null) {
                return null;
            }
            String text = String.valueOf(value).trim();
            return text.isEmpty() ? null : text;
        }
    }
}