通用审计日志(含增改字段新旧值比对,字段翻译为中文)

数据库设计

页面展示

①新增:

新增操作详情:

②编辑: 

 

编辑操作详情展示,编辑的字段(被编辑字段,高亮展示): 

部分核心代码

/**
 * 插入日志字段改变对比结果
 */
private AuditLog insertLogCompareResult(AuditLog auditLog, AuditLogIntercepts auditLogIntercept) {
    // 获取targetQueryBindParam
    String targetQueryBindParam = auditLogIntercept.getTargetQueryBindParam();
    if (StringUtils.isEmpty(targetQueryBindParam)) {
        new ArrayList<>();
    }

    // 获取logDataType --> 返回翻译字典:map集合(英文key,中文value)
    Map<String, String> transSourceMap = this.findTransSourceMap(auditLogIntercept);

    // 只有新增数据
    if (auditLogIntercept.getOperationAction().equals(OperateActionEnum.SAVE.getValue())) {
        // 英转中:字段翻译
        TreeMap<String, Object> trans = this.transLanguage(auditLog.getStandardLogData(), transSourceMap);
        // 组装返回对象
        ArrayList<LogCompareVO> list = new ArrayList<>();
        long i = 1L;
        for (String key : trans.keySet()) {
            if (Strings.isNotBlank(key)) {
                // 处理key,去掉同名key的序号
                LogCompareVO compareVO = new LogCompareVO(i, key.split("@")[0], "-", trans.get(key).toString(), "add");
                i++;
                list.add(compareVO);
            }
        }
        auditLog.setCompareResult(JSONObject.toJSONString(list));
        return BeanUtils.convertTo(auditLog, AuditLog::new);
    } else {
        // 查询最新的条1日志
        List<AuditLog> oldLog = this.queryLogDesc(auditLog.getTargetId(), auditLog.getTargetQueryBindParam(), 1);
        // 编辑情况:字段--新旧值对比
        List<AuditLog> auditLogs = new ArrayList<>(2);
        auditLogs.add(auditLog);
        auditLogs.add(oldLog.get(0));

        // 对比、翻译及去重
        return this.findDiff(auditLogs, transSourceMap);
    }
}
/**
 * 对比获取对象的字段差异
 */
private AuditLog findDiff(List<AuditLog> auditLogs, Map<String, String> map) {

    // 旧值
    String oldValue = auditLogs.get(1).getStandardLogData();

    // 新值
    String newValue = auditLogs.get(0).getOperateData();

    // 查询中文翻译并替换
    TreeMap<String, Object> transN = this.transLanguage(String.valueOf(newValue), map);
    TreeMap<String, Object> transO = this.transLanguage(String.valueOf(oldValue), map);

    // 编辑比对--产生改变的字段
    return this.compareMap(auditLogs, transN, transO);
}
/**
 * map比较编辑--产生改变的字段
 */
private AuditLog compareMap(List<AuditLog> auditLogs, TreeMap<String, Object> nMap, TreeMap<String, Object> oMap) {
    // 转对象用于返回数    组装返回对象LogCompareVO
    ArrayList<LogCompareVO> list = new ArrayList<>();
    long i = 1L;
    for (String key : nMap.keySet()) {
        if (Strings.isNotBlank(key)) {
            LogCompareVO compareVO = null;
            // value改变则存“change”标识,用于前端突出渲染
            if (oMap.get(key) != null) {
                if (oMap.get(key).equals(nMap.get(key))) {
                    // 比较相同则copy & 处理key,去掉同名key的序号
                    compareVO = new LogCompareVO(i, key.split("@")[0], oMap.get(key).toString(), nMap.get(key).toString(), "copy");
                } else {
                    // 比较不同则change & 处理key,去掉同名key的序号
                    compareVO = new LogCompareVO(i, key.split("@")[0], oMap.get(key).toString(), nMap.get(key).toString(), "change");
                }
            } else {
                // oldValue为空 & 处理key,去掉同名key的序号
                compareVO = new LogCompareVO(i, key.split("@")[0], "-", nMap.get(key).toString(), "change");
            }
            i++;
            list.add(compareVO);
        }
    }
    auditLogs.get(0).setCompareResult(JSONObject.toJSONString(list));
    return auditLogs.get(0);
}

/**
 * 获取字段中文翻译集合
 */
private Map<String, String> findTransSourceMap(AuditLogIntercepts auditLogIntercept) {
    String logDataType = auditLogIntercept.getLogDataType();
    String[] dataTypes = logDataType.split(";");
    HashMap<String, String> map = new HashMap<>();
    for (String type : dataTypes) {
        Field[] fields = LogDataTypeChoose.chooseEnumType(type);
        if (fields != null) {
            for (Field field : fields) {
                FieldCompareName annotation = field.getAnnotation(FieldCompareName.class);
                // 防止没加注解的字段报错,有加注解才进翻译集
                if (!Objects.isNull(annotation)) {
                    map.put(field.getName(), annotation.name());
                }
            }
        }
    }

    return map;
}

/**
 * 字段:英转中
 */
private TreeMap<String, Object> transLanguage(String s, Map<String, String> transSource) {
    Map<String, Object> sourceMap = new HashMap<>();
    TreeMap<String, Object> resultMap = new TreeMap<>(new SortStrUtil());
    // 展平多层级json为一层
    AuditUtil.parseJson2Map(sourceMap, s, null);
    // 逐级翻译字段名称
    int num = 1;
    for (String f : sourceMap.keySet()) {
        // 处理e,去掉AuditUtil.parseJson2Map加的同名key尾部序号@x
        String e = f.split("@")[0];
        // 一级key则直接转换保存,一级key不会有重名
        if (transSource.containsKey(e)) {
            resultMap.put(transSource.get(e), sourceMap.get(e));
        } else {
            // 多级key,先拆分key,拼接翻译,保存
            String[] keys = e.split("\\.");
            if (keys.length > 1) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < keys.length; i++) {
                    // 最后拼接的不拼”-“
                    String tr = transSource.get(keys[i]);
                    if (transSource.containsKey(keys[i]) && i == (keys.length - 1)) {
                        sb.append(tr);
                    } else {
                        sb.append(tr).append("-");
                    }
                }
                // 拼接有null字符串的,排除
                if (!sb.toString().contains("null")) {
                    // 如果没有key,直接保存进map
                    // 如果已经有key,则表示key有同名为数组,同名的加序号保存
                    if (resultMap.get(sb.toString()) == null) {
                        resultMap.put(sb.toString(), sourceMap.get(f));
                    } else {
                        String k = sb.toString() + "@" + sourceMap.get(f);
                        num++;
                        resultMap.put(k, sourceMap.get(f));
                    }
                }
            }
            // 没有翻译源的,即没配置翻译信息的,过滤掉,不呈现
        }
    }

    return resultMap;
}

说明
一、被审计接口需要遵循的规范
二、kafka的topic命名
三、日志记录过程说明
四、问题
五、配置项示例

****************************************************************************************************************************
一、被审计接口需要遵循的规范:
    新增接口需要返回id,用于targetId;批量新增需要返回新增成功的id数组,格式示例[1,x,x]

    json数据尽量符合key-value格式-->[x,x,x]不含K-V格式的数组无法进一步解析,直接呈现;

    Key值尽量为可以翻译的字段名,如1:“xx”将不会被翻译,因为1数字key没有翻译源

**************************************************************************************************************************
二、kafka的topic命名:
producer:
        router服务里配置yml:(最终效果为"模块名"+"下列配置",如:xxx.audit)
            开发环境:audit
            sit环境:audit.sit
            uat环境:audit.uat
consumer:
        各个服务里配置ice的yml:(最终效果为"模块名"+"下列配置",如:project.audit)
                    开发环境:"模块名称"+audit  (配置多个topic以,分割)
                    sit环境:"模块名称"+audit.sit
                    uat环境:"模块名称"+audit.uat

动态配置多个topics-->kafka的lietener类,配置topic格式:@KafkaListener(topics = "#{'${kafka.listener_topics}'.split(',')}")
                                yml配置格式(逗号分割topic):listener_topics: kafka-topic-a2b,kafka-topic-c2b

**************************************************************************************************************************
三、日志记录过程说明:

①router截取请求和响应信息,通过kafka发送出来,在各个模块分别接收日志数据,处理数据:基础数据和操作详情。

     配置一处:ModuleFilterUtil 的模块名(白名单),否则kafka发送不了对应模块的消息

②操作详情即对新增和编辑的字段进行展示,此处字段名称需要翻译为中文。
     翻译:在字段上添加翻译注解FieldCompareName,在配置文件AUDIT_CONFIG.yml中配置对象类型如logDataType:TaskDTO,通过枚举LogDataTypeEnum配置解读出对象类型传递到翻译处理类LogDataTypeChoose,通过反射获取翻译信息存放到一个map中(key为英文字段名,value为中文字段名);
     请求信息为多层嵌套的json格式,通过工具将其转化为一层的map,根据map的key与翻译的map的key相同,将value替换为中文翻译。拼装每一个返回对象LogCompareVO,放到集合中返回给前端。

     配置四处:FieldCompareName、AUDIT_CONFIG.yml、LogDataTypeEnum、LogDataTypeChoose

****************************************************************************************************************************
四、问题:
    targetId取请求或者返回数据中的id

    优化日志内容冗余::筛选掉不该展现的值-->没配置注解的,不翻译也不放进比较结果里,翻译过程种中出现null值的key,将被过滤掉,因此没配置翻译的注解,就会被过滤掉

    日志ip问题:nginx有代理,所以要处理下拿到真实ip,再router里进行相关操作

    topic:①kafka:topic模块分发配置;②日志功能封装和按模块拆分;
    动态配置多个topics--》lietener配置topic格式:@KafkaListener(topics = "#{'${kafka.listener_topics}'.split(',')}")
                                yml配置格式(逗号分割topic):listener_topics: kafka-topic-a2b,kafka-topic-c2b

    删除操作返回对象名称如xx任务,配置 targetName: taskName ,进行了 删除 操作-->删除描述统一为?,新增和便编辑类似,后续替换该内容为具体targetName

    新增请求没有id-->补id:新增操作的日志logdata没有“id”:xx,需要从新增成功后返回的response里拿到id,插入到logdata里

    操作详情:增加标识,对比修改的放修改,前端高亮显示。传给前端参数,带change的显示高亮,其他add、copy不传

    logDataType: ProjectDTO--》配置操作的数据对象类型,用于翻译------》logData问题--比较后回填失败:待解决---》修改op的remove,改源码使之失效
    ①处理日志logdata的更细问题---》修改json2map方法源码,每次logdata和编辑的json数据比对,将更新的部分更新,其他的部分保存,这里会产生remove,去掉logdata的部分数据,解决方法,修改jsonpatch源码,不进行remove补丁操作

    ②处理入参出现value值为字符串数组导致出错的问题;---》非结构化多层数据转单层结构

    批量新增的问题,无法记录id,无法生成日志记录。----返回的id组也拆开;………req和respon都拆开来,分开多次进行日志审计
    处理请求数据格式,包括-->[1,2,3]格式和[{xx},{xx}]格式及{”k“:["a","b","c"],}格式

    message的尾部名称获取解决--》targetName配置,从req或resp里根据配置的字段名称作为jsonObject的key,从而拿到名称


***********************************************************************************************************
五、配置项示例:

①# router服务里配置module名,不在该集合内的module将被过滤掉,无法发送kafka消息。示例--> url: /project/TaskService/save里的project
public class ModuleFilterUtil {
    private static final String[] MODULES = {
            "contract",
            "budget",
            "contractbill",
            "contractestimate",
            "core",
            "finance",
            "market",
            "customer",
            "project",
            "subcontract",
    };
}

②# AUDIT_CONFIG.yml配置文件示例
    audit:
      intercepts:
          # 被审计接口的api,查询接口不产生审计日志
        - url: /project/TaskService/save
          # 请求方式
          method: POST
          # 模块名称
          moduleCode: project
          moduleName: 项目管理
          # 用于产生targetId(动态配置,取请求或者返回的id其一),一般取request里的id,新增入参没有id才取response里的id,注意有些编辑和删除接口,不是用id而是如taskId
          requestKey: id
          responseKey: id
          # 操作类型(新增、编辑、禁用、启用、删除、审核、导入、导出)
          operationAction: 新增
          # 配合targetId,绑定新增、编辑和删除的查询参数,配合targetId用于查询同一个操作对象的日志记录对象;命名规则-->同一url: /project/TaskService/XXX下,新增、编辑和删除相同,建议取Url里的Service的名字,如/project/TaskService/save取Task
          targetQueryBindParam: Task
          # 日志记录--对象类型(入参包含的对象,配置多个时以;间隔)//没配为特殊新增如转级,操作详情不记录,用于新增和编辑的操作详情的key字段翻译
          logDataType: TaskDTO;ProjectDTO
          # 日志记录--对象名称-->需要确认字段名称,用于产生message里的被操作日志对象名称, 如message-->项目管理,邱晓东,删除“成员”:张三--<的张三
          targetName: projectName
          # 操作信息-->详见下面的message格式
          message: 项目管理,${USER_NAME},查询项目列表

     message格式-->      项目管理,xxx,删除“成员”:张三
                        项目管理,xxx,编辑“成员”:张三
                        项目管理,xxx,新增“任务”:发电设备安全检查
                        项目管理,xxx,删除“项目”:项目二期
                        项目合同,xxx,删除“分包合同”:项目执行部门合同


③FieldCompareName配置示例
    ProjectDTO extends OperationDTO {
        @FieldCompareName(name = "项目编号")
        private String projectNo;...}

④LogDataTypeEnum配置示例
    public enum LogDataTypeEnum {
        /**
         * 任务对象
         */
        TASK("TaskDTO"),
        /**
         * 项目对象
         */
        PROJECT("ProjectDTO")...}

⑤LogDataTypeChoose配置示例
    public class LogDataTypeChoose {
        public static Field[] chooseEnumType(String enumType) {
            return switch (Objects.requireNonNull(LogDataTypeEnum.fromValue(enumType))) {
                case TASK -> TaskDTO.class.getDeclaredFields();
                case PROJECT -> ProjectDTO.class.getDeclaredFields();...}

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

花落竹思

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值