package jnpf.bizcommon.audit.service.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import jnpf.audit.AuditConsts;
|
import jnpf.bizcommon.audit.entity.AuditEventEntity;
|
import jnpf.bizcommon.audit.entity.model.AuditEventDetailVO;
|
import jnpf.bizcommon.audit.entity.model.AuditEventListVO;
|
import jnpf.bizcommon.audit.entity.model.AuditOperationGroupRow;
|
import jnpf.bizcommon.audit.entity.model.AuditPageQuery;
|
import jnpf.bizcommon.audit.mapper.AuditEventMapper;
|
import jnpf.bizcommon.audit.mapper.AuditQueryMapper;
|
import jnpf.bizcommon.audit.service.AuditFieldDiffViewService;
|
import jnpf.bizcommon.audit.service.AuditFormRegistry;
|
import jnpf.bizcommon.audit.service.AuditQueryService;
|
import jnpf.bizcommon.audit.service.support.AuditQueryResponseSanitizer;
|
import jnpf.bizcommon.audit.service.support.AuditEventCategories;
|
import jnpf.base.vo.PageListVO;
|
import jnpf.base.vo.PaginationVO;
|
import jnpf.exception.DataException;
|
import jnpf.util.JsonUtil;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Service;
|
import org.springframework.util.StringUtils;
|
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
/** 审计事件查询服务;写入侧完成物理合并,查询侧仅按 operationId 组织展示关系。 */
|
@Service
|
public class AuditQueryServiceImpl implements AuditQueryService {
|
|
/** timeline 全量上限(spec:LIMIT 500) */
|
private static final int TIMELINE_LIMIT = 500;
|
/** 列表只下发前 3 条子表标题 + 总数——保持「列表不含 extra 正文」的敏感面约定。 */
|
private static final int LIST_SUB_TITLE_LIMIT = 3;
|
/** 分页大小上限(Step 2 契约:强制 pageSize<=100) */
|
private static final long MAX_PAGE_SIZE = 100L;
|
private static final long DEFAULT_PAGE_SIZE = 20L;
|
|
@Autowired
|
private AuditQueryMapper auditQueryMapper;
|
|
@Autowired
|
private AuditEventMapper auditEventMapper;
|
|
@Autowired
|
private AuditFormRegistry auditFormRegistry;
|
|
@Autowired
|
private AuditFieldDiffViewService auditFieldDiffViewService;
|
|
@Override
|
public PageListVO<AuditEventListVO> page(AuditPageQuery query) {
|
long pageSize = query.getPageSize();
|
if (pageSize <= 0) {
|
pageSize = DEFAULT_PAGE_SIZE;
|
} else if (pageSize > MAX_PAGE_SIZE) {
|
pageSize = MAX_PAGE_SIZE;
|
}
|
long currentPage = query.getCurrentPage();
|
if (currentPage <= 0) {
|
currentPage = 1L;
|
}
|
// Codex 批次三a Minor#3:OFFSET 溢出防护——currentPage 传超大值(如 Long.MAX_VALUE)时
|
// (currentPage-1)*pageSize 会 long 乘法溢出成负数,拼进 SQL LIMIT/OFFSET 语义未定义。
|
// Math.multiplyExact 显式检测溢出,命中则降级为空页(total 仍按条件正常返回,不查明细页)。
|
long offset;
|
boolean offsetOverflow;
|
try {
|
offset = Math.multiplyExact(currentPage - 1, pageSize);
|
offsetOverflow = false;
|
} catch (ArithmeticException ex) {
|
offset = 0L;
|
offsetOverflow = true;
|
}
|
|
QueryWrapper<AuditEventEntity> ew = buildWrapper(query);
|
long total = auditQueryMapper.countOperations(ew);
|
List<AuditEventListVO> list;
|
if (offsetOverflow) {
|
list = Collections.emptyList();
|
} else {
|
List<AuditOperationGroupRow> groups = auditQueryMapper.selectOperationPage(ew, offset, pageSize);
|
list = buildOperationRows(groups);
|
}
|
|
// 回写生效后的分页参数供返回包装读取(对照 LimsBizLogController:Pagination 承载 total,
|
// JsonUtil.getJsonToBean 窄化成 PaginationVO——同一惯例,仅挪到 Service 层以贴合本任务
|
// 字面执行的 Controller 签名)
|
query.setCurrentPage(currentPage);
|
query.setPageSize(pageSize);
|
query.setTotal(total);
|
|
PageListVO<AuditEventListVO> vo = new PageListVO<>();
|
vo.setList(list);
|
vo.setPagination(JsonUtil.getJsonToBean(query, PaginationVO.class));
|
return vo;
|
}
|
|
@Override
|
public AuditEventDetailVO detail(Long id) {
|
AuditEventEntity entity = auditEventMapper.selectById(id);
|
if (entity == null) {
|
throw new DataException("审计事件不存在:id=" + id);
|
}
|
return toDetailVo(entity);
|
}
|
|
@Override
|
public List<AuditEventListVO> timeline(String targetId) {
|
List<AuditOperationGroupRow> groups = groupRows(
|
auditQueryMapper.selectTimeline(targetId, TIMELINE_LIMIT));
|
// target_id 只属于业务目标行;签名等关联事件使用自己的 target_id,需按 operation_id 补查。
|
return buildOperationRows(groups);
|
}
|
|
private List<AuditEventListVO> buildOperationRows(List<AuditOperationGroupRow> groups) {
|
if (groups.isEmpty()) {
|
return Collections.emptyList();
|
}
|
List<String> gids = new ArrayList<>(groups.size());
|
for (AuditOperationGroupRow group : groups) {
|
gids.add(group.getGid());
|
}
|
return buildOperationRows(groups, auditQueryMapper.selectByOperationGids(gids));
|
}
|
|
/** 将已物理合并的事件挂到 BUSINESS 主行;这里不得再次组合 fieldDiffs。 */
|
private List<AuditEventListVO> buildOperationRows(List<AuditOperationGroupRow> groups,
|
List<AuditEventEntity> events) {
|
Map<String, List<AuditEventEntity>> eventsByGid = new LinkedHashMap<>();
|
for (AuditEventEntity event : events) {
|
String gid = operationGid(event);
|
eventsByGid.computeIfAbsent(gid, ignored -> new ArrayList<>()).add(event);
|
}
|
|
List<AuditEventListVO> result = new ArrayList<>(groups.size());
|
for (AuditOperationGroupRow group : groups) {
|
List<AuditEventEntity> related = eventsByGid.get(group.getGid());
|
if (related == null) {
|
continue;
|
}
|
AuditEventEntity business = null;
|
List<AuditEventListVO> signatures = new ArrayList<>(2);
|
for (AuditEventEntity event : related) {
|
if (AuditEventCategories.BUSINESS.equals(event.getEventCategory())) {
|
business = event;
|
} else if (AuditEventCategories.PRIMARY_SIGN.equals(event.getEventCategory())
|
|| AuditEventCategories.REVIEW_SIGN.equals(event.getEventCategory())) {
|
signatures.add(toListVo(event));
|
}
|
}
|
if (business == null) {
|
continue;
|
}
|
AuditEventListVO main = toListVo(business);
|
main.setSubEvents(signatures);
|
main.setSigned(!signatures.isEmpty());
|
result.add(main);
|
}
|
return result;
|
}
|
|
private List<AuditOperationGroupRow> groupRows(List<AuditEventEntity> events) {
|
Map<String, AuditOperationGroupRow> groups = new LinkedHashMap<>();
|
for (AuditEventEntity event : events) {
|
String gid = operationGid(event);
|
AuditOperationGroupRow group = groups.get(gid);
|
if (group == null) {
|
group = new AuditOperationGroupRow();
|
group.setGid(gid);
|
group.setGrpTime(event.getEventTime());
|
groups.put(gid, group);
|
}
|
}
|
return new ArrayList<>(groups.values());
|
}
|
|
private String operationGid(AuditEventEntity event) {
|
return StringUtils.hasText(event.getOperationId()) ? event.getOperationId() : event.getClientEventId();
|
}
|
|
private QueryWrapper<AuditEventEntity> buildWrapper(AuditPageQuery query) {
|
QueryWrapper<AuditEventEntity> ew = new QueryWrapper<>();
|
if (query.getStartTime() != null) {
|
ew.ge("event_time", query.getStartTime());
|
}
|
if (query.getEndTime() != null) {
|
ew.le("event_time", query.getEndTime());
|
}
|
if (StringUtils.hasText(query.getOperatorName())) {
|
ew.like("operator_name", query.getOperatorName());
|
}
|
if (StringUtils.hasText(query.getAppName())) {
|
ew.eq("app_name", query.getAppName());
|
}
|
// Codex 批次三a Important#1:biz_module 是用户视角的业务应用筛选(app_name 层 0 固定为 jnpf-visualdev)
|
if (StringUtils.hasText(query.getBizModule())) {
|
ew.eq("biz_module", query.getBizModule());
|
}
|
if (StringUtils.hasText(query.getEventType())) {
|
ew.eq("event_type", query.getEventType());
|
}
|
if (StringUtils.hasText(query.getActionCode())) {
|
ew.eq("action_code", query.getActionCode());
|
}
|
if (StringUtils.hasText(query.getTargetTable())) {
|
ew.eq("target_table", query.getTargetTable());
|
}
|
if (StringUtils.hasText(query.getBizCode())) {
|
ew.eq("biz_code", query.getBizCode());
|
}
|
if (StringUtils.hasText(query.getBizType())) {
|
ew.eq("biz_type", query.getBizType());
|
}
|
if (StringUtils.hasText(query.getKeyword())) {
|
ew.like("field_diffs", query.getKeyword());
|
}
|
return ew;
|
}
|
|
private AuditEventListVO toListVo(AuditEventEntity e) {
|
AuditEventListVO vo = new AuditEventListVO();
|
vo.setId(e.getId());
|
vo.setOperationId(e.getOperationId());
|
vo.setClientEventId(e.getClientEventId());
|
vo.setEventTime(e.getEventTime());
|
vo.setTenantId(e.getTenantId());
|
vo.setOperatorId(e.getOperatorId());
|
vo.setOperatorName(e.getOperatorName());
|
vo.setOperatorOrgId(e.getOperatorOrgId());
|
vo.setAppName(e.getAppName());
|
vo.setBizModule(e.getBizModule());
|
vo.setIp(e.getIp());
|
vo.setIpRegion(e.getIpRegion());
|
vo.setBrowser(e.getBrowser());
|
vo.setOs(e.getOs());
|
vo.setRequestUri(e.getRequestUri());
|
vo.setEventType(e.getEventType());
|
if (AuditEventCategories.REVIEW_SIGN.equals(e.getEventCategory())) {
|
vo.setSignatureRole("REVIEW");
|
} else if (AuditEventCategories.PRIMARY_SIGN.equals(e.getEventCategory())) {
|
vo.setSignatureRole("PRIMARY");
|
}
|
vo.setActionCode(e.getActionCode());
|
vo.setActionLabel(e.getActionLabel());
|
vo.setSourceLayer(e.getSourceLayer());
|
vo.setTargetTable(e.getTargetTable());
|
vo.setTargetId(AuditQueryResponseSanitizer.hidesTargetId(e.getTargetTable())
|
? null : e.getTargetId());
|
vo.setBizType(e.getBizType());
|
vo.setBizCode(e.getBizCode());
|
vo.setDiffCount(computeDiffCount(e.getFieldDiffs()));
|
vo.setReason(e.getReason());
|
vo.setRecordTitle(e.getRecordTitle());
|
vo.setEntryType(e.getEntryType());
|
vo.setEntryId(e.getEntryId());
|
vo.setEntryName(e.getEntryName());
|
vo.setCreatedAt(e.getCreatedAt());
|
// 子事件保留自身签名标记;BUSINESS 主行会在组装后按关联签名重新赋值。
|
vo.setSigned(AuditConsts.TYPE_E_SIGNATURE.equals(e.getEventType()));
|
applySubTitles(vo, e.getExtra());
|
return vo;
|
}
|
|
/**
|
* 从 extra.recordSubTitles 取子表标题行,只下发前 {@link #LIST_SUB_TITLE_LIMIT} 条 + 总数。
|
* 不整体下发 extra:正文里除 dataSnapshot 外还有各种来源标记与上下文,列表接口不该带出去。
|
*/
|
@SuppressWarnings("unchecked")
|
private void applySubTitles(AuditEventListVO vo, String extraJson) {
|
if (!StringUtils.hasText(extraJson)) {
|
return;
|
}
|
try {
|
Map<String, Object> extra = JsonUtil.stringToMap(extraJson);
|
Object raw = extra == null ? null : extra.get("recordSubTitles");
|
if (!(raw instanceof List)) {
|
return;
|
}
|
List<?> all = (List<?>) raw;
|
List<String> head = new ArrayList<>();
|
for (Object item : all) {
|
if (head.size() >= LIST_SUB_TITLE_LIMIT) {
|
break;
|
}
|
if (item != null) {
|
head.add(String.valueOf(item));
|
}
|
}
|
vo.setSubTitles(head);
|
vo.setSubTitleCount(all.size());
|
} catch (Throwable ignored) {
|
// extra 解析失败不影响列表主行——标题是锦上添花,不能炸掉整页
|
}
|
}
|
|
private AuditEventDetailVO toDetailVo(AuditEventEntity e) {
|
AuditEventDetailVO vo = new AuditEventDetailVO();
|
vo.setId(e.getId());
|
vo.setClientEventId(e.getClientEventId());
|
vo.setOperationId(e.getOperationId());
|
vo.setEventTime(e.getEventTime());
|
vo.setTenantId(e.getTenantId());
|
vo.setOperatorId(e.getOperatorId());
|
vo.setOperatorName(e.getOperatorName());
|
vo.setOperatorOrgId(e.getOperatorOrgId());
|
vo.setAppName(e.getAppName());
|
vo.setBizModule(e.getBizModule());
|
vo.setIp(e.getIp());
|
vo.setIpRegion(e.getIpRegion());
|
vo.setBrowser(e.getBrowser());
|
vo.setOs(e.getOs());
|
vo.setRequestUri(e.getRequestUri());
|
vo.setEventType(e.getEventType());
|
vo.setActionCode(e.getActionCode());
|
vo.setActionLabel(e.getActionLabel());
|
vo.setSourceLayer(e.getSourceLayer());
|
vo.setTargetTable(e.getTargetTable());
|
vo.setTargetId(AuditQueryResponseSanitizer.hidesTargetId(e.getTargetTable())
|
? null : e.getTargetId());
|
vo.setBizType(e.getBizType());
|
vo.setBizCode(e.getBizCode());
|
String modelId = AuditQueryResponseSanitizer.sourceModelId(e.getExtra());
|
Map<String, jnpf.audit.diff.AuditFieldDiff.FieldMeta> metadata =
|
auditFormRegistry.resolveFieldMetadata(modelId, e.getTargetTable());
|
String sanitizedFieldDiffs = AuditQueryResponseSanitizer.sanitizeFieldDiffs(
|
e.getFieldDiffs(), metadata);
|
vo.setFieldDiffs(sanitizedFieldDiffs);
|
vo.setFieldDiffList(auditFieldDiffViewService.build(e, sanitizedFieldDiffs, metadata));
|
vo.setExtra(AuditQueryResponseSanitizer.sanitizeJson(e.getExtra()));
|
vo.setDataSnapshot(AuditQueryResponseSanitizer.sanitizeJson(e.getDataSnapshot(), metadata));
|
vo.setReason(e.getReason());
|
vo.setRecordTitle(e.getRecordTitle());
|
vo.setEntryType(e.getEntryType());
|
vo.setEntryId(e.getEntryId());
|
vo.setEntryName(e.getEntryName());
|
vo.setPrevHash(e.getPrevHash());
|
vo.setCreatedAt(e.getCreatedAt());
|
return vo;
|
}
|
|
/** field_diffs 是 VisualLogModel[] JSON;空/非法 JSON 一律降级为 0,不让摘要计算炸列表接口。 */
|
private Integer computeDiffCount(String fieldDiffs) {
|
if (!StringUtils.hasText(fieldDiffs)) {
|
return 0;
|
}
|
return AuditQueryResponseSanitizer.effectiveDiffCount(fieldDiffs);
|
}
|
}
|