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 解析成「真实姓名/账号」。
*
*
为什么需要它:{@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修改下备注/名称暂不可用},最后那段就是提交人。
* 这是既有缺陷而不是删除场景引入的:以前标题只勾了编号所以没暴露,D6 让列表勾选
* (含提交人)进标题才第一次现形。
*
*
展示格式跟随前端:{@code realName/account}(如「管理员/admin」)。平台自己在
* {@code FormInfoUtils}/{@code OnlineDevInfoUtils} 等十几处都是这个拼法,审计里换个写法
* 只会让人怀疑是不是另一个人。带上账号是审计的刚需——重名的两个人靠姓名分不开。
*
*
不过滤 f_delete_mark / f_enabled_mark:审计追的是「当时是谁」。人离职了、账号
* 停用了、用户被删了,事件里那个 id 指向的仍然是同一个人,这时候把名字换成「名称暂不可用」
* 恰恰是把最需要追溯的那条记录变成不可追溯。租户边界仍然照约束——那是数据隔离,不是历史。
*
*
与 {@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 fields) {
if (context == null || blank(context.getTenantId()) || fields == null || fields.isEmpty()) {
return;
}
Set 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 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));
}
}
}
/**
* 判据是控件类型而不是字段名:字段名是业务起的({@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 names) {
if (blank(raw)) {
return "";
}
List ids = parseValues(raw);
if (ids.isEmpty()) {
return UNAVAILABLE;
}
List 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 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 ids = new ArrayList<>();
for (Object value : (Collection