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