package jnpf.bizcommon.audit.service.sign;
import jnpf.audit.diff.AuditFieldDiff;
import jnpf.bizcommon.audit.service.listener.AuditVisualLogListener;
import jnpf.bizcommon.audit.service.title.AuditTitleSpec;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
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 lims_sign} 而不做成配置:签名将整体挪进通用服务
* (规划中的 {@code jnpf-biz-common} 之类),配置化是为一个已计划废弃的中间态买单。
* 所以把耦合收在本类一处:对外只有 {@link #read},届时换本类实现即可,调用方零改动。
*
*
批量读,不逐个查:与 {@code AuditFieldValueResolver} 的 SPI 约定同口径——
* 层 0 跑在 MQ 消费线程上,逐 ID 查库会把消费速率拖垮。
*/
@Slf4j
@Component
public class AuditSignEvidenceReader {
/**
* 签名凭证表。挪到通用服务时改这里(连同下面的列名)。
*
*
public 是刻意的:签名事件的 {@code target_table} 写的是它,而 A1-b 的历史反查
* ({@code selectRecordedSignIds})要按同一个值去查——两处分别写一份字面量,改表名时
* 漏掉一处就会让反查恒查不到、历史签名重新被当成本次签的,而且不报任何错。
*/
public static final String SIGN_TABLE = "lims_sign";
/** 单次 IN 的上限。一份表单上的签名字段个位数,超了说明判定跑偏,宁可少查也不打爆库。 */
private static final int MAX_IDS = 20;
@Autowired
private JdbcTemplate jdbcTemplate;
/**
* 本次事件里「可能是签名字段」的候选(小写、去重、稳定顺序)。
*
*
三条来源,按优先级排列——{@code ordered} 是 {@link LinkedHashSet}(保序),
* {@link #MAX_IDS} 顶格截断时先保住排在前面的:
*
* - {@code configuredFields}(D4 权威来源,见
* {@link jnpf.bizcommon.audit.service.AuditFormRegistry#resolveSignFields}):表单配置
* {@code confirmBtnConfig}/{@code reviewBtnConfig} 里显式指定的 {@code biz_sign_field}。
* 多人签名场景下字段名可以是任意的,必须最先放进候选集,否则会被后两条来源
* 挤到 20 上限之外;
* - 命名规则命中 {@code isCredentialReference} 的字段——恒为候选,不需要勾选,
* 这批字段本就已被强制脱敏(命名规则+撞库兜底,覆盖配置没提到的字段);
* - 表单上勾了 {@code biz_log_enabled} 的主表字段——是不是签名由 {@link #read}
* 查表定,本方法只负责挑出"值得去查一下"的那些。
*
*
* 值为空的一律不进候选:空值查签名表纯属浪费,还会把空串塞进 IN 参数。
*/
public static List candidateFields(Map row, AuditTitleSpec spec,
List configuredFields) {
if (row == null || row.isEmpty()) {
return Collections.emptyList();
}
Set ordered = new LinkedHashSet();
if (configuredFields != null) {
for (String field : configuredFields) {
if (field == null) {
continue;
}
String lowerField = field.toLowerCase(Locale.ROOT);
if (hasValue(valueOf(row, lowerField))) {
ordered.add(lowerField);
}
}
}
for (Map.Entry entry : row.entrySet()) {
String field = entry.getKey();
if (field != null && AuditVisualLogListener.isCredentialReference(field)
&& hasValue(entry.getValue())) {
ordered.add(field.toLowerCase(Locale.ROOT));
}
}
if (spec != null) {
for (String field : spec.mainFields()) {
if (field == null) {
continue;
}
String lower = field.toLowerCase(Locale.ROOT);
if (hasValue(valueOf(row, lower))) {
ordered.add(lower);
}
}
}
return ordered.isEmpty() ? Collections.emptyList()
: Collections.unmodifiableList(new ArrayList(ordered));
}
/**
* 按签名 id 批量读取签名事实。查不到的 id 不出现在结果里(= 那个字段不是签名字段)。
*
* 任何异常都返回空 Map 并告警:签名事实读不到时,宁可这次不产出签名事件,
* 也不能因此丢掉业务事件——审计的红线是「这次改过不能缺」。
*
*
🔴 按租户收窄(2026-08-05 批 D):{@code lims_sign} 主键是
* {@code (f_id, f_tenant_id)},{@code f_id} 单列唯一不是数据库不变量——「雪花 id
* 全局唯一」是实现约定不是约束。批 A 已经在历史反查侧({@code selectRecordedSignIds})
* 钉过这一条,但读取侧这一半当时漏了:不收窄的话,请求里塞一个别的租户的 signId
* 照样能读出那一行,然后被当成本次操作的签名凭证写进 append-only 的审计表。
* 实查当前库 1306 条签名全在租户 '0'、0 个跨租户重复的 f_id——是埋着的陷阱不是活的故障,
* 但两侧口径必须一致,否则下一个人只会看到「反查按租户、读取不按」而无从判断哪个是对的。
*
* @param tenantId 调用方的租户({@code AuditCtx.tenantId},永不为 null,见其默认值)
*/
public Map read(Collection signIds, String tenantId) {
if (signIds == null || signIds.isEmpty()) {
return Collections.emptyMap();
}
List ids = new ArrayList();
for (String id : signIds) {
if (id != null && !id.trim().isEmpty() && ids.size() < MAX_IDS) {
ids.add(id.trim());
}
}
if (ids.isEmpty()) {
return Collections.emptyMap();
}
StringBuilder sql = new StringBuilder(
"SELECT f_id, account_name, f_creator_user_id, note, op_type, f_creator_time, meta_data FROM ")
.append(SIGN_TABLE).append(" WHERE f_id IN (");
for (int i = 0; i < ids.size(); i++) {
sql.append(i == 0 ? "?" : ",?");
}
sql.append(") AND f_tenant_id = ?");
List