package jnpf.audit.sdk;
|
|
import cn.hutool.http.useragent.UserAgent;
|
import cn.hutool.http.useragent.UserAgentUtil;
|
import jnpf.audit.AuditApiConsts;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.base.UserInfo;
|
import jnpf.util.IpUtil;
|
import jnpf.util.RandomUtil;
|
import jnpf.util.ServletUtil;
|
import jnpf.util.UserProvider;
|
import org.springframework.web.context.request.RequestContextHolder;
|
|
import java.util.Date;
|
|
/**
|
* 审计上下文采集器(Task 7 Step 1):只填 DTO 的空缺字段,不覆盖调用方已给值。
|
*
|
* <p>操作人/租户取自 {@link UserProvider}(Sa-Token 登录上下文);IP/浏览器/OS 取自请求上下文,
|
* 工具与平台 {@code jnpf.RequestLogAspect} 同款({@link IpUtil#getIpAddr()} / {@link IpUtil#getIpCity(String)} /
|
* {@link ServletUtil#getUserAgent()} + hutool {@link UserAgentUtil#parse(String)})。
|
*
|
* <p>关键约束(终验#6):operationId **不由本采集器生成、也不写 MDC**——生成与 MDC 写入统一归
|
* {@link AuditTxHolder};采集器若先写 MDC,会让 holder 读到的 previousMdc 变成新值本身,恢复等于没恢复。
|
*/
|
public class AuditContextCollector {
|
|
/** 无登录上下文时的操作人占位(异步/系统触发链路) */
|
private static final String OPERATOR_SYSTEM = "SYSTEM";
|
|
private final String appName;
|
|
public AuditContextCollector(String appName) {
|
this.appName = appName;
|
}
|
|
/** 填充空缺字段并返回同一 dto(供 AuditClient 冻结后交 holder 缓冲)。 */
|
public AuditEventDTO fill(AuditEventDTO dto) {
|
if (isBlank(dto.getClientEventId())) {
|
dto.setClientEventId(RandomUtil.uuId()); // 平台雪花(= String.valueOf(YitIdHelper.nextId()))
|
}
|
if (dto.getEventTime() == null) {
|
dto.setEventTime(new Date());
|
}
|
if (isBlank(dto.getAppName())) {
|
dto.setAppName(appName);
|
}
|
fillOperator(dto);
|
fillRequest(dto);
|
return dto;
|
}
|
|
/**
|
* 操作人/租户:有登录用户则填 userId/userName/organizeId/tenantId;无则 operator=SYSTEM。
|
*
|
* <p>租户回落(M2 阻断项修复):单租户部署下 {@code user.getTenantId()} 返回**空串**,
|
* 无会话链路更是无从取得——两种情况都在最后统一回落到 {@link AuditApiConsts#DEFAULT_TENANT_ID},
|
* 否则服务端 validate 会以「tenantId 不能为空」拒收,事件进 %RETRY% 直至 DLQ。
|
* 回落只在空白时发生,多租户部署下真实租户号不受影响。
|
*/
|
private void fillOperator(AuditEventDTO dto) {
|
UserInfo user = currentUser();
|
if (user != null && !isBlank(user.getUserId())) {
|
if (isBlank(dto.getOperatorId())) {
|
dto.setOperatorId(user.getUserId());
|
}
|
if (isBlank(dto.getOperatorName())) {
|
dto.setOperatorName(user.getUserName());
|
}
|
if (isBlank(dto.getOperatorOrgId())) {
|
dto.setOperatorOrgId(user.getOrganizeId());
|
}
|
if (isBlank(dto.getTenantId())) {
|
dto.setTenantId(user.getTenantId());
|
}
|
} else if (isBlank(dto.getOperatorId())) {
|
// 无登录上下文:operator=SYSTEM(tenantId 由下方哨兵回落兜底)
|
dto.setOperatorId(OPERATOR_SYSTEM);
|
}
|
if (isBlank(dto.getTenantId())) {
|
dto.setTenantId(AuditApiConsts.DEFAULT_TENANT_ID);
|
}
|
}
|
|
/** IP/浏览器/OS:仅在有请求上下文时采集,best-effort,采集失败静默降级不影响审计主流程。 */
|
private void fillRequest(AuditEventDTO dto) {
|
if (RequestContextHolder.getRequestAttributes() == null) {
|
return; // 无请求上下文(异步/系统触发)
|
}
|
try {
|
if (isBlank(dto.getIp())) {
|
String ip = IpUtil.getIpAddr();
|
dto.setIp(ip);
|
if (isBlank(dto.getIpRegion())) {
|
dto.setIpRegion(IpUtil.getIpCity(ip));
|
}
|
}
|
String ua = ServletUtil.getUserAgent();
|
if (!isBlank(ua)) {
|
UserAgent agent = UserAgentUtil.parse(ua);
|
if (isBlank(dto.getBrowser())) {
|
dto.setBrowser(join(agent.getBrowser().getName(), agent.getVersion()));
|
}
|
if (isBlank(dto.getOs())) {
|
dto.setOs(join(agent.getPlatform().getName(), agent.getOsVersion()));
|
}
|
}
|
} catch (Exception ignore) {
|
// 上下文采集属 best-effort,任何异常不得冒泡影响业务与审计主链路
|
}
|
}
|
|
private static UserInfo currentUser() {
|
try {
|
return UserProvider.getUser();
|
} catch (Exception e) {
|
return null; // 未登录/无 token(Sa-Token 可能抛异常),视为无用户
|
}
|
}
|
|
private static String join(String a, String b) {
|
String x = a == null ? "" : a.trim();
|
String y = b == null ? "" : b.trim();
|
String r = (x + " " + y).trim();
|
return r.isEmpty() ? null : r;
|
}
|
|
private static boolean isBlank(String s) {
|
return s == null || s.trim().isEmpty();
|
}
|
}
|