基于Mybatis-Plus的数据库操作日志方案

摘要:本文主要介绍了如何使用Mybatis-Plus数据变动记录插件 来记录我们的业务操作日志,下文主要改造了DataChangeRecorderInnerInterceptor拦截器,插入了我们业务实际的一些操作,可以让我们更加方便的使用,项目采用SpringBoot + Mybatis-Plus

简介

官方的DataChangeRecorderInnerInterceptor插件会拦截每一条insert/update/delete语句,而我们的业务中,我们只需要关注我们需要拦截的业务,所以在该插件的基础上定制了实际的业务场景代码,并且输出的变更语句对象更加容易后期扩展。

  • 只针对单表的insert/update/delete拦截,复杂sql不支持。
  • update/delete操作如果主键不存在,可能会导致性能损耗严重。
  • 批量插入、更新数据不支持,需要改为for循环执行。

详细代码

测试数据库表

定义了两张表用于测试业务

sql 复制代码
CREATE TABLE `userinfo` (
  `id` bigint(20) NOT NULL,
  `code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `money` decimal(15,4) DEFAULT NULL,
  `create_time` datetime DEFAULT NULL,
  `update_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE `sub` (
  `id` bigint(20) NOT NULL,
  `parent_id` bigint(11) DEFAULT NULL,
  `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  `num` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

操作日志数据库表

如果是单系统,可以放在一起,但是最好通过MQ解耦,操作日志单独在一个系统中,本案例是放在一起的,用于简单演示。

sql 复制代码
CREATE TABLE `operate_log` (
  `id` bigint(20) NOT NULL,
  `trace_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '业务追踪ID',
  `domain` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '归属领域',
  `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作类型',
  `table_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表名',
  `table_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作表描述',
  `data_id` bigint(20) DEFAULT NULL COMMENT '数据id',
  `db_operation` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '数据库操作类型',
  `cost` bigint(20) DEFAULT NULL COMMENT '本次拦截时间消耗(ms)',
  `generate_time` datetime(3) DEFAULT NULL COMMENT '拦截的数据生成时间',
  `create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志';

CREATE TABLE `operate_log_item` (
  `id` bigint(20) NOT NULL,
  `operate_log_id` bigint(20) DEFAULT NULL COMMENT '操作日志ID',
  `column_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列名称',
  `column_desc` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '列描述',
  `original_value` text COLLATE utf8mb4_unicode_ci COMMENT '原始值',
  `update_value` text COLLATE utf8mb4_unicode_ci COMMENT '更新后的值',
  PRIMARY KEY (`id`),
  KEY `idx_oli` (`operate_log_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

基础Entity/Mapper相关

UserinfoEntity

less 复制代码
@ApiModel(value = "用户信息")
@Data
@TableName("userinfo")
public class UserinfoEntity {

    @ApiModelProperty(value = "id")
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "编码")
    private String code;

    @ApiModelProperty(value = "名称")
    private String name;

    private BigDecimal money;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

}

SubEntity

kotlin 复制代码
@Data
@TableName("sub")
public class SubEntity {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;

    @ApiModelProperty(value = "父id")
    private Long parentId;

    @ApiModelProperty(value = "名称")
    private String name;

    @ApiModelProperty(value = "数量")
    private Integer num;
}

OperateLogEntity

less 复制代码
@OperateLogIgnore
@ApiModel(value = "操作日志表")
@Data
@TableName("operate_log")
public class OperateLogEntity {

    /** 数据ID */
    private Long id;
    /** 追踪ID */
    private String traceId;
    /** 归属领域 */
    private String domain;
    /** 操作类型 */
    private String type;
    /** 表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 数据ID */
    private String dataId;
    /** 数据库操作类型 */
    private String dbOperation;
    /** 花费时间毫秒 */
    private Long cost;
    /** 数据生成的时间 */
    private Date generateTime;
    /** 创建时间 */
    private Date createTime;

}

OperateLogItemEntity

less 复制代码
@OperateLogIgnore
@Data
@TableName("operate_log_item")
public class OperateLogItemEntity {

    private Long id;

    private Long operateLogId;

    /** 数据库列字段 */
    private String columnName;
    /** 字段描述 */
    private String columnDesc;
    /** 原始值 */
    private String originalValue;
    /** 更新后的值 */
    private String updateValue;

}

UserinfoMapper

less 复制代码
@Repository
public interface UserinfoMapper extends BaseMapper<UserinfoEntity> {

    @Update("update userinfo set code = #{code} where id = #{id}")
    void complexUpdate(@Param("code") String code,@Param("id")  Long id);

    @Update("update userinfo,sub set userinfo.money = sub.num where userinfo.id = sub.parent_id and userinfo.id = #{id}")
    void complexUpdate2(@Param("id")  Long id);
}

SubMapper

java 复制代码
@Repository
public interface SubMapper extends BaseMapper<SubEntity> {
}

OperateLogMapper

java 复制代码
@Repository
public interface OperateLogMapper extends BaseMapper<OperateLogEntity> {
}

OperateLogItemMapper

java 复制代码
@Repository
public interface OperateLogItemMapper extends BaseMapper<OperateLogItemEntity> {
}

定义注解

OperateLog操作日志注解

只有Controller上的方法加了该注解才记录后面相关的操作日志。

less 复制代码
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {

    /**
     * 操作类型
     * Alias for the type();
     * @return
     */
    String value();

    /**
     * 业务领域
     * @return
     */
    String domain() default "";

    /**
     * 操作类型
     * @return
     */
    String type() default "";
}

OperateLogIgnore忽略记录的表注解

加了该注解后,就不会存储该表数据变动的日志。

less 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLogIgnore {
}

dto定义

用于该业务上下文的dto

OperateLogContextDto 业务上下文DTO

dto配合@OperateLog注解使用的,有了该注解,上下文才会存储这个对象。

arduino 复制代码
@Data
public class OperateLogContextDto {

    /** 调用链路ID */
    private String traceId;

    /** 归属领域 */
    private String domain;

    /** 操作类型 */
    private String type;

}

TableInfoDto 表结构信息

该类存储了我们定义的Entity对象的表描述和字段描述信息

  • 表名描述用@ApiModel(value = "用户信息")
  • 属性描述用@ApiModelProperty(value = "编码")

这样就能把实体对应的描述存储起来。

arduino 复制代码
@Data
public class TableInfoDto {

    /** 表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 是否忽略表 */
    private Boolean ignore = Boolean.FALSE;

    /**
     * 属性map
     * key=表的属性名称  value=属性描述
     */
    private Map<String, String> columnMap = new LinkedHashMap<>();

}

数据变动相关DTO

DataChangeColumnResult列变动记录

记录了列变动的先后值

arduino 复制代码
@Data
public class DataChangeColumnResult {
    /** 数据库列字段 */
    private String columnName;
    /** 字段描述 */
    private String columnDesc;
    /** 原始值 */
    private String originalValue;
    /** 更新后的值 */
    private String updateValue;
}
DataChangeResult数据行变动记录

记录一行数据的变动情况

ruby 复制代码
@Data
public class DataChangeResult {
    /** id */
    private String id;
    /** 其他的列变动情况 */
    private List<DataChangeColumnResult> changeColumnResults;
}
OperateLogResult 每一次数据库操作影响的数据行和操作集合
arduino 复制代码
@Data
public class OperateLogResult {

    /** 上下文信息 */
    private OperateLogContextDto context;

    /** 数据库操作类型 */
    private String operation;
    /** 数据记录状态 */
    private boolean recordStatus;
    /** 操作的表名称 */
    private String tableName;
    /** 表描述 */
    private String tableDesc;
    /** 数据变更记录 */
    private List<DataChangeResult> dataChangeResults = new ArrayList<>();
    /**
     * cost for this plugin, ms
     */
    private long cost;
    /** 数据构建的时间 */
    private long generateTime = System.currentTimeMillis();

}

OperateLogContextHolder上下文信息持有者

controller的方法上有@OperateLog注解,则会通过拦截器放入操作信息到该上下文中。

csharp 复制代码
public class OperateLogContextHolder {

    private static final ThreadLocal<OperateLogContextDto> operateTraceDtoContext = new ThreadLocal<>();

    /**
     * 设置上下文
     * @param operateLogContextDto
     */
    public static void set(OperateLogContextDto operateLogContextDto){
        if(null != operateLogContextDto){
            operateTraceDtoContext.set(operateLogContextDto);
        }
    }

    /**
     * 获取上下文
     * @return
     */
    public static OperateLogContextDto get(){
        return operateTraceDtoContext.get();
    }

    /**
     * 清除上下文
     */
    public static void clear(){
        operateTraceDtoContext.remove();
    }

}

SpringEvent事件定义

本方案采用的是事件解耦

OperateLogTransactionEvent带事务的事件对象

scala 复制代码
@Data
public class OperateLogTransactionEvent extends ApplicationEvent {

    private static final long serialVersionUID = 4675816574446023168L;

    private OperateLogResult operateLogResult;

    public OperateLogTransactionEvent(Object source, OperateLogResult operateLogResult) {
        super(source);
        this.operateLogResult = operateLogResult;
    }
}

OperateLogNormalEvent普通事件对象

scala 复制代码
@Data
public class OperateLogNormalEvent extends ApplicationEvent {

    private static final long serialVersionUID = 2795119755419728981L;
    private OperateLogResult operateLogResult;

    public OperateLogNormalEvent(Object source, OperateLogResult operateLogResult) {
        super(source);
        this.operateLogResult = operateLogResult;
    }
}

事件监听者

用来消费事件消息,做操作日志最后的存储

less 复制代码
@Slf4j
@Component
public class OperateLogEventListener {

    @Autowired
    private OperateLogMapper operateLogMapper;
    @Autowired
    private OperateLogItemMapper operateLogItemMapper;

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void transaction(OperateLogTransactionEvent operateLogTransactionEvent) {
        // 在这里编写你想在事务提交后执行的代码
        log.info(JSON.toJSONString(operateLogTransactionEvent));
        this.saveLog(operateLogTransactionEvent.getOperateLogResult());
    }

    @EventListener
    public void normal(OperateLogNormalEvent operateLogNormalEvent){
        // 在这里编写你想在事务提交后执行的代码
        log.info(JSON.toJSONString(operateLogNormalEvent));
        this.saveLog(operateLogNormalEvent.getOperateLogResult());
    }

    private void saveLog(OperateLogResult operateLogResult){
        for (DataChangeResult dataChangeResult : operateLogResult.getDataChangeResults()) {
            OperateLogEntity operateLogEntity = new OperateLogEntity();
            operateLogEntity.setId(IdWorker.getId());
            operateLogEntity.setTraceId(operateLogResult.getContext().getTraceId());
            operateLogEntity.setDomain(operateLogResult.getContext().getDomain());
            operateLogEntity.setType(operateLogResult.getContext().getType());
            operateLogEntity.setTableName(operateLogResult.getTableName());
            operateLogEntity.setTableDesc(operateLogResult.getTableDesc());
            operateLogEntity.setDbOperation(operateLogResult.getOperation());
            operateLogEntity.setCost(operateLogResult.getCost());
            operateLogEntity.setGenerateTime(new Date(operateLogResult.getGenerateTime()));
            operateLogEntity.setDataId(dataChangeResult.getId());
            operateLogEntity.setCreateTime(new Date());
            operateLogMapper.insert(operateLogEntity);
            for (DataChangeColumnResult changeColumnResult : dataChangeResult.getChangeColumnResults()) {
                OperateLogItemEntity operateLogItemEntity = new OperateLogItemEntity();
                operateLogItemEntity.setId(IdWorker.getId());
                operateLogItemEntity.setOperateLogId(operateLogEntity.getId());
                operateLogItemEntity.setColumnName(changeColumnResult.getColumnName());
                operateLogItemEntity.setColumnDesc(changeColumnResult.getColumnDesc());
                operateLogItemEntity.setOriginalValue(changeColumnResult.getOriginalValue());
                operateLogItemEntity.setUpdateValue(changeColumnResult.getUpdateValue());
                operateLogItemMapper.insert(operateLogItemEntity);
            }
        }
    }
}

mvc拦截器配置

OperateLogWebInterceptor

判断controller的方法上是否有@OperateLog注解,然后存入上下文中

java 复制代码
public class OperateLogWebInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(handler instanceof HandlerMethod){
            Method method = ((HandlerMethod) handler).getMethod();
            OperateLog operateLog = method.getAnnotation(OperateLog.class);
            String operateType = this.buildOperateType(operateLog);
            if(StrUtil.isNotBlank(operateType)){
                OperateLogContextDto operateLogContextDto = new OperateLogContextDto();
                operateLogContextDto.setTraceId(UUID.randomUUID().toString());
                operateLogContextDto.setDomain(operateLog.domain());
                operateLogContextDto.setType(operateType);
                OperateLogContextHolder.set(operateLogContextDto);
            }
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        OperateLogContextHolder.clear();
    }

    /**
     * 构建操作类型
     * @param operateLog
     * @return
     */
    private String buildOperateType(OperateLog operateLog){
        if(null == operateLog){
            return null;
        }
        if(StrUtil.isNotBlank(operateLog.value())){
            return operateLog.value();
        }
        return operateLog.type();
    }
}

OperateLogMvcConfig

配置拦截器在SpringBoot中生效

typescript 复制代码
@Configuration
public class OperateLogMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new OperateLogWebInterceptor());
    }
}

Mybatis-Plus数据拦截相关

OperateLogTableInfoHelper

用来存储表的结构信息工具类

scss 复制代码
@Slf4j
public class OperateLogTableInfoHelper {

    private static final Map<String, TableInfoDto> TABLE_INFO_DTO_MAP = new ConcurrentHashMap<>();

    /**
     * 获取表结构信息
     * @param tableName
     * @return
     */
    public static TableInfoDto getTableInfoDto(String tableName){
        if(!TABLE_INFO_DTO_MAP.containsKey(tableName)){
            synchronized (tableName.intern()){
                initTableInfoDto(tableName);
            }
        }
        return TABLE_INFO_DTO_MAP.get(tableName);
    }

    /**
     * 初始化表信息
     * @param tableName
     */
    private static void initTableInfoDto(String tableName){
        if(TABLE_INFO_DTO_MAP.containsKey(tableName)){
            return;
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
        if(null == tableInfo){
            log.error("数据库表不存在,tableName={}",tableName);
            return;
        }
        TableInfoDto tableInfoDto = new TableInfoDto();
        Map<String, String> columnMap = new LinkedHashMap<>();
        tableInfoDto.setTableName(tableName);
        tableInfoDto.setColumnMap(columnMap);
        ApiModel apiModel = tableInfo.getEntityType().getAnnotation(ApiModel.class);
        if(null != apiModel){
            tableInfoDto.setTableDesc(apiModel.value());
        }
        OperateLogIgnore operateLogIgnore = tableInfo.getEntityType().getAnnotation(OperateLogIgnore.class);
        if(null != operateLogIgnore){
            tableInfoDto.setIgnore(true);
        }
        for (TableFieldInfo tableFieldInfo : tableInfo.getFieldList()) {
            Field field = ReflectUtil.getField(tableInfo.getEntityType(), tableFieldInfo.getProperty());
            if(null == field){
                continue;
            }
            ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
            String propertyName = null;
            if(null != apiModelProperty){
                propertyName = apiModelProperty.value();
            }
            columnMap.put(tableFieldInfo.getColumn(), propertyName);
        }
        TABLE_INFO_DTO_MAP.put(tableName, tableInfoDto);
    }

}

OperateLogMybatisInnerInterceptor数据拦截插件

主题内容来源于DataChangeRecorderInnerInterceptor插件,我在该基础上加上了业务操作

scss 复制代码
public class OperateLogMybatisInnerInterceptor implements InnerInterceptor {

    protected final Logger logger = LoggerFactory.getLogger(this.getClass());
    @SuppressWarnings("unused")
    public static final String IGNORED_TABLE_COLUMN_PROPERTIES = "ignoredTableColumns";

    private final Map<String, Set<String>> ignoredTableColumns = new ConcurrentHashMap<>();
    private final Set<String> ignoreAllColumns = new HashSet<>();//全部表的这些字段名,INSERT/UPDATE都忽略,delete暂时保留
    //批量更新上限, 默认一次最多1000条
    private int BATCH_UPDATE_LIMIT = 1000;
    private boolean batchUpdateLimitationOpened = false;
    private final Map<String, Integer> BATCH_UPDATE_LIMIT_MAP = new ConcurrentHashMap<>();//表名->批量更新上限

    private ApplicationEventPublisher publisher;

    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        if(null == OperateLogContextHolder.get()){
            return;
        }
        PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpSh.mappedStatement();
        final BoundSql boundSql = mpSh.boundSql();
        SqlCommandType sct = ms.getSqlCommandType();
        if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) {
            PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
            OperateLogResult operateLogResult;
            long startTs = System.currentTimeMillis();
            try {
                Statement statement = JsqlParserGlobal.parse(mpBs.sql());
                if (statement instanceof Insert) {
                    operateLogResult = processInsert((Insert) statement, mpSh.boundSql());
                } else if (statement instanceof Update) {
                    operateLogResult = processUpdate((Update) statement, ms, boundSql, connection);
                } else if (statement instanceof Delete) {
                    operateLogResult = processDelete((Delete) statement, ms, boundSql, connection);
                } else {
                    logger.info("other operation sql={}", mpBs.sql());
                    return;
                }
            } catch (Exception e) {
                if (e instanceof DataUpdateLimitationException) {
                    throw (DataUpdateLimitationException) e;
                }
                logger.error("Unexpected error for mappedStatement={}, sql={}", ms.getId(), mpBs.sql(), e);
                return;
            }
            long costThis = System.currentTimeMillis() - startTs;
            if (operateLogResult != null) {
                operateLogResult.setCost(costThis);
                dealOperationResult(operateLogResult);
            }
        }
    }

    /**
     * 判断哪些SQL需要处理
     * 默认INSERT/UPDATE/DELETE语句
     *
     * @param sql
     * @return
     */
    protected boolean allowProcess(String sql) {
        String sqlTrim = sql.trim().toUpperCase();
        return sqlTrim.startsWith("INSERT") || sqlTrim.startsWith("UPDATE") || sqlTrim.startsWith("DELETE");
    }

    /**
     * 处理数据更新结果,默认打印
     *
     * @param operateLogResult
     */
    protected void dealOperationResult(OperateLogResult operateLogResult) {
        if(null == operateLogResult){
            return;
        }
        this.fillOperationResult(operateLogResult);
        if (TransactionSynchronizationManager.isActualTransactionActive()){
            publisher.publishEvent(new OperateLogTransactionEvent(this, operateLogResult));
        }else{
            publisher.publishEvent(new OperateLogNormalEvent(this, operateLogResult));
        }
    }

    /**
     * 填充其他数据
     * @param operateLogResult
     */
    private void fillOperationResult(OperateLogResult operateLogResult){
        TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(operateLogResult.getTableName());
        if(null == tableInfoDto){
            return;
        }
        operateLogResult.setTableDesc(tableInfoDto.getTableDesc());
        operateLogResult.getDataChangeResults().forEach(dataChangeResult -> {
            dataChangeResult.getChangeColumnResults().forEach(dataChangeColumnResult -> {
                dataChangeColumnResult.setColumnDesc(tableInfoDto.getColumnMap().get(dataChangeColumnResult.getColumnName().toLowerCase()));
                if(dataChangeColumnResult.getColumnName().equalsIgnoreCase("id") && ObjectUtils.isEmpty(dataChangeResult.getId())){
                    dataChangeResult.setId(dataChangeColumnResult.getUpdateValue());
                }
            });
            dataChangeResult.getChangeColumnResults().removeIf(v -> v.getColumnName().equalsIgnoreCase("id"));
        });
        operateLogResult.setContext(OperateLogContextHolder.get());
    }

    public OperateLogResult processInsert(Insert insertStmt, BoundSql boundSql) {
        String operation = SqlCommandType.INSERT.name().toLowerCase();
        Table table = insertStmt.getTable();
        String tableName = table.getName();
        if (checkOperateLogIgnore(tableName)) {
            return null;
        }
        Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
        if (optionalOperationResult.isPresent()) {
            return optionalOperationResult.get();
        }
        OperateLogResult result = new OperateLogResult();
        result.setOperation(operation);
        result.setTableName(tableName);
        result.setRecordStatus(true);
        Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, insertStmt);
        result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), null, updatedColumnDatas)));
        return result;
    }

    /**
     * 检查是否是忽略的表
     * @param tableName
     * @return
     */
    private boolean checkOperateLogIgnore(String tableName) {
        TableInfoDto tableInfoDto = OperateLogTableInfoHelper.getTableInfoDto(tableName);
        if(null == tableInfoDto || tableInfoDto.getIgnore()){
            return true;
        }
        return false;
    }

    public OperateLogResult processUpdate(Update updateStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        Expression where = updateStmt.getWhere();
        PlainSelect selectBody = new PlainSelect();
        Table table = updateStmt.getTable();
        String tableName = table.getName();
        if (checkOperateLogIgnore(tableName)) {
            return null;
        }
        String operation = SqlCommandType.UPDATE.name().toLowerCase();
        Optional<OperateLogResult> optionalOperationResult = ignoredTableColumns(tableName, operation);
        if (optionalOperationResult.isPresent()) {
            return optionalOperationResult.get();
        }
        selectBody.setFromItem(table);
        List<Column> updateColumns = new ArrayList<>();
        for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
            updateColumns.addAll(updateSet.getColumns());
        }
        Columns2SelectItemsResult buildColumns2SelectItems = buildColumns2SelectItems(tableName, updateColumns);
        selectBody.setSelectItems(buildColumns2SelectItems.getSelectItems());
        selectBody.setWhere(where);
        SelectItem<PlainSelect> plainSelectSelectItem = new SelectItem<>(selectBody);

        BoundSql boundSql4Select = new BoundSql(mappedStatement.getConfiguration(), plainSelectSelectItem.toString(),
                prepareParameterMapping4Select(boundSql.getParameterMappings(), updateStmt),
                boundSql.getParameterObject());
        PluginUtils.MPBoundSql mpBoundSql = PluginUtils.mpBoundSql(boundSql);
        Map<String, Object> additionalParameters = mpBoundSql.additionalParameters();
        if (additionalParameters != null && !additionalParameters.isEmpty()) {
            for (Map.Entry<String, Object> ety : additionalParameters.entrySet()) {
                boundSql4Select.setAdditionalParameter(ety.getKey(), ety.getValue());
            }
        }
        Map<String, Object> updatedColumnDatas = getUpdatedColumnDatas(tableName, boundSql, updateStmt);
        OriginalDataObj originalData = buildOriginalObjectData(updatedColumnDatas, selectBody, buildColumns2SelectItems.getPk(), mappedStatement, boundSql4Select, connection);
        OperateLogResult result = new OperateLogResult();
        result.setOperation(operation);
        result.setTableName(tableName);
        result.setRecordStatus(true);
        result.setDataChangeResults(this.buildDataStr(compareAndGetUpdatedColumnDatas(result.getTableName(), originalData, updatedColumnDatas)));
        return result;
    }

    private Optional<OperateLogResult> ignoredTableColumns(String table, String operation) {
        final Set<String> ignoredColumns = ignoredTableColumns.get(table.toUpperCase());
        if (ignoredColumns != null) {
            if (ignoredColumns.stream().anyMatch("*"::equals)) {
                OperateLogResult result = new OperateLogResult();
                result.setOperation(operation);
                result.setTableName(table + ":*");
                result.setRecordStatus(false);
                return Optional.of(result);
            }
        }
        return Optional.empty();
    }

    private TableInfo getTableInfoByTableName(String tableName) {
        for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
            if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
                return tableInfo;
            }
        }
        return null;
    }

    /**
     * 将update SET部分的jdbc参数去除
     *
     * @param originalMappingList 这里只会包含JdbcParameter参数
     * @param updateStmt
     * @return
     */
    private List<ParameterMapping> prepareParameterMapping4Select(List<ParameterMapping> originalMappingList, Update updateStmt) {
        List<Expression> updateValueExpressions = new ArrayList<>();
        for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
            updateValueExpressions.addAll(updateSet.getValues());
        }
        int removeParamCount = 0;
        for (Expression expression : updateValueExpressions) {
            if (expression instanceof JdbcParameter) {
                ++removeParamCount;
            }
        }
        return originalMappingList.subList(removeParamCount, originalMappingList.size());
    }

    protected Map<String, Object> getUpdatedColumnDatas(String tableName, BoundSql updateSql, Statement statement) {
        Map<String, Object> columnNameValMap = new HashMap<>(updateSql.getParameterMappings().size());
        Map<Integer, String> columnSetIndexMap = new HashMap<>(updateSql.getParameterMappings().size());
        List<Column> selectItemsFromUpdateSql = new ArrayList<>();
        if (statement instanceof Update) {
            Update updateStmt = (Update) statement;
            int index = 0;
            for (UpdateSet updateSet : updateStmt.getUpdateSets()) {
                selectItemsFromUpdateSql.addAll(updateSet.getColumns());
                final ExpressionList<Expression> updateList = (ExpressionList<Expression>) updateSet.getValues();
                for (int i = 0; i < updateList.size(); ++i) {
                    Expression updateExps = updateList.get(i);
                    if (!(updateExps instanceof JdbcParameter)) {
                        columnNameValMap.put(updateSet.getColumns().get(i).getColumnName().toUpperCase(), updateExps.toString());
                    }
                    columnSetIndexMap.put(index++, updateSet.getColumns().get(i).getColumnName().toUpperCase());
                }
            }
        } else if (statement instanceof Insert) {
            Insert insert = (Insert) statement;
            selectItemsFromUpdateSql.addAll(insert.getColumns());
            columnNameValMap.putAll(detectInsertColumnValuesNonJdbcParameters(insert));
        }
        Map<String, String> relatedColumnsUpperCaseWithoutUnderline = new HashMap<>(selectItemsFromUpdateSql.size(), 1);
        for (Column item : selectItemsFromUpdateSql) {
            //FIRSTNAME: FIRST_NAME/FIRST-NAME/FIRST$NAME/FIRST.NAME
            relatedColumnsUpperCaseWithoutUnderline.put(item.getColumnName().replaceAll("[._\-$]", "").toUpperCase(), item.getColumnName().toUpperCase());
        }
        MetaObject metaObject = SystemMetaObject.forObject(updateSql.getParameterObject());
        int index = 0;
        for (ParameterMapping parameterMapping : updateSql.getParameterMappings()) {
            String propertyName = parameterMapping.getProperty();
            if (propertyName.startsWith("ew.paramNameValuePairs")) {
                ++index;
                continue;
            }
            String[] arr = propertyName.split("\.");
            String propertyNameTrim = arr[arr.length - 1].replace("_", "").toUpperCase();
            try {
                final String columnName = columnSetIndexMap.getOrDefault(index++, getColumnNameByProperty(propertyNameTrim, tableName));
                if (relatedColumnsUpperCaseWithoutUnderline.containsKey(propertyNameTrim)) {
                    final String colkey = relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim);
                    Object valObj = metaObject.getValue(propertyName);
                    if (valObj instanceof IEnum) {
                        valObj = ((IEnum<?>) valObj).getValue();
                    } else if (valObj instanceof Enum) {
                        valObj = getEnumValue((Enum) valObj);
                    }
                    if (columnNameValMap.containsKey(colkey)) {
                        columnNameValMap.put(relatedColumnsUpperCaseWithoutUnderline.get(propertyNameTrim), String.valueOf(columnNameValMap.get(colkey)).replace("?", valObj == null ? "" : valObj.toString()));
                    }
                    if (columnName != null && !columnNameValMap.containsKey(columnName)) {
                        columnNameValMap.put(columnName, valObj);
                    }
                } else {
                    if (columnName != null) {
                        columnNameValMap.put(columnName, String.valueOf(metaObject.getValue(propertyName)));
                    }
                }
            } catch (Exception e) {
                logger.warn("get value error,propertyName:{},parameterMapping:{}", propertyName, parameterMapping);
            }
        }
        dealWithUpdateWrapper(columnSetIndexMap, columnNameValMap, updateSql);
        return columnNameValMap;
    }

    /**
     * @param originalDataObj
     * @return
     */
    private List<DataChangedRecord> compareAndGetUpdatedColumnDatas(String tableName, OriginalDataObj originalDataObj, Map<String, Object> columnNameValMap) {
        final Set<String> ignoredColumns = ignoredTableColumns.get(tableName.toUpperCase());
        if (originalDataObj == null || originalDataObj.isEmpty()) {
            DataChangedRecord oneRecord = new DataChangedRecord();
            List<DataColumnChangeResult> updateColumns = new ArrayList<>(columnNameValMap.size());
            for (Map.Entry<String, Object> ety : columnNameValMap.entrySet()) {
                String columnName = ety.getKey();
                if ((ignoredColumns == null || !ignoredColumns.contains(columnName)) && !ignoreAllColumns.contains(columnName)) {
                    updateColumns.add(DataColumnChangeResult.constrcutByUpdateVal(columnName, ety.getValue()));
                }
            }
            oneRecord.setUpdatedColumns(updateColumns);
//            oneRecord.setUpdatedColumns(Collections.EMPTY_LIST);
            return Collections.singletonList(oneRecord);
        }
        List<DataChangedRecord> originalDataList = originalDataObj.getOriginalDataObj();
        List<DataChangedRecord> updateDataList = new ArrayList<>(originalDataList.size());
        for (DataChangedRecord originalData : originalDataList) {
            if (originalData.hasUpdate(columnNameValMap, ignoredColumns, ignoreAllColumns)) {
                updateDataList.add(originalData);
            }
        }
        return updateDataList;
    }

    private Object getEnumValue(Enum enumVal) {
        Optional<String> enumValueFieldName = MybatisEnumTypeHandler.findEnumValueFieldName(enumVal.getClass());
        if (enumValueFieldName.isPresent()) {
            return SystemMetaObject.forObject(enumVal).getValue(enumValueFieldName.get());
        }
        return enumVal;

    }

    @SuppressWarnings("rawtypes")
    private void dealWithUpdateWrapper(Map<Integer, String> columnSetIndexMap, Map<String, Object> columnNameValMap, BoundSql updateSql) {
        if (columnSetIndexMap.size() <= columnNameValMap.size()) {
            return;
        }
        MetaObject mpgenVal = SystemMetaObject.forObject(updateSql.getParameterObject());
        if(!mpgenVal.hasGetter(Constants.WRAPPER)){
            return;
        }
        Object ew = mpgenVal.getValue(Constants.WRAPPER);
        if (ew instanceof UpdateWrapper || ew instanceof LambdaUpdateWrapper) {
            final String sqlSet = ew instanceof UpdateWrapper ? ((UpdateWrapper) ew).getSqlSet() : ((LambdaUpdateWrapper) ew).getSqlSet();// columnName=#{val}
            if (sqlSet == null) {
                return;
            }
            MetaObject ewMeta = SystemMetaObject.forObject(ew);
            final Map paramNameValuePairs = (Map) ewMeta.getValue("paramNameValuePairs");
            String[] setItems = sqlSet.split(",");
            for (String setItem : setItems) {
                //age=#{ew.paramNameValuePairs.MPGENVAL1}
                String[] nameAndValuePair = setItem.split("=", 2);
                if (nameAndValuePair.length == 2) {
                    String setColName = nameAndValuePair[0].trim().toUpperCase();
                    String setColVal = nameAndValuePair[1].trim();//#{.mp}
                    if (columnSetIndexMap.containsValue(setColName)) {
                        String[] mpGenKeyArray = setColVal.split("\.");
                        String mpGenKey = mpGenKeyArray[mpGenKeyArray.length - 1].replace("}", "");
                        final Object setVal = paramNameValuePairs.get(mpGenKey);
                        if (setVal instanceof IEnum) {
                            columnNameValMap.put(setColName, String.valueOf(((IEnum<?>) setVal).getValue()));
                        } else {
                            columnNameValMap.put(setColName, String.valueOf(setVal));
                        }
                    }
                }
            }
        }
    }

    private Map<String, String> detectInsertColumnValuesNonJdbcParameters(Insert insert) {
        Map<String, String> columnNameValMap = new HashMap<>(4);
        final Select select = insert.getSelect();
        List<Column> columns = insert.getColumns();
        if (select instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) select;
            final List<Select> selects = setOperationList.getSelects();
            if (CollectionUtils.isEmpty(selects)) {
                return columnNameValMap;
            }
            final Select selectBody = selects.get(0);
            if (!(selectBody instanceof Values)) {
                return columnNameValMap;
            }
            Values valuesStatement = (Values) selectBody;
            if (valuesStatement.getExpressions() instanceof ExpressionList) {
                ExpressionList expressionList = valuesStatement.getExpressions();
                List<Expression> expressions = expressionList;
                for (Expression expression : expressions) {
                    if (expression instanceof RowConstructor) {
                        final ExpressionList exprList = ((RowConstructor) expression);
                        final List<Expression> insertExpList = exprList;
                        for (int i = 0; i < insertExpList.size(); ++i) {
                            Expression e = insertExpList.get(i);
                            if (!(e instanceof JdbcParameter)) {
                                final String columnName = columns.get(i).getColumnName();
                                final String val = e.toString();
                                columnNameValMap.put(columnName, val);
                            }
                        }
                    }
                }
            }
        }
        return columnNameValMap;
    }

    private String getColumnNameByProperty(String propertyName, String tableName) {
        for (TableInfo tableInfo : TableInfoHelper.getTableInfos()) {
            if (tableName.equalsIgnoreCase(tableInfo.getTableName())) {
                final List<TableFieldInfo> fieldList = tableInfo.getFieldList();
                if (CollectionUtils.isEmpty(fieldList)) {
                    return propertyName;
                }
                for (TableFieldInfo tableFieldInfo : fieldList) {
                    if (propertyName.equalsIgnoreCase(tableFieldInfo.getProperty())) {
                        return tableFieldInfo.getColumn().toUpperCase();
                    }
                }
                return propertyName;
            }
        }
        return propertyName;
    }


    private Map<String, Object> buildParameterObjectMap(BoundSql boundSql) {
        MetaObject metaObject = PluginUtils.getMetaObject(boundSql.getParameterObject());
        Map<String, Object> propertyValMap = new HashMap<>(boundSql.getParameterMappings().size());
        for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
            String propertyName = parameterMapping.getProperty();
            if (propertyName.startsWith("ew.paramNameValuePairs")) {
                continue;
            }
            Object propertyValue = metaObject.getValue(propertyName);
            propertyValMap.put(propertyName, propertyValue);
        }
        return propertyValMap;

    }


    private List<DataChangeResult> buildOriginalData(Select selectStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        List<DataChangeResult> dataChangeResults = new ArrayList<>();
        try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
            DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            ResultSet resultSet = statement.executeQuery();
            final ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            int count = 0;
            while (resultSet.next()) {
                ++count;
                if (checkTableBatchLimitExceeded(selectStmt, count)) {
                    logger.error("batch delete limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
                    throw DataUpdateLimitationException.DEFAULT;
                }
                DataChangeResult dataChangeResult = new DataChangeResult();
                List<DataChangeColumnResult> columnChangeResults = new ArrayList<>();
                dataChangeResult.setChangeColumnResults(columnChangeResults);
                for (int i = 1; i <= columnCount; ++i) {
                    DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
                    changeColumnResult.setColumnName(metaData.getColumnName(i));
                    Object res = resultSet.getObject(i);
                    if (res instanceof Clob) {
                        changeColumnResult.setOriginalValue(DataColumnChangeResult.convertClob((Clob) res));
                    } else {
                        changeColumnResult.setOriginalValue(null != res ? res.toString() : null);
                    }
                    if("id".equalsIgnoreCase(changeColumnResult.getColumnName())){
                        dataChangeResult.setId(changeColumnResult.getOriginalValue());
                    }
                    columnChangeResults.add(changeColumnResult);
                }
                dataChangeResults.add(dataChangeResult);
            }
            resultSet.close();
            return dataChangeResults;
        } catch (Exception e) {
            if (e instanceof DataUpdateLimitationException) {
                throw (DataUpdateLimitationException) e;
            }
            logger.error("try to get record tobe deleted for selectStmt={}", selectStmt, e);
            return null;
        }
    }

    private OriginalDataObj buildOriginalObjectData(Map<String, Object> updatedColumnDatas, Select selectStmt, Column pk, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        try (PreparedStatement statement = connection.prepareStatement(selectStmt.toString())) {
            DefaultParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(), boundSql);
            parameterHandler.setParameters(statement);
            ResultSet resultSet = statement.executeQuery();
            List<DataChangedRecord> originalObjectDatas = new LinkedList<>();
            int count = 0;

            while (resultSet.next()) {
                ++count;
                if (checkTableBatchLimitExceeded(selectStmt, count)) {
                    logger.error("batch update limit exceed: count={}, BATCH_UPDATE_LIMIT={}", count, BATCH_UPDATE_LIMIT);
                    throw DataUpdateLimitationException.DEFAULT;
                }
                originalObjectDatas.add(prepareOriginalDataObj(updatedColumnDatas, resultSet, pk));
            }
            OriginalDataObj result = new OriginalDataObj();
            result.setOriginalDataObj(originalObjectDatas);
            resultSet.close();
            return result;
        } catch (Exception e) {
            if (e instanceof DataUpdateLimitationException) {
                throw (DataUpdateLimitationException) e;
            }
            logger.error("try to get record tobe updated for selectStmt={}", selectStmt, e);
            return new OriginalDataObj();
        }
    }

    /**
     * 防止出现全表批量更新
     * 默认一次更新不超过1000条
     *
     * @param selectStmt
     * @param count
     * @return
     */
    private boolean checkTableBatchLimitExceeded(Select selectStmt, int count) {
        if (!batchUpdateLimitationOpened) {
            return false;
        }
        final PlainSelect selectBody = (PlainSelect) selectStmt;
        final FromItem fromItem = selectBody.getFromItem();
        if (fromItem instanceof Table) {
            Table fromTable = (Table) fromItem;
            final String tableName = fromTable.getName().toUpperCase();
            if (!BATCH_UPDATE_LIMIT_MAP.containsKey(tableName)) {
                if (count > BATCH_UPDATE_LIMIT) {
                    logger.error("batch update limit exceed for tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                            tableName, BATCH_UPDATE_LIMIT, count);
                    return true;
                }
                return false;
            }
            final Integer limit = BATCH_UPDATE_LIMIT_MAP.get(tableName);
            if (count > limit) {
                logger.error("batch update limit exceed for configured tableName={}, BATCH_UPDATE_LIMIT={}, count={}",
                        tableName, limit, count);
                return true;
            }
            return false;
        }
        return count > BATCH_UPDATE_LIMIT;
    }


    /**
     * get records : include related column with original data in DB
     *
     * @param resultSet
     * @param pk
     * @return
     * @throws SQLException
     */
    private DataChangedRecord prepareOriginalDataObj(Map<String, Object> updatedColumnDatas, ResultSet resultSet, Column pk) throws SQLException {
        final ResultSetMetaData metaData = resultSet.getMetaData();
        int columnCount = metaData.getColumnCount();
        List<DataColumnChangeResult> originalColumnDatas = new LinkedList<>();
        DataColumnChangeResult pkval = null;
        for (int i = 1; i <= columnCount; ++i) {
            String columnName = metaData.getColumnName(i).toUpperCase();
            DataColumnChangeResult col;
            Object updateVal = updatedColumnDatas.get(columnName);
            if (updateVal != null && updateVal.getClass().getCanonicalName().startsWith("java.")) {
                col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i, updateVal.getClass()));
            } else {
                col = DataColumnChangeResult.constrcutByOriginalVal(columnName, resultSet.getObject(i));
            }
            if (pk != null && columnName.equalsIgnoreCase(pk.getColumnName())) {
                pkval = col;
            } else {
                originalColumnDatas.add(col);
            }
        }
        DataChangedRecord changedRecord = new DataChangedRecord();
        changedRecord.setOriginalColumnDatas(originalColumnDatas);
        if (pkval != null) {
            changedRecord.setPkColumnName(pkval.getColumnName());
            changedRecord.setPkColumnVal(pkval.getOriginalValue());
        }
        return changedRecord;
    }


    private Columns2SelectItemsResult buildColumns2SelectItems(String tableName, List<Column> columns) {
        if (columns == null || columns.isEmpty()) {
            return Columns2SelectItemsResult.build(Collections.singletonList(new SelectItem<>(new AllColumns())), 0);
        }
        List<SelectItem<?>> selectItems = new ArrayList<>(columns.size());
        for (Column column : columns) {
            selectItems.add(new SelectItem<>(column));
        }
        TableInfo tableInfo = getTableInfoByTableName(tableName);
        if (tableInfo == null) {
            return Columns2SelectItemsResult.build(selectItems, 0);
        }
        Column pk = new Column(tableInfo.getKeyColumn());
        selectItems.add(new SelectItem<>(pk));
        Columns2SelectItemsResult result = Columns2SelectItemsResult.build(selectItems, 1);
        result.setPk(pk);
        return result;
    }

    private String buildParameterObject(BoundSql boundSql) {
        Object paramObj = boundSql.getParameterObject();
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        if (paramObj instanceof Map) {
            Map<String, Object> paramMap = (Map<String, Object>) paramObj;
            int index = 1;
            boolean hasParamIndex = false;
            String key;
            while (paramMap.containsKey((key = "param" + index))) {
                Object paramIndex = paramMap.get(key);
                sb.append(""").append(key).append(""").append(":").append(""").append(paramIndex).append(""").append(",");
                hasParamIndex = true;
                ++index;
            }
            if (hasParamIndex) {
                sb.delete(sb.length() - 1, sb.length());
                sb.append("}");
                return sb.toString();
            }
            for (Map.Entry<String, Object> ety : paramMap.entrySet()) {
                sb.append(""").append(ety.getKey()).append(""").append(":").append(""").append(ety.getValue()).append(""").append(",");
            }
            sb.delete(sb.length() - 1, sb.length());
            sb.append("}");
            return sb.toString();
        }
        sb.append("param:").append(paramObj);
        sb.append("}");
        return sb.toString();
    }

    public OperateLogResult processDelete(Delete deleteStmt, MappedStatement mappedStatement, BoundSql boundSql, Connection connection) {
        Table table = deleteStmt.getTable();
        if (checkOperateLogIgnore(table.getName())) {
            return null;
        }
        Expression where = deleteStmt.getWhere();
        PlainSelect selectBody = new PlainSelect();
        selectBody.setFromItem(table);
        selectBody.setSelectItems(Collections.singletonList(new SelectItem<>((new AllColumns()))));
        selectBody.setWhere(where);
        List<DataChangeResult> dataChangeResults = buildOriginalData(selectBody, mappedStatement, boundSql, connection);
        OperateLogResult result = new OperateLogResult();
        result.setOperation("delete");
        result.setTableName(table.getName());
        result.setRecordStatus(null != dataChangeResults);
        result.setDataChangeResults(null != dataChangeResults ? dataChangeResults : new ArrayList<>());
        return result;
    }

    /**
     * 设置批量更新记录条数上限
     *
     * @param limit
     * @return
     */
    public OperateLogMybatisInnerInterceptor setBatchUpdateLimit(int limit) {
        this.BATCH_UPDATE_LIMIT = limit;
        return this;
    }

    public OperateLogMybatisInnerInterceptor openBatchUpdateLimitation() {
        this.batchUpdateLimitationOpened = true;
        return this;
    }

    public OperateLogMybatisInnerInterceptor configTableLimitation(String tableName, int limit) {
        this.BATCH_UPDATE_LIMIT_MAP.put(tableName.toUpperCase(), limit);
        return this;
    }

    /**
     * ignoredColumns = TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2; TABLE3.*; *.COLUMN1,COLUMN2
     * 多个表用分号分隔
     * TABLE_NAME1.COLUMN1,COLUMN2 : 表示忽略这个表的这2个字段
     * TABLE3.*: 表示忽略这张表的INSERT/UPDATE,delete暂时还保留
     * *.COLUMN1,COLUMN2:表示所有表的这个2个字段名都忽略
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {

        String ignoredTableColumns = properties.getProperty("ignoredTableColumns");
        if (ignoredTableColumns == null || ignoredTableColumns.trim().isEmpty()) {
            return;
        }
        String[] array = ignoredTableColumns.split(";");
        for (String table : array) {
            int index = table.indexOf(".");
            if (index == -1) {
                logger.warn("invalid data={} for ignoredColumns, format should be TABLE_NAME1.COLUMN1,COLUMN2; TABLE2.COLUMN1,COLUMN2;", table);
                continue;
            }
            String tableName = table.substring(0, index).trim().toUpperCase();
            String[] columnArray = table.substring(index + 1).split(",");
            Set<String> columnSet = new HashSet<>(columnArray.length);
            for (String column : columnArray) {
                column = column.trim().toUpperCase();
                if (column.isEmpty()) {
                    continue;
                }
                columnSet.add(column);
            }
            if ("*".equals(tableName)) {
                ignoreAllColumns.addAll(columnSet);
            } else {
                this.ignoredTableColumns.put(tableName, columnSet);
            }
        }
    }

    public List<DataChangeResult> buildDataStr(List<DataChangedRecord> records) {
        List<DataChangeResult> dataChangeResults = new ArrayList<>();
        for (DataChangedRecord r : records) {
            DataChangeResult dataChangeResult = new DataChangeResult();
            dataChangeResult.setId(null != r.getPkColumnVal() ? r.getPkColumnVal().toString() : null);
            List<DataChangeColumnResult> changeColumnResults = new ArrayList<>();
            dataChangeResult.setChangeColumnResults(changeColumnResults);
            for (DataColumnChangeResult updatedColumn : r.getUpdatedColumns()) {
                DataChangeColumnResult changeColumnResult = new DataChangeColumnResult();
                changeColumnResult.setColumnName(updatedColumn.getColumnName());
                changeColumnResult.setOriginalValue(updatedColumn.convertDoubleQuotes(updatedColumn.getOriginalValue()));
                changeColumnResult.setUpdateValue(updatedColumn.convertDoubleQuotes(updatedColumn.getUpdateValue()));
                changeColumnResults.add(changeColumnResult);
            }
            dataChangeResults.add(dataChangeResult);
        }
        return dataChangeResults;
    }



    @Data
    public static class Columns2SelectItemsResult {

        private Column pk;
        /**
         * all column with additional columns: ID, etc.
         */
        private List<SelectItem<?>> selectItems;
        /**
         * newly added column count from meta data.
         */
        private int additionalItemCount;

        public static Columns2SelectItemsResult build(List<SelectItem<?>> selectItems, int additionalItemCount) {
            Columns2SelectItemsResult result = new Columns2SelectItemsResult();
            result.setSelectItems(selectItems);
            result.setAdditionalItemCount(additionalItemCount);
            return result;
        }
    }

    @Data
    public static class OriginalDataObj {

        private List<DataChangedRecord> originalDataObj;

        public boolean isEmpty() {
            return originalDataObj == null || originalDataObj.isEmpty();
        }

    }

    @Data
    public static class DataColumnChangeResult {

        private String columnName;
        private Object originalValue;
        private Object updateValue;

        @SuppressWarnings("rawtypes")
        public boolean isDataChanged(Object updateValue) {
            if (!Objects.equals(originalValue, updateValue)) {
                if (originalValue instanceof Clob) {
                    String originalStr = convertClob((Clob) originalValue);
                    setOriginalValue(originalStr);
                    return !originalStr.equals(updateValue);
                }
                if (originalValue instanceof Comparable) {
                    Comparable original = (Comparable) originalValue;
                    Comparable update = (Comparable) updateValue;
                    try {
                        return update == null || original.compareTo(update) != 0;
                    } catch (Exception e) {
                        return true;
                    }
                }
                return true;
            }
            return false;
        }

        public static String convertClob(Clob clobObj) {
            try {
                return clobObj.getSubString(0, (int) clobObj.length());
            } catch (Exception e) {
                try (Reader is = clobObj.getCharacterStream()) {
                    char[] chars = new char[64];
                    int readChars;
                    StringBuilder sb = new StringBuilder();
                    while ((readChars = is.read(chars)) != -1) {
                        sb.append(chars, 0, readChars);
                    }
                    return sb.toString();
                } catch (Exception e2) {
                    //ignored
                    return "unknown clobObj";
                }
            }
        }

        public static DataColumnChangeResult constrcutByUpdateVal(String columnName, Object updateValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setUpdateValue(updateValue);
            return res;
        }

        public static DataColumnChangeResult constrcutByOriginalVal(String columnName, Object originalValue) {
            DataColumnChangeResult res = new DataColumnChangeResult();
            res.setColumnName(columnName);
            res.setOriginalValue(originalValue);
            return res;
        }

        public String generateDataStr() {
            StringBuilder sb = new StringBuilder();
            sb.append(""").append(columnName).append(""").append(":").append(""").append(convertDoubleQuotes(originalValue)).append("->").append(convertDoubleQuotes(updateValue)).append(""").append(",");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.toString().replace(""", "\"");
        }
    }

    @Data
    public static class DataChangedRecord {

        private String pkColumnName;
        private Object pkColumnVal;
        private List<DataColumnChangeResult> originalColumnDatas;
        private List<DataColumnChangeResult> updatedColumns;

        public boolean hasUpdate(Map<String, Object> columnNameValMap, Set<String> ignoredColumns, Set<String> ignoreAllColumns) {
            if (originalColumnDatas == null) {
                return true;
            }
            boolean hasUpdate = false;
            updatedColumns = new ArrayList<>(originalColumnDatas.size());
            for (DataColumnChangeResult originalColumn : originalColumnDatas) {
                final String columnName = originalColumn.getColumnName().toUpperCase();
                if (ignoredColumns != null && ignoredColumns.contains(columnName) || ignoreAllColumns.contains(columnName)) {
                    continue;
                }
                Object updatedValue = columnNameValMap.get(columnName);
                if (originalColumn.isDataChanged(updatedValue)) {
                    hasUpdate = true;
                    originalColumn.setUpdateValue(updatedValue);
                    updatedColumns.add(originalColumn);
                }
            }
            return hasUpdate;
        }

        public String generateUpdatedDataStr() {
            StringBuilder sb = new StringBuilder();
            sb.append("{");
            if (pkColumnName != null) {
                sb.append(""").append(pkColumnName).append(""").append(":").append(""").append(convertDoubleQuotes(pkColumnVal)).append(""").append(",");
            }
            for (DataColumnChangeResult update : updatedColumns) {
                sb.append(update.generateDataStr());
            }
            sb.replace(sb.length() - 1, sb.length(), "}");
            return sb.toString();
        }

        public String convertDoubleQuotes(Object obj) {
            if (obj == null) {
                return null;
            }
            return obj.toString().replace(""", "\"");
        }
    }

    public static class DataUpdateLimitationException extends MybatisPlusException {

        public DataUpdateLimitationException(String message) {
            super(message);
        }

        public static DataUpdateLimitationException DEFAULT = new DataUpdateLimitationException("本次操作 因超过系统安全阈值 被拦截,如需继续,请联系管理员!");
    }

    public void setPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
}

MybatisPlusConfig

java 复制代码
@Configuration
public class MybatisPlusConfig {

    @Autowired
    private ApplicationEventPublisher publisher;
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        OperateLogMybatisInnerInterceptor operateLogMybatisInnerInterceptor = new OperateLogMybatisInnerInterceptor();
        // 配置安全阈值,例如限制批量更新或插入的记录数不超过 1000 条
        operateLogMybatisInnerInterceptor.setBatchUpdateLimit(1000);
        operateLogMybatisInnerInterceptor.setPublisher(publisher);
        interceptor.addInnerInterceptor(operateLogMybatisInnerInterceptor);
        return interceptor;
    }
}

Swagger配置

less 复制代码
@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public Docket petApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .groupName("日志审计")
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.huzhihui.data.change.controller"))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo(){
        Contact contact = new Contact("huzhihui", "https://xxx.com/", "xxx@qq.com");
        return new ApiInfo(
                "日志审计",
                "日志审计DEMO",
                "v1.0",
                "https://xxx.com/",
                contact,
                "Apache 2.0",
                "https://www.apache.org/licenses/LICENSE-2.0",
                new ArrayList());
    }
}

测试方法

less 复制代码
@Api(tags = {"用户信息"})
@RestController
@RequestMapping("userinfo")
public class UserinfoController {

    @Autowired
    private UserinfoService userinfoService;
    @Autowired
    private UserinfoMapper userinfoMapper;
    @Autowired
    private SubMapper subMapper;

    @OperateLog("新增")
    @ApiOperation(value = "新增", extensions = {@Extension(name = "", properties = {@ExtensionProperty(name = "", value = "")})})
    @Transactional(rollbackFor = Exception.class)
    @PostMapping(value = "add")
    public Object add(@RequestBody UserinfoEntity entity){
        userinfoService.save(entity);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "batchAdd")
    public Object batchAdd(int count, String preName){
        List<UserinfoEntity> entities = new ArrayList<>();
        for (int i = 0; i < count; i++) {
            UserinfoEntity entity = new UserinfoEntity();
            entity.setName(preName + i);
            entities.add(entity);
        }
        userinfoService.saveBatch(entities, count);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @PostMapping(value = "update")
    public Object update(@RequestBody UserinfoEntity entity){
        userinfoService.updateById(entity);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "delete")
    public Object delete(Long id){
        userinfoService.removeById(id);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "otherUpdate1")
    public Object otherUpdate1(String name){
        LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(UserinfoEntity::getName, name + new Date())
                .set(UserinfoEntity::getUpdateTime, new Date())
                .eq(UserinfoEntity::getName, name);
        userinfoService.update(updateWrapper);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "otherUpdate2")
    public Object otherUpdate2(String name){
        UserinfoEntity userinfoEntity = new UserinfoEntity();
        userinfoEntity.setMoney(new BigDecimal(1));
        userinfoEntity.setCode("T1");
        LambdaUpdateWrapper<UserinfoEntity> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.set(UserinfoEntity::getName, name + new Date())
                .set(UserinfoEntity::getCode, "C1")
                .set(UserinfoEntity::getUpdateTime, new Date())
                .eq(UserinfoEntity::getName, name);
        userinfoService.update(userinfoEntity, updateWrapper);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "complexUpdate1")
    public Object complexUpdate1(Long id, String code){
        userinfoMapper.complexUpdate(code,id);
        return "SUCCESS";
    }

    @Transactional(rollbackFor = Exception.class)
    @GetMapping(value = "complexUpdate2")
    public Object complexUpdate2(Long id){
        userinfoMapper.complexUpdate2(id);
        return "SUCCESS";
    }

    @GetMapping(value = "tableInfo")
    public Object tableInfo(String tableName){
        TableInfo tableInfo = TableInfoHelper.getTableInfo(tableName);
        return tableInfo;
    }

    @OperateLog("复合新增")
    @GetMapping(value = "complexAdd")
    public Object complexAdd(){
        UserinfoEntity entity = new UserinfoEntity();
        entity.setId(IdWorker.getId());
        entity.setName("XXX");
        userinfoMapper.insert(entity);

        SubEntity subEntity = new SubEntity();
        subEntity.setId(IdWorker.getId());
        subEntity.setParentId(entity.getId());
        subMapper.insert(subEntity);
        return "SUCCESS";
    }

}

测试结果

相关推荐
JH30738 小时前
SpringBoot 优雅处理金额格式化:拦截器+自定义注解方案
java·spring boot·spring
qq_124987075311 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_12 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_8187320612 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu15 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶15 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip16 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide17 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf17 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva17 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端