package jnpf.dmsIntegration.audit; import com.alibaba.fastjson.JSON; import jakarta.servlet.http.HttpServletRequest; import jnpf.audit.AuditConsts; import jnpf.audit.diff.AuditFieldDiff; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.model.AuditRelatedSignDTO; import jnpf.audit.sdk.AuditClient; import jnpf.base.ModuleApi; import jnpf.base.entity.ModuleEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinJiluEntity; import jnpf.dmsEntity.fenfa.DmsWenjianDayinRenwuEntity; import jnpf.dmsEntity.fenfa.DmsWenjianFenfaRenwuEntity; import jnpf.dmsEntity.fenfa.DmsWenjianHuishouMingxiEntity; import jnpf.dmsEntity.permission.entity.DmsAccessExclusionEntity; import jnpf.dmsEntity.permission.entity.DmsFilePermissionEntity; import jnpf.util.JsonUtil; import jnpf.util.ServletUtil; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; @Slf4j @Component public class DmsAuditAdapter { private static final String FOLDER_TABLE = "dms_folders"; private static final String FOLDER_BIZ_MODULE = "DMS"; private static final String FOLDER_BIZ_TYPE = "DMS_FOLDER"; private static final String FILE_TABLE = "dms_documents"; private static final String FILE_VERSION_TABLE = "dms_file_versions"; private static final String VERSION_STATE_TABLE = "dms_version_states"; private static final String FILE_BIZ_TYPE = "DMS_FILE"; private static final String FILE_ENTRY_TYPE = "MENU"; private static final String FILE_ENTRY_ID = "dms-file-center"; private static final String FILE_ENTRY_NAME = "DMS 文件中心"; private static final String PERMISSION_BIZ_TYPE = "DMS_PERMISSION"; private static final String PERMISSION_ENTRY_ID = "dms-permission-management"; private static final String PERMISSION_ENTRY_NAME = "DMS 权限管理"; private static final String RECYCLE_MAIN_TABLE = "dms_wenjian_huishou"; private static final String RECYCLE_DETAIL_TABLE = "dms_wenjian_huishou_zixiang"; private static final String PRINT_RECORD_TABLE = "dms_wenjian_dayin_jilu"; private static final String RECYCLE_BIZ_TYPE = "DMS_FILE_RECYCLE"; private static final String RECYCLE_ENTRY_ID = "POST:/api/dms/biz/wenjian/fenfa/huishou"; private static final String RECYCLE_ENTRY_NAME = "文件扫码回收"; private static final String PRINT_TASK_TABLE = "dms_wenjian_dayin_renwu"; private static final String DISTRIBUTION_TASK_TABLE = "dms_wenjian_fenfa_renwu"; private static final String PRINT_BIZ_TYPE = "DMS_FILE_PRINT"; private static final String PRINT_SOURCE = "LIST_PRINT"; private static final long MENU_CACHE_MILLIS = 5 * 60 * 1000L; private static final Map FOLDER_FIELD_META; private static final Map FILE_FIELD_META; private static final Map PERMISSION_FIELD_META; private static final Map EXCLUSION_FIELD_META; private static final Map RECYCLE_MAIN_FIELD_META; private static final Map RECYCLE_DETAIL_FIELD_META; private static final Map RECYCLE_PRINT_FIELD_META; private static final Map PRINT_TASK_FIELD_META; private static final Map PRINT_RECORD_FIELD_META; private static final Map DISTRIBUTION_TASK_FIELD_META; static { Map meta = new LinkedHashMap<>(); meta.put("name", new AuditFieldDiff.FieldMeta("文件夹名称", null, null)); meta.put("parent_id", new AuditFieldDiff.FieldMeta("父文件夹", null, null)); FOLDER_FIELD_META = Collections.unmodifiableMap(meta); Map fileMeta = new LinkedHashMap<>(); fileMeta.put("document_no", meta("文件编号")); fileMeta.put("name", meta("文件名称")); fileMeta.put("document_type", meta("文件类型")); fileMeta.put("responsible_dept_id", meta("归属部门")); fileMeta.put("folder_id", meta("所属文件夹")); fileMeta.put("version_no", meta("版本序号")); fileMeta.put("version_label", meta("版本号")); fileMeta.put("file_name", meta("文件名")); fileMeta.put("mime_type", meta("MIME 类型")); fileMeta.put("size_bytes", meta("文件大小")); fileMeta.put("checksum_sha256", meta("SHA-256")); fileMeta.put("prepared_by", meta("制定人")); fileMeta.put("prepared_on", meta("制定日期")); fileMeta.put("lifecycle_status", lifecycleStatusMeta()); fileMeta.put("effective_from", meta("生效时间")); fileMeta.put("effective_to", meta("失效时间")); fileMeta.put("status_reason", meta("状态变更原因")); FILE_FIELD_META = Collections.unmodifiableMap(fileMeta); Map permissionMeta = new LinkedHashMap<>(); permissionMeta.put("resource_type", options("资源类型", "FOLDER", "文件夹", "DOCUMENT", "文件", "FILE_VERSION", "文件版本")); permissionMeta.put("resource_id", meta("授权资源")); permissionMeta.put("resource_scope", options("授权范围", "SELF", "当前资源", "SUBTREE", "当前文件夹及子树")); permissionMeta.put("principal_type", options("主体类型", "USER", "用户", "ORG", "组织", "POSITION", "岗位", "ROLE", "角色", "GROUP", "权限组")); permissionMeta.put("principal_id", meta("授权主体")); permissionMeta.put("include_child_orgs", booleanMeta("包含子组织")); permissionMeta.put("approved_org_scope", options("审核查阅部门范围", "SELF", "本部门", "CHILDREN", "本部门及直接子部门", "DESCENDANTS", "本部门及全部下级")); permissionMeta.put("action_code", actionMeta()); permissionMeta.put("source_type", sourceTypeMeta()); permissionMeta.put("source_id", meta("来源标识")); permissionMeta.put("valid_from", meta("生效时间")); permissionMeta.put("valid_to", meta("失效时间")); permissionMeta.put("max_use_count", meta("最大使用次数")); permissionMeta.put("used_count", meta("已使用次数")); permissionMeta.put("grant_reason", meta("授权原因")); permissionMeta.put("granted_by_type", options("授权人类型", "USER", "用户", "SYSTEM", "系统")); permissionMeta.put("granted_by_id", meta("授权人")); permissionMeta.put("revoked_at", meta("撤销时间")); permissionMeta.put("revoked_by_type", options("撤销人类型", "USER", "用户", "SYSTEM", "系统")); permissionMeta.put("revoked_by_id", meta("撤销人")); permissionMeta.put("revoke_reason", meta("撤销原因")); permissionMeta.put("revision", meta("修订号")); PERMISSION_FIELD_META = Collections.unmodifiableMap(permissionMeta); Map exclusionMeta = new LinkedHashMap<>(); exclusionMeta.put("resource_type", permissionMeta.get("resource_type")); exclusionMeta.put("resource_id", meta("排除资源")); exclusionMeta.put("user_id", meta("排除用户")); exclusionMeta.put("action_code", actionMeta()); exclusionMeta.put("source_type", sourceTypeMeta()); exclusionMeta.put("source_id", meta("来源标识")); exclusionMeta.put("valid_from", meta("生效时间")); exclusionMeta.put("valid_to", meta("失效时间")); exclusionMeta.put("exclude_reason", meta("排除原因")); exclusionMeta.put("excluded_by_type", options("排除操作人类型", "USER", "用户", "SYSTEM", "系统")); exclusionMeta.put("excluded_by_id", meta("排除操作人")); exclusionMeta.put("revoked_at", meta("撤销时间")); exclusionMeta.put("revoked_by_type", permissionMeta.get("revoked_by_type")); exclusionMeta.put("revoked_by_id", permissionMeta.get("revoked_by_id")); exclusionMeta.put("revoke_reason", permissionMeta.get("revoke_reason")); exclusionMeta.put("revision", permissionMeta.get("revision")); EXCLUSION_FIELD_META = Collections.unmodifiableMap(exclusionMeta); Map recycleMainMeta = new LinkedHashMap<>(); recycleMainMeta.put("beizhu_shuoming", meta("回收备注")); RECYCLE_MAIN_FIELD_META = Collections.unmodifiableMap(recycleMainMeta); Map recycleDetailMeta = new LinkedHashMap<>(); recycleDetailMeta.put("f_foreign_id", meta("回收单")); recycleDetailMeta.put("dayin_jilu_id", meta("打印记录")); recycleDetailMeta.put("dayin_liushuihao", meta("打印流水号")); recycleDetailMeta.put("wenjian_bianhao", meta("文件编号")); recycleDetailMeta.put("wenjian_mingcheng", meta("文件名称")); recycleDetailMeta.put("banbenhao", meta("版本号")); recycleDetailMeta.put("qianshou_bumen", meta("签收部门")); recycleDetailMeta.put("qianshou_ren", meta("签收人")); recycleDetailMeta.put("qianshou_shijian", meta("签收时间")); recycleDetailMeta.put("huishou_ren_id", meta("回收人")); recycleDetailMeta.put("huishou_shijian", meta("回收时间")); RECYCLE_DETAIL_FIELD_META = Collections.unmodifiableMap(recycleDetailMeta); Map recyclePrintMeta = new LinkedHashMap<>(); recyclePrintMeta.put("biz_status", options( "文件副本状态", "daifafang", "待发放", "daiqianshou", "待签收", "daihuishou", "待回收", "daixiaohui", "待销毁")); RECYCLE_PRINT_FIELD_META = Collections.unmodifiableMap(recyclePrintMeta); Map printTaskMeta = new LinkedHashMap<>(); printTaskMeta.put("biz_status", options( "打印状态", "daidayin", "待打印", "bufendayin", "部分打印", "yidayin", "已打印")); PRINT_TASK_FIELD_META = Collections.unmodifiableMap(printTaskMeta); Map printRecordMeta = new LinkedHashMap<>(); printRecordMeta.put("dayin_renwu_id", meta("打印任务")); printRecordMeta.put("dayin_xuhao", meta("打印序号")); printRecordMeta.put("dayin_liushuihao", meta("打印流水号")); printRecordMeta.put("dayin_jishu_liushuihao", meta("计数流水号")); printRecordMeta.put("dayin_ren_id", meta("打印人")); printRecordMeta.put("dayin_bumen_id", meta("打印部门")); printRecordMeta.put("dayin_shijian", meta("打印时间")); printRecordMeta.put("biz_status", options( "分发状态", "daifafang", "待发放", "yiguidang", "已归档")); PRINT_RECORD_FIELD_META = Collections.unmodifiableMap(printRecordMeta); Map distributionTaskMeta = new LinkedHashMap<>(); distributionTaskMeta.put("dayin_renwu_id", meta("打印任务")); distributionTaskMeta.put("fenfa_bianhao", meta("分发编号")); distributionTaskMeta.put("fafang_leixing", options( "发放类型", "zidongfenfa", "自动分发")); distributionTaskMeta.put("biz_status", options( "分发状态", "daifafang", "待发放")); DISTRIBUTION_TASK_FIELD_META = Collections.unmodifiableMap(distributionTaskMeta); } private final ObjectProvider auditClient; private final ObjectProvider permissionDisplayResolver; private final ObjectProvider moduleApi; private final Map menuCache = new ConcurrentHashMap<>(); @Autowired public DmsAuditAdapter(ObjectProvider auditClient, ObjectProvider permissionDisplayResolver, ObjectProvider moduleApi) { this.auditClient = auditClient; this.permissionDisplayResolver = permissionDisplayResolver; this.moduleApi = moduleApi; } /** Test and compatibility constructor for callers without menu resolution. */ public DmsAuditAdapter(ObjectProvider auditClient, ObjectProvider permissionDisplayResolver) { this.auditClient = auditClient; this.permissionDisplayResolver = permissionDisplayResolver; this.moduleApi = null; } /** Test and compatibility constructor for callers that do not need permission display resolution. */ public DmsAuditAdapter(ObjectProvider auditClient) { this.auditClient = auditClient; this.permissionDisplayResolver = null; this.moduleApi = null; } public void dataChange(String actionCode, String actionLabel, String table, String id, String reason, String extra) { record(AuditConsts.TYPE_DATA_CHANGE, actionCode, actionLabel, table, id, reason, extra); } public void permissionChange(String actionCode, String actionLabel, List before, List after, List relatedSigns) { try { List retained = retainedPermissions(before, after); if (retained.isEmpty()) return; DmsPermissionAuditDisplayResolver.DisplayValues displays = permissionDisplays(retained); List> targets = new ArrayList<>(); Map beforeById = permissionsById(before); for (DmsFilePermissionEntity item : retained) { List> changes = permissionDiffs(beforeById.get(item.getId()), item, displays, false); targets.add(permissionTarget(item, changes, displays)); } DmsFilePermissionEntity first = retained.get(0); PermissionActionSummary actions = permissionActions(retained); Map extra = batchExtra(targets); extra.put("permissionId", value(first.getId())); extra.put("actionCodes", actions.codes); extra.put("actionNames", actions.names); extra.put("sourceType", first.getSourceType()); extra.put("sourceId", first.getSourceId()); List> firstDiffs = permissionDiffs( beforeById.get(first.getId()), first, displays, true); combineActionDiff(firstDiffs, actions); Map firstSnapshot = permissionSnapshot(first, displays); combineActionSnapshot(firstSnapshot, actions); recordPermissionEvent(actionCode, actionLabel, first, firstDiffs, firstSnapshot, extra, first.getRevokeReason() == null ? first.getGrantReason() : first.getRevokeReason(), permissionTitle(first, displays, actions.display), relatedSigns); } catch (RuntimeException ex) { log.warn("DMS permission audit event construction failed actionCode={}", actionCode, ex); } } public void accessExclusionChange(String actionCode, String actionLabel, List before, List after, List relatedSigns) { try { List retained = retainedExclusions(before, after); if (retained.isEmpty()) return; DmsPermissionAuditDisplayResolver.DisplayValues displays = exclusionDisplays(retained); List> targets = new ArrayList<>(); Map beforeById = exclusionsById(before); for (DmsAccessExclusionEntity item : retained) { List> changes = exclusionDiffs(beforeById.get(item.getId()), item, displays, false); targets.add(exclusionTarget(item, changes, displays)); } DmsAccessExclusionEntity first = retained.get(0); Map extra = batchExtra(targets); extra.put("exclusionId", value(first.getId())); extra.put("sourceType", first.getSourceType()); extra.put("sourceId", first.getSourceId()); recordExclusionEvent(actionCode, actionLabel, first, exclusionDiffs(beforeById.get(first.getId()), first, displays, true), exclusionSnapshot(first, displays), extra, first.getRevokeReason() == null ? first.getExcludeReason() : first.getRevokeReason(), exclusionTitle(first, displays), relatedSigns); } catch (RuntimeException ex) { log.warn("DMS access exclusion audit event construction failed actionCode={}", actionCode, ex); } } public void folderChange(DmsFolderAuditEvent event) { if (event == null || event.getFolderId() == null) { throw new IllegalArgumentException("文件夹审计事件和 folderId 不能为空"); } DmsFolderAuditSnapshot before = event.getBefore(); DmsFolderAuditSnapshot after = event.getAfter(); Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", 1); extra.put("oldParentId", value(before == null ? null : before.getParentId())); extra.put("newParentId", value(after == null ? null : after.getParentId())); extra.put("oldParentName", before == null ? null : before.getParentName()); extra.put("newParentName", after == null ? null : after.getParentName()); extra.put("oldPath", before == null ? null : before.getPath()); extra.put("newPath", after == null ? null : after.getPath()); extra.put("requestRevision", event.getRequestRevision()); extra.put("resultRevision", after == null ? null : after.getRevision()); extra.put("signatureRequired", event.isSignatureRequired()); DmsFolderAuditSnapshot retained = after == null ? before : after; auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode(event.getActionCode()) .actionLabel(event.getActionLabel()) .bizModule(FOLDER_BIZ_MODULE) .bizType(FOLDER_BIZ_TYPE) .bizCode(event.getFolderId().toString()) .targetTable(FOLDER_TABLE) .targetId(event.getFolderId().toString()) .fieldDiffs(AuditFieldDiff.diff(diffData(before), diffData(after), Collections.emptySet(), FOLDER_FIELD_META)) .dataSnapshot(JsonUtil.getObjectToString(snapshotData(retained))) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(title(before, after, event.getFolderId().toString())) .entryType(event.getEntryType()) .entryId(event.getEntryId()) .entryName(event.getEntryName()) .relatedSigns(event.getRelatedSigns()) .sourceLayer(3) .build())); } public void fileChange(DmsFileAuditEvent event) { if (event == null || event.getDocumentId() == null) { throw new IllegalArgumentException("文件审计事件和 documentId 不能为空"); } DmsFileAuditSnapshot before = event.getBefore(); DmsFileAuditSnapshot after = event.getAfter(); DmsFileAuditSnapshot retained = after == null ? before : after; List> diffs = new ArrayList<>(); addScopedDiffs(diffs, FILE_TABLE, event.getDocumentId().toString(), documentDiffData(before), documentDiffData(after)); DmsFileVersionAuditSnapshot beforeVersion = before == null ? null : before.getCurrentVersion(); DmsFileVersionAuditSnapshot afterVersion = after == null ? null : after.getCurrentVersion(); if (afterVersion != null && (beforeVersion == null || !afterVersion.getFileVersionId().equals(beforeVersion.getFileVersionId()))) { addScopedDiffs(diffs, FILE_VERSION_TABLE, value(afterVersion.getFileVersionId()), null, versionDiffData(afterVersion)); addScopedDiffs(diffs, VERSION_STATE_TABLE, value(afterVersion.getFileVersionId()), null, stateDiffData(afterVersion)); } if (beforeVersion != null && event.getPreviousVersionAfter() != null) { addScopedDiffs(diffs, VERSION_STATE_TABLE, value(beforeVersion.getFileVersionId()), stateDiffData(beforeVersion), stateDiffData(event.getPreviousVersionAfter())); } Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", 1); extra.put("sourceServiceId", event.getSourceServiceId()); extra.put("sourceId", event.getSourceId()); extra.put("baseFileVersionId", beforeVersion == null ? null : value(beforeVersion.getFileVersionId())); extra.put("newFileVersionId", afterVersion == null ? null : value(afterVersion.getFileVersionId())); extra.put("oldVersionLabel", beforeVersion == null ? null : beforeVersion.getVersionLabel()); extra.put("newVersionLabel", afterVersion == null ? null : afterVersion.getVersionLabel()); extra.put("oldChecksumSha256", beforeVersion == null ? null : beforeVersion.getChecksumSha256()); extra.put("newChecksumSha256", afterVersion == null ? null : afterVersion.getChecksumSha256()); auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode(event.getActionCode()) .actionLabel(event.getActionLabel()) .bizModule(FOLDER_BIZ_MODULE) .bizType(FILE_BIZ_TYPE) .bizCode(retained == null || retained.getDocumentNo() == null ? event.getDocumentId().toString() : retained.getDocumentNo()) .targetTable(FILE_TABLE) .targetId(event.getDocumentId().toString()) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(fileSnapshotData(retained))) .reason(event.getReason()) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(fileTitle(before, after, event.getDocumentId().toString())) .entryType(FILE_ENTRY_TYPE) .entryId(FILE_ENTRY_ID) .entryName(FILE_ENTRY_NAME) .relatedSigns(event.getRelatedSigns()) .sourceLayer(3) .build())); } /** 文件归档两阶段状态变更;不借用回收单据承载归档审计。 */ public void archiveStatus(List recordIds, String before, String after, Map signParameters) { if (recordIds == null || recordIds.isEmpty()) return; List> diffs = new ArrayList<>(); Map statusMeta = Collections.singletonMap("biz_status", options("文件副本状态", "daihuishou", "待回收", "daiguidang", "待归档", "yiguidang", "已归档")); for (String id : recordIds) { addScopedDiffs(diffs, PRINT_RECORD_TABLE, id, Collections.singletonMap("biz_status", before), Collections.singletonMap("biz_status", after), statusMeta); } Map snapshot = new LinkedHashMap<>(); snapshot.put("dayin_jilu_ids", recordIds); snapshot.put("before_status", before); snapshot.put("after_status", after); boolean completed = "yiguidang".equals(after); auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode(completed ? "FILE_ARCHIVE_COMPLETE" : "FILE_ARCHIVE_CONFIRM") .actionLabel(completed ? "文件归档完成" : "文件确认归档") .bizModule(FOLDER_BIZ_MODULE).bizType("DMS_FILE_ARCHIVE") .bizCode(recordIds.get(0)).targetTable(PRINT_RECORD_TABLE).targetId(recordIds.get(0)) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .relatedSigns(DmsAuditRequestSignResolver.resolve(signParameters)) .sourceLayer(3).build())); } public void recycle(DmsWenjianHuishouAuditEvent event) { if (event == null || blank(event.getHuishouDanjuId())) { throw new IllegalArgumentException("文件回收审计事件和回收单ID不能为空"); } if (event.getDetails() == null || event.getDetails().isEmpty()) { throw new IllegalArgumentException("文件回收审计明细不能为空"); } try { List> diffs = new ArrayList<>(); Map mainAfter = new LinkedHashMap<>(); mainAfter.put("beizhu_shuoming", event.getBeizhuShuoming()); addScopedDiffs(diffs, RECYCLE_MAIN_TABLE, event.getHuishouDanjuId(), null, mainAfter, RECYCLE_MAIN_FIELD_META); List> targets = new ArrayList<>(); List subtitles = new ArrayList<>(); for (DmsWenjianHuishouMingxiEntity detail : event.getDetails()) { addScopedDiffs(diffs, RECYCLE_DETAIL_TABLE, detail.getId(), null, recycleDetailDiffData(detail), RECYCLE_DETAIL_FIELD_META); String beforeStatus = event.getBeforeStatuses() == null ? null : event.getBeforeStatuses().get(detail.getDayinJiluId()); Map oldStatus = Collections.singletonMap( "biz_status", beforeStatus); Map newStatus = Collections.singletonMap( "biz_status", "daixiaohui"); addScopedDiffs(diffs, PRINT_RECORD_TABLE, detail.getDayinJiluId(), oldStatus, newStatus, RECYCLE_PRINT_FIELD_META); Map target = recycleTarget(detail, beforeStatus); targets.add(target); subtitles.add(String.valueOf(target.get("recordTitle"))); } Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", 1); extra.put("batch", targets.size() > 1); extra.put("batchSize", targets.size()); extra.put("affected", targets.size()); extra.put("affectedTargets", targets); extra.put("recordSubTitles", subtitles); auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode("FILE_RECYCLE") .actionLabel("文件回收") .bizModule(FOLDER_BIZ_MODULE) .bizType(RECYCLE_BIZ_TYPE) .bizCode(event.getHuishouDanjuId()) .targetTable(RECYCLE_MAIN_TABLE) .targetId(event.getHuishouDanjuId()) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(recycleSnapshot(event, targets))) .reason(event.getBeizhuShuoming()) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(recycleTitle(event.getDetails())) .entryType("API") .entryId(RECYCLE_ENTRY_ID) .entryName(RECYCLE_ENTRY_NAME) .relatedSigns(event.getRelatedSigns()) .sourceLayer(3) .build())); } catch (RuntimeException ex) { log.warn("DMS recycle audit event construction failed recycleId={}", event.getHuishouDanjuId(), ex); } } public void print(DmsWenjianDayinAuditEvent event) { try { List tasks = event == null ? Collections.emptyList() : orEmpty(event.getTasks()); List records = event == null ? Collections.emptyList() : orEmpty(event.getRecords()); if (tasks.isEmpty() || records.isEmpty()) { throw new IllegalArgumentException("文件打印审计任务和打印记录不能为空"); } Map taskById = new LinkedHashMap<>(); Map> recordsByTask = new LinkedHashMap<>(); for (DmsWenjianDayinRenwuEntity task : tasks) { if (task == null || blank(task.getId()) || taskById.put(task.getId(), task) != null) { throw new IllegalArgumentException("文件打印审计任务无效或重复"); } recordsByTask.put(task.getId(), new ArrayList<>()); } for (DmsWenjianDayinJiluEntity record : records) { List taskRecords = record == null ? null : recordsByTask.get(record.getDayinRenwuId()); if (record == null || blank(record.getId()) || taskRecords == null) { throw new IllegalArgumentException("打印记录未关联本次打印任务"); } taskRecords.add(record); } List> diffs = new ArrayList<>(); List> targets = new ArrayList<>(tasks.size()); List subtitles = new ArrayList<>(tasks.size()); for (DmsWenjianDayinRenwuEntity task : tasks) { List taskRecords = recordsByTask.get(task.getId()); if (taskRecords.isEmpty()) { throw new IllegalArgumentException("打印任务没有生成打印记录"); } if (event.getAfterStatuses() == null || blank(event.getAfterStatuses().get(task.getId()))) { throw new IllegalArgumentException("打印审计缺少实际更新状态"); } addScopedDiffs(diffs, PRINT_TASK_TABLE, task.getId(), Collections.singletonMap("biz_status", task.getBizStatus()), Collections.singletonMap("biz_status", event.getAfterStatuses().get(task.getId())), PRINT_TASK_FIELD_META); for (DmsWenjianDayinJiluEntity record : taskRecords) { addScopedDiffs(diffs, PRINT_RECORD_TABLE, record.getId(), null, printRecordDiffData(record), PRINT_RECORD_FIELD_META); } Map target = printTarget(task, taskRecords); targets.add(target); subtitles.add(String.valueOf(target.get("title"))); } for (DmsWenjianFenfaRenwuEntity distributionTask : orEmpty( event.getDistributionTasks())) { if (distributionTask != null && !blank(distributionTask.getId())) { addScopedDiffs(diffs, DISTRIBUTION_TASK_TABLE, distributionTask.getId(), null, distributionTaskDiffData(distributionTask), DISTRIBUTION_TASK_FIELD_META); } } boolean batch = tasks.size() > 1; int totalCopies = records.size(); PrintMenuContext menu = resolvePrintMenu(); Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", 1); extra.put("printName", "受控文件打印"); extra.put("targetTable", PRINT_TASK_TABLE); extra.put("bizType", PRINT_BIZ_TYPE); extra.put("source", PRINT_SOURCE); if (menu.menuId != null) extra.put("menuId", menu.menuId); if (menu.menuName != null) extra.put("menuName", menu.menuName); extra.put("menuResolution", menu.resolution); extra.put("printMode", batch ? "BATCH" : "SINGLE"); extra.put("printCount", tasks.size()); extra.put("totalCopies", totalCopies); extra.put("printTargets", targets); extra.put("recordSubTitles", subtitles); DmsWenjianDayinRenwuEntity single = batch ? null : tasks.get(0); String eventId = "PRINT-" + UUID.randomUUID().toString().replace("-", ""); auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .clientEventId(eventId) .operationId(eventId) .eventType(AuditConsts.TYPE_PRINT) .actionCode("PRINT") .actionLabel("发起打印") .bizModule(menu.menuName == null ? "待打印台账" : menu.menuName) .bizType(PRINT_BIZ_TYPE) .bizCode(single == null ? null : single.getWenjianBianhao()) .targetTable(PRINT_TASK_TABLE) .targetId(single == null ? null : single.getId()) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(printSnapshot(targets, totalCopies))) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(printTitle(tasks, totalCopies)) .entryType(menu.menuId == null ? null : "MENU") .entryId(menu.menuId) .entryName(menu.menuName) .sourceLayer(3) .build())); } catch (RuntimeException ex) { log.warn("DMS print audit event construction failed", ex); } } private void recordPermissionEvent(String actionCode, String actionLabel, DmsFilePermissionEntity first, List> diffs, Map snapshot, Map extra, String reason, String recordTitle, List relatedSigns) { auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode(actionCode) .actionLabel(actionLabel) .bizModule(FOLDER_BIZ_MODULE) .bizType(PERMISSION_BIZ_TYPE) .bizCode(value(first.getResourceId())) .targetTable(resourceTable(first.getResourceType())) .targetId(value(first.getResourceId())) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .reason(reason) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(batchTitle(recordTitle, extra)) .entryType(FILE_ENTRY_TYPE) .entryId(PERMISSION_ENTRY_ID) .entryName(PERMISSION_ENTRY_NAME) .relatedSigns(relatedSigns) .sourceLayer(3) .build())); } private void recordExclusionEvent(String actionCode, String actionLabel, DmsAccessExclusionEntity first, List> diffs, Map snapshot, Map extra, String reason, String recordTitle, List relatedSigns) { auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(AuditConsts.TYPE_DATA_CHANGE) .actionCode(actionCode) .actionLabel(actionLabel) .bizModule(FOLDER_BIZ_MODULE) .bizType(PERMISSION_BIZ_TYPE) .bizCode(value(first.getResourceId())) .targetTable(resourceTable(first.getResourceType())) .targetId(value(first.getResourceId())) .fieldDiffs(JsonUtil.getObjectToString(diffs)) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .reason(reason) .extra(JsonUtil.getObjectToString(extra)) .recordTitle(batchTitle(recordTitle, extra)) .entryType(FILE_ENTRY_TYPE) .entryId(PERMISSION_ENTRY_ID) .entryName(PERMISSION_ENTRY_NAME) .relatedSigns(relatedSigns) .sourceLayer(3) .build())); } private static Map batchExtra(List> targets) { Map extra = new LinkedHashMap<>(); extra.put("schemaVersion", 1); extra.put("batch", targets.size() > 1); extra.put("batchSize", targets.size()); extra.put("affected", targets.size()); extra.put("affectedTargets", targets); List titles = new ArrayList<>(); for (Map target : targets) { Object title = target.get("recordTitle"); if (title != null) titles.add(String.valueOf(title)); } extra.put("recordSubTitles", titles); return extra; } private static String batchTitle(String title, Map extra) { int size = Integer.parseInt(String.valueOf(extra.get("batchSize"))); return size > 1 ? title + "(共 " + size + " 条)" : title; } private static List> permissionDiffs(DmsFilePermissionEntity before, DmsFilePermissionEntity after, DmsPermissionAuditDisplayResolver.DisplayValues displays, boolean scoped) { return auditDiffs(permissionData(before), permissionData(after), PERMISSION_FIELD_META, "dms_file_permissions", after == null ? value(before.getId()) : value(after.getId()), (field, raw, oldValue) -> permissionDisplay(field, raw, oldValue ? before : after, displays), scoped); } private static List> exclusionDiffs(DmsAccessExclusionEntity before, DmsAccessExclusionEntity after, DmsPermissionAuditDisplayResolver.DisplayValues displays, boolean scoped) { return auditDiffs(exclusionData(before), exclusionData(after), EXCLUSION_FIELD_META, "dms_access_exclusions", after == null ? value(before.getId()) : value(after.getId()), (field, raw, oldValue) -> exclusionDisplay(field, raw, oldValue ? before : after, displays), scoped); } @SuppressWarnings("unchecked") private static List> auditDiffs(Map before, Map after, Map meta, String table, String id, DisplayLookup displayLookup, boolean scoped) { List parsed = JSON.parseArray(AuditFieldDiff.diff(before, after, Collections.emptySet(), meta), Map.class); List> result = new ArrayList<>(); for (Map item : parsed) { Map diff = new LinkedHashMap<>(); if (scoped) { diff.put("targetTable", table); diff.put("targetId", id); } diff.putAll(item); String field = String.valueOf(item.get("field")); String oldRaw = String.valueOf(item.get("oldData")); String newRaw = String.valueOf(item.get("newData")); diff.put("oldDisplay", displayLookup.display(field, oldRaw, true)); diff.put("newDisplay", displayLookup.display(field, newRaw, false)); result.add(diff); } return result; } private static String permissionDisplay(String field, String raw, DmsFilePermissionEntity row, DmsPermissionAuditDisplayResolver.DisplayValues displays) { if (raw == null || raw.isEmpty() || "null".equals(raw)) return ""; if (row != null && "resource_id".equals(field)) return displays.resource(row.getResourceType(), row.getResourceId()); if (row != null && "principal_id".equals(field)) return displays.principal(row.getPrincipalType(), row.getPrincipalId()); if (row != null && ("granted_by_id".equals(field) || "revoked_by_id".equals(field))) return displays.user(raw); return optionDisplay(PERMISSION_FIELD_META.get(field), raw); } private static String exclusionDisplay(String field, String raw, DmsAccessExclusionEntity row, DmsPermissionAuditDisplayResolver.DisplayValues displays) { if (raw == null || raw.isEmpty() || "null".equals(raw)) return ""; if (row != null && "resource_id".equals(field)) return displays.resource(row.getResourceType(), row.getResourceId()); if ("user_id".equals(field) || "excluded_by_id".equals(field) || "revoked_by_id".equals(field)) { return displays.user(raw); } return optionDisplay(EXCLUSION_FIELD_META.get(field), raw); } private static String optionDisplay(AuditFieldDiff.FieldMeta meta, String raw) { String display = meta == null ? null : meta.optionDisplay(raw); return display == null ? raw : display; } private static Map permissionTarget(DmsFilePermissionEntity value, List> changes, DmsPermissionAuditDisplayResolver.DisplayValues displays) { Map target = permissionSnapshot(value, displays); target.put("changes", changes); target.put("recordTitle", permissionTitle(value, displays)); return target; } private static Map exclusionTarget(DmsAccessExclusionEntity value, List> changes, DmsPermissionAuditDisplayResolver.DisplayValues displays) { Map target = exclusionSnapshot(value, displays); target.put("changes", changes); target.put("recordTitle", exclusionTitle(value, displays)); return target; } private static Map permissionSnapshot(DmsFilePermissionEntity value, DmsPermissionAuditDisplayResolver.DisplayValues displays) { Map data = new LinkedHashMap<>(); data.put("permissionId", value(value.getId())); data.put("resourceType", value.getResourceType()); data.put("resourceId", value(value.getResourceId())); data.put("resourceName", displays.resource(value.getResourceType(), value.getResourceId())); data.put("resourceScope", value.getResourceScope()); data.put("principalType", value.getPrincipalType()); data.put("principalId", value.getPrincipalId()); data.put("principalName", displays.principal(value.getPrincipalType(), value.getPrincipalId())); data.put("includeChildOrgs", value.getIncludeChildOrgs()); if (value.getApprovedOrgScope() != null) data.put("approvedOrgScope", value.getApprovedOrgScope()); data.put("actionCode", value.getActionCode()); data.put("actionName", optionDisplay(PERMISSION_FIELD_META.get("action_code"), value.getActionCode())); data.put("sourceType", value.getSourceType()); data.put("sourceId", value.getSourceId()); data.put("validFrom", value.getValidFrom()); data.put("validTo", value.getValidTo()); data.put("maxUseCount", value.getMaxUseCount()); data.put("usedCount", value.getUsedCount()); data.put("grantReason", value.getGrantReason()); data.put("grantedByType", value.getGrantedByType()); data.put("grantedById", value.getGrantedById()); data.put("grantedByName", displays.user(value.getGrantedById())); data.put("revokedAt", value.getRevokedAt()); data.put("revokedByType", value.getRevokedByType()); data.put("revokedById", value.getRevokedById()); data.put("revokedByName", displays.user(value.getRevokedById())); data.put("revokeReason", value.getRevokeReason()); data.put("revision", value.getRevision()); return data; } private static Map exclusionSnapshot(DmsAccessExclusionEntity value, DmsPermissionAuditDisplayResolver.DisplayValues displays) { Map data = new LinkedHashMap<>(); data.put("exclusionId", value(value.getId())); data.put("resourceType", value.getResourceType()); data.put("resourceId", value(value.getResourceId())); data.put("resourceName", displays.resource(value.getResourceType(), value.getResourceId())); data.put("userId", value.getUserId()); data.put("userName", displays.user(value.getUserId())); data.put("actionCode", value.getActionCode()); data.put("actionName", optionDisplay(EXCLUSION_FIELD_META.get("action_code"), value.getActionCode())); data.put("sourceType", value.getSourceType()); data.put("sourceId", value.getSourceId()); data.put("validFrom", value.getValidFrom()); data.put("validTo", value.getValidTo()); data.put("excludeReason", value.getExcludeReason()); data.put("excludedByType", value.getExcludedByType()); data.put("excludedById", value.getExcludedById()); data.put("excludedByName", displays.user(value.getExcludedById())); data.put("revokedAt", value.getRevokedAt()); data.put("revokedByType", value.getRevokedByType()); data.put("revokedById", value.getRevokedById()); data.put("revokedByName", displays.user(value.getRevokedById())); data.put("revokeReason", value.getRevokeReason()); data.put("revision", value.getRevision()); return data; } private static Map permissionData(DmsFilePermissionEntity value) { if (value == null) return null; Map data = new LinkedHashMap<>(); data.put("resource_type", value.getResourceType()); data.put("resource_id", value.getResourceId()); data.put("resource_scope", value.getResourceScope()); data.put("principal_type", value.getPrincipalType()); data.put("principal_id", value.getPrincipalId()); data.put("include_child_orgs", value.getIncludeChildOrgs()); if (value.getApprovedOrgScope() != null) data.put("approved_org_scope", value.getApprovedOrgScope()); data.put("action_code", value.getActionCode()); data.put("source_type", value.getSourceType()); data.put("source_id", value.getSourceId()); data.put("valid_from", value.getValidFrom()); data.put("valid_to", value.getValidTo()); data.put("max_use_count", value.getMaxUseCount()); data.put("used_count", value.getUsedCount()); data.put("grant_reason", value.getGrantReason()); data.put("granted_by_type", value.getGrantedByType()); data.put("granted_by_id", value.getGrantedById()); data.put("revoked_at", value.getRevokedAt()); data.put("revoked_by_type", value.getRevokedByType()); data.put("revoked_by_id", value.getRevokedById()); data.put("revoke_reason", value.getRevokeReason()); data.put("revision", value.getRevision()); return data; } private static Map exclusionData(DmsAccessExclusionEntity value) { if (value == null) return null; Map data = new LinkedHashMap<>(); data.put("resource_type", value.getResourceType()); data.put("resource_id", value.getResourceId()); data.put("user_id", value.getUserId()); data.put("action_code", value.getActionCode()); data.put("source_type", value.getSourceType()); data.put("source_id", value.getSourceId()); data.put("valid_from", value.getValidFrom()); data.put("valid_to", value.getValidTo()); data.put("exclude_reason", value.getExcludeReason()); data.put("excluded_by_type", value.getExcludedByType()); data.put("excluded_by_id", value.getExcludedById()); data.put("revoked_at", value.getRevokedAt()); data.put("revoked_by_type", value.getRevokedByType()); data.put("revoked_by_id", value.getRevokedById()); data.put("revoke_reason", value.getRevokeReason()); data.put("revision", value.getRevision()); return data; } private static String permissionTitle(DmsFilePermissionEntity value, DmsPermissionAuditDisplayResolver.DisplayValues displays) { return permissionTitle(value, displays, optionDisplay(PERMISSION_FIELD_META.get("action_code"), value.getActionCode())); } private static String permissionTitle(DmsFilePermissionEntity value, DmsPermissionAuditDisplayResolver.DisplayValues displays, String actionDisplay) { return displays.resource(value.getResourceType(), value.getResourceId()) + " - " + displays.principal(value.getPrincipalType(), value.getPrincipalId()) + " - " + actionDisplay; } private static PermissionActionSummary permissionActions(List values) { LinkedHashSet remaining = new LinkedHashSet<>(); for (DmsFilePermissionEntity item : values) { if (item != null && item.getActionCode() != null && !item.getActionCode().trim().isEmpty()) { remaining.add(item.getActionCode().trim()); } } List codes = new ArrayList<>(); for (String known : new String[]{"CREATE", "READ", "PRINT", "DOWNLOAD", "UPDATE", "DELETE"}) { if (remaining.remove(known)) codes.add(known); } codes.addAll(remaining); List names = new ArrayList<>(); for (String code : codes) names.add(optionDisplay(PERMISSION_FIELD_META.get("action_code"), code)); return new PermissionActionSummary(codes, names); } private static void combineActionDiff(List> diffs, PermissionActionSummary actions) { for (Map diff : diffs) { if (!"action_code".equals(diff.get("field"))) continue; diff.put("newData", actions.raw); diff.put("newDisplay", actions.display); return; } } private static void combineActionSnapshot(Map snapshot, PermissionActionSummary actions) { snapshot.put("actionCode", actions.raw); snapshot.put("actionName", actions.display); snapshot.put("actionCodes", actions.codes); snapshot.put("actionNames", actions.names); } private static String exclusionTitle(DmsAccessExclusionEntity value, DmsPermissionAuditDisplayResolver.DisplayValues displays) { return displays.resource(value.getResourceType(), value.getResourceId()) + " - " + displays.user(value.getUserId()) + " - 排除查看"; } private DmsPermissionAuditDisplayResolver.DisplayValues permissionDisplays( List values) { DmsPermissionAuditDisplayResolver resolver = permissionDisplayResolver == null ? null : permissionDisplayResolver.getIfAvailable(); return resolver == null ? new DmsPermissionAuditDisplayResolver.DisplayValues() : resolver.resolvePermissions(values.get(0).getTenantId(), values); } private DmsPermissionAuditDisplayResolver.DisplayValues exclusionDisplays( List values) { DmsPermissionAuditDisplayResolver resolver = permissionDisplayResolver == null ? null : permissionDisplayResolver.getIfAvailable(); return resolver == null ? new DmsPermissionAuditDisplayResolver.DisplayValues() : resolver.resolveExclusions(values.get(0).getTenantId(), values); } private static List retainedPermissions(List before, List after) { return after == null || after.isEmpty() ? orEmpty(before) : after; } private static List retainedExclusions(List before, List after) { return after == null || after.isEmpty() ? orEmpty(before) : after; } private static Map permissionsById(List values) { Map result = new HashMap<>(); for (DmsFilePermissionEntity value : orEmpty(values)) result.put(value.getId(), value); return result; } private static Map exclusionsById(List values) { Map result = new HashMap<>(); for (DmsAccessExclusionEntity value : orEmpty(values)) result.put(value.getId(), value); return result; } private static List orEmpty(List values) { return values == null ? Collections.emptyList() : values; } private static String resourceTable(String type) { if ("FOLDER".equals(type)) return FOLDER_TABLE; if ("DOCUMENT".equals(type)) return FILE_TABLE; if ("FILE_VERSION".equals(type)) return FILE_VERSION_TABLE; return "dms_permission_resources"; } private interface DisplayLookup { String display(String field, String raw, boolean oldValue); } private static final class PermissionActionSummary { private final List codes; private final List names; private final String raw; private final String display; private PermissionActionSummary(List codes, List names) { this.codes = codes; this.names = names; this.raw = String.join(",", codes); this.display = String.join("、", names); } } private static Map recycleDetailDiffData( DmsWenjianHuishouMingxiEntity detail) { Map data = new LinkedHashMap<>(); data.put("f_foreign_id", detail.getForeignId()); data.put("dayin_jilu_id", detail.getDayinJiluId()); data.put("dayin_liushuihao", detail.getDayinLiushuihao()); data.put("wenjian_bianhao", detail.getWenjianBianhao()); data.put("wenjian_mingcheng", detail.getWenjianMingcheng()); data.put("banbenhao", detail.getBanbenhao()); data.put("qianshou_bumen", detail.getQianshouBumen()); data.put("qianshou_ren", detail.getQianshouRen()); data.put("qianshou_shijian", detail.getQianshouShijian()); data.put("huishou_ren_id", detail.getHuishouRenId()); data.put("huishou_shijian", detail.getHuishouShijian()); return data; } private static Map recycleTarget(DmsWenjianHuishouMingxiEntity detail, String beforeStatus) { Map data = new LinkedHashMap<>(); data.put("detailId", detail.getId()); data.put("dayinJiluId", detail.getDayinJiluId()); data.put("dayinLiushuihao", detail.getDayinLiushuihao()); data.put("wenjianBianhao", detail.getWenjianBianhao()); data.put("wenjianMingcheng", detail.getWenjianMingcheng()); data.put("banbenhao", detail.getBanbenhao()); data.put("qianshouBumen", detail.getQianshouBumen()); data.put("qianshouRen", detail.getQianshouRen()); data.put("qianshouShijian", detail.getQianshouShijian()); data.put("oldStatus", beforeStatus); data.put("newStatus", "daixiaohui"); data.put("recordTitle", recycleDetailTitle(detail)); return data; } private static Map recycleSnapshot(DmsWenjianHuishouAuditEvent event, List> targets) { Map data = new LinkedHashMap<>(); data.put("huishouDanjuId", event.getHuishouDanjuId()); data.put("beizhuShuoming", event.getBeizhuShuoming()); data.put("huishouRenId", event.getHuishouRenId()); data.put("huishouShijian", event.getHuishouShijian()); data.put("huishouFenshu", targets.size()); data.put("huishouMingxi", targets); return data; } private static String recycleTitle(List details) { String first = recycleDetailTitle(details.get(0)); return details.size() > 1 ? first + "(共 " + details.size() + " 份)" : first; } private static String recycleDetailTitle(DmsWenjianHuishouMingxiEntity detail) { String document = "[" + detail.getWenjianBianhao() + "] " + detail.getWenjianMingcheng(); return document + " - " + detail.getBanbenhao() + " / " + detail.getDayinLiushuihao(); } private static Map printRecordDiffData(DmsWenjianDayinJiluEntity record) { Map data = new LinkedHashMap<>(); data.put("dayin_renwu_id", record.getDayinRenwuId()); data.put("dayin_xuhao", record.getDayinXuhao()); data.put("dayin_liushuihao", record.getDayinLiushuihao()); data.put("dayin_jishu_liushuihao", record.getDayinJishuLiushuihao()); data.put("dayin_ren_id", record.getDayinRenId()); data.put("dayin_bumen_id", record.getDayinBumenId()); data.put("dayin_shijian", record.getDayinShijian()); data.put("biz_status", record.getBizStatus()); return data; } private static Map distributionTaskDiffData( DmsWenjianFenfaRenwuEntity task) { Map data = new LinkedHashMap<>(); data.put("dayin_renwu_id", task.getDayinRenwuId()); data.put("fenfa_bianhao", task.getFenfaBianhao()); data.put("fafang_leixing", task.getFafangLeixing()); data.put("biz_status", task.getBizStatus()); return data; } private static Map printTarget(DmsWenjianDayinRenwuEntity task, List records) { Map data = new LinkedHashMap<>(); List recordIds = new ArrayList<>(records.size()); List serialNumbers = new ArrayList<>(records.size()); for (DmsWenjianDayinJiluEntity record : records) { recordIds.add(record.getId()); serialNumbers.add(record.getDayinLiushuihao()); } data.put("id", task.getId()); data.put("bizCode", task.getWenjianBianhao()); data.put("title", printTaskTitle(task)); data.put("wenjianId", task.getWenjianId()); data.put("version", task.getBanbenhao()); data.put("copies", records.size()); data.put("printRecordIds", recordIds); data.put("serialNumbers", serialNumbers); return data; } private static Map printSnapshot(List> targets, int totalCopies) { Map data = new LinkedHashMap<>(); data.put("printTaskCount", targets.size()); data.put("totalCopies", totalCopies); data.put("printTargets", targets); return data; } private static String printTitle(List tasks, int totalCopies) { String first = printTaskTitle(tasks.get(0)); if (tasks.size() > 1) { return first + "(批量打印 " + tasks.size() + " 个任务,共 " + totalCopies + " 份)"; } return first + "(打印 " + totalCopies + " 份)"; } private static String printTaskTitle(DmsWenjianDayinRenwuEntity task) { return "[" + task.getWenjianBianhao() + "] " + task.getWenjianMingcheng() + " - " + task.getBanbenhao(); } private PrintMenuContext resolvePrintMenu() { String menuId; try { HttpServletRequest request = ServletUtil.getRequest(); if (request == null) return PrintMenuContext.missing(); menuId = textOrNull(request.getHeader("Jnpf-Menu-Id")); if (menuId != null && menuId.length() > 100) { return PrintMenuContext.invalid(); } } catch (Throwable ex) { log.warn("DMS print audit cannot read menu header", ex); return PrintMenuContext.invalid(); } if (menuId == null) return PrintMenuContext.missing(); long now = System.currentTimeMillis(); MenuCacheEntry cached = menuCache.get(menuId); if (cached != null && cached.expiresAt > now) { return PrintMenuContext.resolved(menuId, cached.menuName); } if (cached != null) menuCache.remove(menuId, cached); ModuleApi resolver = moduleApi == null ? null : moduleApi.getIfAvailable(); if (resolver == null) return PrintMenuContext.failed(menuId); try { ModuleEntity menu = resolver.getModuleByList(menuId); String menuName = menu == null || Integer.valueOf(1).equals(menu.getDeleteMark()) ? null : textOrNull(menu.getFullName()); if (menuName == null) return PrintMenuContext.unresolved(menuId); menuCache.put(menuId, new MenuCacheEntry(menuName, now + MENU_CACHE_MILLIS)); return PrintMenuContext.resolved(menuId, menuName); } catch (Throwable ex) { log.warn("DMS print audit cannot resolve menu menuId={}", menuId, ex); return PrintMenuContext.failed(menuId); } } private static String textOrNull(String value) { if (value == null) return null; String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } private static boolean blank(String value) { return value == null || value.trim().isEmpty(); } private static final class PrintMenuContext { private final String menuId; private final String menuName; private final String resolution; private PrintMenuContext(String menuId, String menuName, String resolution) { this.menuId = menuId; this.menuName = menuName; this.resolution = resolution; } private static PrintMenuContext resolved(String menuId, String menuName) { return new PrintMenuContext(menuId, menuName, "RESOLVED"); } private static PrintMenuContext unresolved(String menuId) { return new PrintMenuContext(menuId, null, "UNRESOLVED"); } private static PrintMenuContext failed(String menuId) { return new PrintMenuContext(menuId, null, "FAILED"); } private static PrintMenuContext missing() { return new PrintMenuContext(null, null, "MISSING"); } private static PrintMenuContext invalid() { return new PrintMenuContext(null, null, "INVALID"); } } private static final class MenuCacheEntry { private final String menuName; private final long expiresAt; private MenuCacheEntry(String menuName, long expiresAt) { this.menuName = menuName; this.expiresAt = expiresAt; } } private void record(String eventType, String actionCode, String actionLabel, String table, String id, String reason, String extra) { auditClient.ifAvailable(client -> client.record(AuditEventDTO.builder() .eventType(eventType) .actionCode(actionCode) .actionLabel(actionLabel) .bizModule("DMS_PERMISSION") .bizType("DMS_PERMISSION") .targetTable(table) .targetId(id) .reason(reason) .extra(extra) .sourceLayer(3) .build())); } private static Map diffData(DmsFolderAuditSnapshot snapshot) { if (snapshot == null) { return null; } Map data = new LinkedHashMap<>(); data.put("name", snapshot.getName()); data.put("parent_id", value(snapshot.getParentId())); return data; } @SuppressWarnings("unchecked") private static void addScopedDiffs(List> combined, String table, String id, Map before, Map after) { addScopedDiffs(combined, table, id, before, after, FILE_FIELD_META); } @SuppressWarnings("unchecked") private static void addScopedDiffs(List> combined, String table, String id, Map before, Map after, Map fieldMeta) { List items = JSON.parseArray(AuditFieldDiff.diff(before, after, Collections.emptySet(), fieldMeta), Map.class); for (Map item : items) { Map scoped = new LinkedHashMap<>(); scoped.put("targetTable", table); scoped.put("targetId", id); scoped.putAll(item); combined.add(scoped); } } private static Map documentDiffData(DmsFileAuditSnapshot snapshot) { if (snapshot == null) return null; Map data = new LinkedHashMap<>(); data.put("document_no", snapshot.getDocumentNo()); data.put("name", snapshot.getDocumentName()); data.put("document_type", snapshot.getDocumentType()); data.put("responsible_dept_id", snapshot.getResponsibleDeptId()); data.put("folder_id", value(snapshot.getFolderId())); return data; } private static Map versionDiffData(DmsFileVersionAuditSnapshot snapshot) { if (snapshot == null) return null; Map data = new LinkedHashMap<>(); data.put("version_no", snapshot.getVersionNo()); data.put("version_label", snapshot.getVersionLabel()); data.put("file_name", snapshot.getFileName()); data.put("mime_type", snapshot.getMimeType()); data.put("size_bytes", snapshot.getSizeBytes()); data.put("checksum_sha256", snapshot.getChecksumSha256()); data.put("prepared_by", snapshot.getPreparedBy()); data.put("prepared_on", snapshot.getPreparedOn()); return data; } private static Map stateDiffData(DmsFileVersionAuditSnapshot snapshot) { if (snapshot == null) return null; Map data = new LinkedHashMap<>(); data.put("lifecycle_status", snapshot.getLifecycleStatus()); data.put("effective_from", snapshot.getEffectiveFrom()); data.put("effective_to", snapshot.getEffectiveTo()); data.put("status_reason", snapshot.getStatusReason()); return data; } private static Map fileSnapshotData(DmsFileAuditSnapshot snapshot) { if (snapshot == null) return Collections.emptyMap(); Map data = new LinkedHashMap<>(); data.put("documentId", value(snapshot.getDocumentId())); data.put("documentNo", snapshot.getDocumentNo()); data.put("documentName", snapshot.getDocumentName()); data.put("documentType", snapshot.getDocumentType()); data.put("folderId", value(snapshot.getFolderId())); data.put("folderPath", snapshot.getFolderPath()); data.put("responsibleDeptId", snapshot.getResponsibleDeptId()); DmsFileVersionAuditSnapshot version = snapshot.getCurrentVersion(); if (version != null) { Map current = new LinkedHashMap<>(); current.put("fileVersionId", value(version.getFileVersionId())); current.put("versionNo", version.getVersionNo()); current.put("versionLabel", version.getVersionLabel()); current.put("fileName", version.getFileName()); current.put("mimeType", version.getMimeType()); current.put("sizeBytes", version.getSizeBytes()); current.put("checksumSha256", version.getChecksumSha256()); current.put("preparedBy", version.getPreparedBy()); current.put("preparedOn", version.getPreparedOn()); current.put("lifecycleStatus", version.getLifecycleStatus()); current.put("effectiveFrom", version.getEffectiveFrom()); current.put("effectiveTo", version.getEffectiveTo()); current.put("statusReason", version.getStatusReason()); data.put("currentVersion", current); } return data; } private static String fileTitle(DmsFileAuditSnapshot before, DmsFileAuditSnapshot after, String fallback) { DmsFileAuditSnapshot retained = after == null ? before : after; if (retained == null) return fallback; String base = "[" + retained.getDocumentNo() + "] " + retained.getDocumentName(); DmsFileVersionAuditSnapshot oldVersion = before == null ? null : before.getCurrentVersion(); DmsFileVersionAuditSnapshot newVersion = after == null ? null : after.getCurrentVersion(); if (oldVersion != null && newVersion != null && !oldVersion.getFileVersionId().equals(newVersion.getFileVersionId())) { return base + ":" + oldVersion.getVersionLabel() + " -> " + newVersion.getVersionLabel(); } return newVersion == null ? base : base + " - " + newVersion.getVersionLabel(); } private static AuditFieldDiff.FieldMeta meta(String label) { return new AuditFieldDiff.FieldMeta(label, null, null); } private static AuditFieldDiff.FieldMeta booleanMeta(String label) { return options(label, "true", "是", "false", "否"); } private static AuditFieldDiff.FieldMeta actionMeta() { return options("权限动作", "CREATE", "新建", "READ", "查看", "PRINT", "打印", "DOWNLOAD", "下载", "UPDATE", "修改", "DELETE", "删除"); } private static AuditFieldDiff.FieldMeta sourceTypeMeta() { return options("授权来源", "MANUAL", "手工", "WORKFLOW", "工作流", "SYSTEM", "系统", "MIGRATION", "数据迁移"); } private static AuditFieldDiff.FieldMeta options(String label, String... values) { Map labels = new LinkedHashMap<>(); for (int i = 0; i + 1 < values.length; i += 2) labels.put(values[i], values[i + 1]); return new AuditFieldDiff.FieldMeta(label, null, null, "string", "select", null, labels); } private static AuditFieldDiff.FieldMeta lifecycleStatusMeta() { Map labels = new LinkedHashMap<>(); labels.put("PENDING_EFFECTIVE", "待生效"); labels.put("EFFECTIVE", "生效"); labels.put("SUPERSEDED", "已替代"); labels.put("EXPIRED", "已过期"); labels.put("REVOKED", "已撤销"); labels.put("INVALIDATED", "已失效"); labels.put("OBSOLETE", "已作废"); return new AuditFieldDiff.FieldMeta("生命周期状态", null, null, "string", null, null, labels); } private static Map snapshotData(DmsFolderAuditSnapshot snapshot) { if (snapshot == null) { return Collections.emptyMap(); } Map data = new LinkedHashMap<>(); data.put("id", value(snapshot.getId())); data.put("name", snapshot.getName()); data.put("parentId", value(snapshot.getParentId())); data.put("parentName", snapshot.getParentName()); data.put("folderPath", snapshot.getPath()); data.put("revision", snapshot.getRevision()); return data; } private static String title(DmsFolderAuditSnapshot before, DmsFolderAuditSnapshot after, String fallback) { DmsFolderAuditSnapshot snapshot = after == null ? before : after; if (snapshot == null) { return fallback; } if (snapshot.getPath() != null && !snapshot.getPath().trim().isEmpty()) { return snapshot.getPath(); } return snapshot.getName() == null ? fallback : snapshot.getName(); } private static String value(Object value) { return value == null ? null : String.valueOf(value); } }