如何优雅的设计后台操作日志?

一、背景
在企业级应用,我们经常需要记录后端用户操作各种功能的日志,方便未来发现业务有问题,能通过用户操作日志追溯全流程。此外,我们还可以通过统计用户操作日志,分析某些功能点击的次数,判断哪些功能经常使用,那些功能不经常使用甚至可以下线,通过这种量化的分析,对于产品未来的功能规划也起到指导作用,所以优雅的设计一个后台操作日志功能,对于企业级系统必不可少。

二、应用场景
1、规则类变更场景(比如活动配置规则修改)
2、配置类变更场景(比如公共配置修改)
3、敏感信息类变更场景(比如客户信息修改)
以上场景,如果有变更,通常会对我们系统的业务产生直接的影响,如果人为失误,有可能造成直接的经济损失,所以我们需要记录每一笔操作日志。
那究竟如何优雅的设计一个后端操作日志功能呢?我认为起码要满足以下几个要求:
1)记录后端操作日志必须与业务功能解耦,不能硬编码耦合在一起,增加开发人员的工作量。
2)后端操作日志必须记录管理后台操作的菜单、操作的功能、操作人、操作时间等重要核心的信息。
3)后端操作日志异步入库,不能阻塞主业务流程。
4)后端操作日志重要的信息支持全模糊查询方便管理员快速查询信息。比如:操作内容字段等

三、实现方案
1、底层base公共服务提供日志相关的服务(保存日志、查询日志)
2、通过自定义注解+AOP拦截请求,自动上报日志到base的日志服务
3、通过引入guava的eventbus异步发布事件实现日志的异步入mysql库。
4、考虑到操作内容字段内容比较大,基于canal+kafka,异步将日志表记录同步到es,通过全模糊查询es,可快速查询日志记录。
同时考虑到操作日志表比较大,每3个月归档日志表一次,保存mysql日志表查询性能。

四、架构设计及实现


五、代码案例

查询活动详情接口

/**
 * 活动业务处理
 *
 * @author kyle0432
 * @date 2024/03/03 15:01
 */
@Api(tags = "活动相关api")
@RestController
@Slf4j
@RequestMapping(value = "/act/info")
public class ActInfoController {   

    @ApiOperation(value = "根据Id查询活动信息", notes = "根据Id查询活动信息")
    @PostMapping("findById")
    @SysOpLogAnnotation(menuName="活动管理", menuBtn = "查看详情", opContent = "查询活动详情",
            reqParam = "#req.id")
    public RespResult<ActInfoRespDTO> findById(@RequestBody ActInfoReqDTO req) {
        return RespResult.success(DCBeanUtil.copyNotNull(actInfoService.findById(req.getId()),
                new ActInfoRespDTO()));
    }

    @ApiOperation(value = "修改活动", notes = "修改活动")
    @PostMapping("update")
    @SysOpLogAnnotation(menuName="活动管理", menuBtn = "修改活动", opContent = "修改活动",
            reqParam = "#actInfoSaveDTO.id")
    public RespResult<Boolean> update(@RequestBody ActInfoSaveDTO actInfoSaveDTO) {
        log.info("修改保存活动....");
        if(StrUtil.isEmpty(actInfoSaveDTO.getId())){
            return RespResult.error("id不能为空");
        }
        return actInfoService.updateActInfo(actInfoSaveDTO);
    }

}

根据Id-查询活动信息

@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class ActInfoService extends ServiceImpl<ActInfoDao, ActInfoDO> {
       
    @Resource
    private ActInfoDao actInfoDao;
    
    public ActInfoDO findById(String id) {
        return actInfoDao.selectById(id);
    }

    public RespResult<Boolean> updateActInfo(ActInfoSaveDTO actInfoSaveDTO) {
        ActInfoDO actInfoDO = actInfoDao.selectById(actInfoSaveDTO.getId());
        if(ObjectUtil.isNull(actInfoDO)){
            return RespResult.error("活动不存在!actId:{}", actInfoSaveDTO.getId());
        }
        if(!ActStatusEnum.NOT_PUBLISH.getCode().equals(actInfoDO.getStatus())){
            return RespResult.error("只能修改未发布状态的活动");
        }
        DCBeanUtil.copyNotNull(actInfoDO, actInfoSaveDTO);
        this.updateById(actInfoDO);
        log.info("修改活动成功!data:{}", JSONUtil.toJsonStr(actInfoDO));
        return RespResult.success(true);
    }

}

 自定义注解

/**
 * 系统操作日志注解
 *
 * @author kyle0432
 * @date 2024/03/03 15:15
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface SysOpLogAnnotation {

    /**
     * 菜单名称
     */
    String menuName() default "";

    /**
     * 按钮名称
     */
    String menuBtn() default "";

    /**
     * 操作内容
     */
    String opContent() default "";

    /**
     * 请求参数
     */
    String reqParam() default "";
}

日志AOP拦截

package com.litian.dancechar.framework.log.bizlog.aspect;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.context.HttpContext;
import com.litian.dancechar.framework.common.eventbus.EventBusFactory;
import com.litian.dancechar.framework.common.util.IPUtil;
import com.litian.dancechar.framework.log.bizlog.annotation.SysOpLogAnnotation;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import com.litian.dancechar.framework.log.bizlog.eventbus.SysOpLogEvent;
import com.litian.dancechar.framework.log.bizlog.eventbus.SysOpLogEventBusListener;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;

/**
 * 系统操作日志切面
 *
 * @author kyle0432
 * @date 2024/03/03 15:20
 */
@Slf4j
@Aspect
@Configuration
public class SysOpLogAspect {
    private static ExpressionParser expressionParser = new SpelExpressionParser();
    private static LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();


    @Pointcut(value = "@annotation(com.litian.dancechar.framework.log.bizlog.annotation.SysOpLogAnnotation)")
    public void aroundBefore(){
        log.info("log aspect start....");
    }

    @AfterReturning(value = "aroundBefore()", returning = "respResult")
    public void aroundAfter(JoinPoint joinPoint, Object respResult){
        try {
            Signature signature = joinPoint.getSignature();
            MethodSignature methodSignature =  (MethodSignature)signature;
            Method method = methodSignature.getMethod();
            if(method.isAnnotationPresent(SysOpLogAnnotation.class)){
                ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder
                        .getRequestAttributes();
                HttpServletRequest request = attributes.getRequest();
                SysOpLogAnnotation sysOpLogAnnotation = method.getAnnotation(SysOpLogAnnotation.class);
                SysOpLogReqDTO sysOpLogReqDTO = buildSysOpLogReqDTO(sysOpLogAnnotation, request, joinPoint,
                        respResult, signature, method);
                // 发布异步事件
                EventBusFactory.build().registerAsyncEvent(SysOpLogEventBusListener.class);
                EventBusFactory.build().postAsyncEvent(SysOpLogEvent.builder()
                        .sysOpLogReqDTO(sysOpLogReqDTO).eventName("后台操作日志发布事件").build());
            }
        }catch (Throwable e){
            log.error(e.getMessage(), e);
        }
    }

    /**
     * 构建请求参数
     */
    private SysOpLogReqDTO buildSysOpLogReqDTO(SysOpLogAnnotation sysOpLogAnnotation,HttpServletRequest request,
                                              JoinPoint joinPoint, Object respResult, Signature signature,
                                              Method method){
        SysOpLogReqDTO sysOpLogReqDTO = new SysOpLogReqDTO();
        sysOpLogReqDTO.setMenuName(sysOpLogAnnotation.menuName());
        sysOpLogReqDTO.setMenuBtn(sysOpLogAnnotation.menuBtn());
        sysOpLogReqDTO.setUrl(request.getRequestURI());
        sysOpLogReqDTO.setClassName(signature.getDeclaringTypeName());
        sysOpLogReqDTO.setMethodName(method.getName());
        sysOpLogReqDTO.setReqType(request.getMethod());
        StringBuilder builder = new StringBuilder();
        Object[] args = joinPoint.getArgs();
        sysOpLogReqDTO.setOpContent(sysOpLogAnnotation.opContent());
        if(args != null && args.length > 0){
            for (Object obj : args){
                builder.append(JSONUtil.toJsonStr(obj));
            }
            if(StrUtil.isNotEmpty(sysOpLogAnnotation.reqParam())){
                sysOpLogReqDTO.setOpContent(sysOpLogReqDTO.getOpContent()+"#" + parse(sysOpLogAnnotation.reqParam(),
                        method, args));
            }
        }
        sysOpLogReqDTO.setParams(builder.toString());
        if(respResult instanceof RespResult){
            RespResult result = (RespResult)respResult;
            sysOpLogReqDTO.setSuccess(result.isOk());
        }
        sysOpLogReqDTO.setResult(JSONUtil.toJsonStr(respResult));
        sysOpLogReqDTO.setOpAccount(HttpContext.getMobile());
        sysOpLogReqDTO.setOpTime(new Date());
        sysOpLogReqDTO.setOpIp(IPUtil.getIpAddress(request));
        return sysOpLogReqDTO;
    }

    public static String parse(String key ,Method method, Object[] args){
        String[] params = discoverer.getParameterNames(method);
        if(params == null || Objects.equals("default", key)){
            return key;
        }
        EvaluationContext context = new StandardEvaluationContext();
        for(int i=0; i < params.length;i++){
            context.setVariable(params[i], args[i]);
        }
        String[] keys = key.split(",");
        StringBuilder result = new StringBuilder();
        for(String k : keys){
            result.append(expressionParser.parseExpression(k).getValue(context ,String.class));
            result.append(":");
        }
        return result.deleteCharAt(result.length() -1).toString();
    }
}

系统日志实体类

package com.litian.dancechar.framework.log.bizlog.dto;

import com.litian.dancechar.framework.common.base.BasePage;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

/**
 * 操作日志请求对象
 *
 * @author kyle0432
 * @date 2024/03/03 16:18
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class SysOpLogReqDTO extends BasePage implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    private String id;

    /**
     * 菜单名称
     */
    private String menuName;

    /**
     * 菜单按钮
     */
    private String menuBtn;

    /**
     * 请求url
     */
    private String url;

    /**
     * 类名
     */
    private String className;

    /**
     * 方法名
     */
    private String methodName;

    /**
     * 请求的类型(Get、POST)
     */
    private String reqType;

    /**
     * 请求参数
     */
    private String params;

    /**
     * 执行是否成功
     */
    private Boolean success;

    /**
     * 执行结果
     */
    private String result;

    /**
     * 操作时间
     */
    private Date opTime;

    /**
     * 操作内容
     */
    private String opAccount;

    /**
     * 操作内容
     */
    private String opContent;

    /**
     * 操作IP
     */
    private String opIp;
}

通过eventbus发布异步事件

package com.litian.dancechar.framework.common.eventbus;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.google.common.collect.Maps;
import com.google.common.eventbus.AsyncEventBus;
import com.google.common.eventbus.EventBus;
import com.litian.dancechar.framework.common.thread.CustomThreadPoolFactory;
import lombok.extern.slf4j.Slf4j;

import java.util.EventListener;
import java.util.Map;

/**
 * eventbus发布事件工厂类
 *
 * @author kyle0432
 * @date 2024/03/03 17:18
 */
@Slf4j
public class EventBusFactory {
    private static volatile EventBusFactory instance;
    private Map<String, Class<? extends EventListener>> registerListenerContainers = Maps.newConcurrentMap();
    private Map<String, Class<? extends EventListener>> asyncRegisterListenerContainers = Maps.newConcurrentMap();
    private final EventBus eventBus = new EventBus();
    private final AsyncEventBus asyncEventBus = new AsyncEventBus(new CustomThreadPoolFactory().newFixedThreadPool(
            2 * Runtime.getRuntime().availableProcessors() + 1));

    private EventBusFactory() {
    }

    public static EventBusFactory build() {
        // 双重校验实现单例
        if (instance == null) {
            synchronized (EventBusFactory.class) {
                if (instance == null) {
                    instance = new EventBusFactory();
                }
            }
        }
        return instance;
    }

    /**
     * 发布异步事件
     *
     * @param event 事件对象
     */
    public void postAsyncEvent(BaseEvent event) {
        asyncEventBus.post(event);
    }

    /**
     * 注册异步监听器
     *
     * @param asyncClass 异步监听器
     */
    public void registerAsyncEvent(Class<? extends EventListener> asyncClass) {
        String asyncClassName = asyncClass.getSimpleName();
        if (asyncRegisterListenerContainers.containsKey(asyncClassName)) {
            return;
        }
        try {
            asyncRegisterListenerContainers.put(asyncClassName, asyncClass);
            Object obj = SpringUtil.getBean(asyncClass);
            if (ObjectUtil.isNull(obj)) {
                obj = asyncRegisterListenerContainers.get(asyncClassName).newInstance();
            }
            asyncEventBus.register(obj);
        } catch (Exception e) {
            log.error("注册异步监听器系统异常!asyncClassName:{},errMsg:{}", asyncClassName, e.getMessage(), e);
        }
    }
}

系统操作日志异步事件

package com.litian.dancechar.framework.log.bizlog.eventbus;

import cn.hutool.core.util.IdUtil;
import com.litian.dancechar.framework.common.eventbus.BaseEvent;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import lombok.Builder;
import lombok.Getter;

/**
 * 系统操作日志异步事件
 *
 * @author kyle0432
 * @date 2024/03/03 17:35
 */
@Getter
public class SysOpLogEvent extends BaseEvent {
    /**
     * 操作日志对象
     */
    private SysOpLogReqDTO sysOpLogReqDTO;

    @Builder
    public SysOpLogEvent(SysOpLogReqDTO sysOpLogReqDTO, String eventName) {
        super(IdUtil.objectId(), eventName);
        this.sysOpLogReqDTO = sysOpLogReqDTO;
    }
}

系统操作日志异步操作监听

package com.litian.dancechar.framework.log.bizlog.eventbus;

import cn.hutool.core.util.ObjectUtil;
import com.google.common.eventbus.AllowConcurrentEvents;
import com.google.common.eventbus.Subscribe;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import com.litian.dancechar.framework.log.bizlog.feign.BaseServiceClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.EventListener;


/**
 * 系统操作日志异步操作监听
 *
 * @author kyle0432
 * @date 2024/03/03 18:15
 */
@Slf4j
@Component
public class SysOpLogEventBusListener implements EventListener {
    @Resource
    private BaseServiceClient baseServiceClient;

    @Subscribe
    @AllowConcurrentEvents
    public void listener(SysOpLogEvent sysOpLogEvent){
        SysOpLogReqDTO sysOpLogReqDTO = sysOpLogEvent.getSysOpLogReqDTO();
        if(ObjectUtil.isNotNull(sysOpLogReqDTO)){
            baseServiceClient.saveWithInsert(sysOpLogReqDTO);
        }
    }
}

feign调用另外一个服务,进行数据库新增日志操作

package com.litian.dancechar.framework.log.bizlog.feign;

import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.log.bizlog.dto.SysOpLogReqDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;


/**
 * 基础服务feign
 *
 * @author kyle0432
 * @date 2024/03/03 18:04
 */
@FeignClient("dancechar-base-service")
public interface BaseServiceClient {

    /**
     * 保存操作日志
     */
    @PostMapping("/sys/oplog/saveWithInsert")
    RespResult<Boolean> saveWithInsert(@RequestBody SysOpLogReqDTO req);
}

调用服务进行数据处理及新增数据库日志

package com.litian.dancechar.base.biz.oplog.controller;

import cn.hutool.core.util.StrUtil;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogReqDTO;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogRespDTO;
import com.litian.dancechar.base.biz.oplog.service.SysOpLogService;
import com.litian.dancechar.base.common.constants.CommConstants;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.util.DCBeanUtil;
import com.litian.dancechar.framework.es.util.ElasticsearchUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.List;

/**
 * 系统操作日志处理
 *
 * @author kyle0432
 * @date 2024/03/03 16:26
 */
@Api(tags = "系统操作日志相关api")
@RestController
@Slf4j
@RequestMapping(value = "/sys/oplog/")
public class SysOpLogController {
    @Resource
    private SysOpLogService sysOpLogService;

    @Resource
    private ElasticsearchUtil elasticsearchUtil;


    @ApiOperation(value = "通过es分页查询列表", notes = "通过es分页查询列表")
    @PostMapping("listPagedByEs")
    public RespResult<PageWrapperDTO<SysOpLogRespDTO>> listPagedByEs(@RequestBody SysOpLogReqDTO req) {
        // 构造搜索条件
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        sourceBuilder.query(QueryBuilders.matchAllQuery());
        if(StrUtil.isNotEmpty(req.getMenuName())){
            // 模糊查询
            sourceBuilder.query(QueryBuilders.multiMatchQuery(req.getMenuName(),"menuName"));
        }
        if(StrUtil.isNotEmpty(req.getOpAccount())){
            // 精准查询
            sourceBuilder.query(QueryBuilders.termsQuery(req.getOpContent(),"opContent"));
        }
        PageWrapperDTO<SysOpLogRespDTO> opLogList =  elasticsearchUtil.queryListPageDataByCondition(
                CommConstants.EsIndex.INDEX_SYS_OP_LOG, sourceBuilder, req.getPageSize(), req.getPageNo(),
                "id,menuName,className", "updateDate", SortOrder.DESC, SysOpLogRespDTO.class);
        return RespResult.success(opLogList);
    }


    @ApiOperation(value = "新增保存", notes = "新增保存")
    @PostMapping("saveWithInsert")
    public RespResult<Boolean> saveWithInsert(@RequestBody SysOpLogReqDTO req) {
        log.info("新增保存数据....");
        return sysOpLogService.saveWithInsert(req);
    }
}

操作日志服务

package com.litian.dancechar.base.biz.oplog.service;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.pagehelper.PageHelper;
import com.litian.dancechar.base.biz.oplog.dao.entity.SysOpLogDO;
import com.litian.dancechar.base.biz.oplog.dao.inf.SysOpLogDao;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogReqDTO;
import com.litian.dancechar.base.biz.oplog.dto.SysOpLogRespDTO;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import com.litian.dancechar.framework.common.base.RespResult;
import com.litian.dancechar.framework.common.util.DCBeanUtil;
import com.litian.dancechar.framework.common.util.PageResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;


/**
 * 操作日志服务
 *
 * @author kyle0432
 * @date 2024/03/03 18:30
 */
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class SysOpLogService extends ServiceImpl<SysOpLogDao, SysOpLogDO> {
    @Resource
    private SysOpLogDao sysOpLogDao;

    /**
     * 功能: 分页查询操作日志列表
     */
    public RespResult<PageWrapperDTO<SysOpLogRespDTO>> listPaged(SysOpLogReqDTO req) {
        PageHelper.startPage(req.getPageNo(), req.getPageSize());
        PageWrapperDTO<SysOpLogRespDTO> pageCommon = new PageWrapperDTO<>();
        PageResultUtil.setPageResult(sysOpLogDao.findList(req), pageCommon);
        return RespResult.success(pageCommon);
    }

    /**
     * 功能:查询操作日志信息
     */
    public SysOpLogDO findById(String id) {
        return sysOpLogDao.selectById(id);
    }

    /**
     * 功能:新增操作日志
     */
    public RespResult<Boolean> saveWithInsert(SysOpLogReqDTO sysOpLogReqDTO) {
        if(StrUtil.isNotEmpty(sysOpLogReqDTO.getUrl()) && sysOpLogReqDTO.getUrl().length() > 500){
            sysOpLogReqDTO.setUrl(sysOpLogReqDTO.getUrl().substring(0,500));
        }
        if(StrUtil.isNotEmpty(sysOpLogReqDTO.getParams()) && sysOpLogReqDTO.getParams().length() > 1024){
            sysOpLogReqDTO.setParams(sysOpLogReqDTO.getParams().substring(0,1024));
        }
        if(StrUtil.isNotEmpty(sysOpLogReqDTO.getOpContent()) && sysOpLogReqDTO.getOpContent().length() > 1024){
            sysOpLogReqDTO.setOpContent(sysOpLogReqDTO.getOpContent().substring(0,1024));
        }
        SysOpLogDO sysOpLogDO = new SysOpLogDO();
        DCBeanUtil.copyNotNull(sysOpLogDO, sysOpLogReqDTO);
        save(sysOpLogDO);
        return RespResult.success(true);
    }
}

安装一个canal服务端,并且配置mysql库,日志写入mysql,然后canal监听mysql并且解析binlog日志,将解析后的信息发送到kafka。

消费操作日志MQ,并且写入到ES

package com.litian.dancechar.canal.biz.oplog;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.otter.canal.protocol.FlatMessage;
import com.litian.dancechar.canal.common.constants.CommConstants;
import com.litian.dancechar.framework.es.util.ElasticsearchUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;
import java.util.Map;


/**
 * 消费操作日志MQ
 *
 * @author kyle0432
 * @date 2024/03/03 23:13
 */
@Slf4j
@Component
public class ConsumerSysOpLogMQMsg {
    @Resource
    private ElasticsearchUtil elasticsearchUtil;

    @KafkaListener(groupId="dancechar-canal-data-service", topics = {CommConstants.KafkaTopic.TOPIC_SYS_OP_LOG})
    public void consumerCustomer(ConsumerRecord<Integer, String> record) {
        log.info("消费操作日志消息!topic:{},partition:{},value:{}",record.topic(),record.partition(), record.value());
        FlatMessage flatMessage = JSONUtil.toBean(record.value(), FlatMessage.class);
        if(ObjectUtil.isNotNull(flatMessage)){
            String type = flatMessage.getType();
            List<Map<String, String>> result = flatMessage.getData();
            if(CollUtil.isNotEmpty(result)){
                Map<String, String> dataMap = result.get(0);
                SysOpLogDO sysOpLogDO = new SysOpLogDO();
                BeanUtil.fillBeanWithMap(dataMap, sysOpLogDO, true);
                dealData(type, sysOpLogDO);
            }
        }
    }

    private void dealData(String type, SysOpLogDO sysOpLogDO){
        if("INSERT".equals(type)){
            elasticsearchUtil.addData(sysOpLogDO, CommConstants.EsIndex.INDEX_SYS_OP_LOG,sysOpLogDO.getId());
            log.info("新增记录成功!docId:{}", sysOpLogDO.getId());
        }else if("UPDATE".equals(type)){
            elasticsearchUtil.updateDataById(sysOpLogDO, CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId());
            log.info("修改记录成功!docId:{}", sysOpLogDO.getId());
        }else if("DELETE".equals(type)){
            elasticsearchUtil.deleteDataById(CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId());
            log.info("删除记录成功!docId:{}", sysOpLogDO.getId());
        }
        log.info("操作完成后查询es记录!{}", elasticsearchUtil.searchDataById(CommConstants.EsIndex.INDEX_SYS_OP_LOG, sysOpLogDO.getId()));
    }
}

es工具类

package com.litian.dancechar.framework.es.util;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import com.litian.dancechar.framework.common.base.PageWrapperDTO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.core.TimeValue;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.FetchSourceContext;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Qualifier;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * es工具类
 *
 * @author kyle0432
 * @date 2024/03/04 14:30
 */
@Slf4j
public class ElasticsearchUtil {
    @Resource
    @Qualifier("restHighLevelClient")
    private RestHighLevelClient restHighLevelClient;

    /**
     * 获取低水平客户端
     */
    public RestClient getLowLevelClient() {
        return restHighLevelClient.getLowLevelClient();
    }

    /**
     * 创建索引
     * @param indexName 索引
     * @return 结果
     */
    public boolean createIndex(String indexName){
        try{
            if(isIndexExist(indexName)){
                log.error("Index is  exits!");
                return false;
            }
            //1.创建索引请求
            CreateIndexRequest request = new CreateIndexRequest(indexName);
            //2.执行客户端请求
            org.elasticsearch.client.indices.CreateIndexResponse response = restHighLevelClient.indices()
                    .create(request, RequestOptions.DEFAULT);
            return response.isAcknowledged();
        } catch (Exception e){
            log.error(e.getMessage() ,e);
            return false;
        }
    }

    /**
     * 判断索引是否存在
     * @param indexName 索引
     * @return 索引是否存在
     */
    public boolean isIndexExist(String indexName) {
        try{
            GetIndexRequest request = new GetIndexRequest(indexName);
            return restHighLevelClient.indices().exists(request, RequestOptions.DEFAULT);
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return false;
        }

    }

    /**
     * 删除索引
     * @param indexName 索引名称
     * @return 索引删除结果
     */
    public boolean deleteIndex(String indexName) {
        try{
            if(!isIndexExist(indexName)) {
                log.error("Index is not exits!");
                return false;
            }
            DeleteIndexRequest request = new DeleteIndexRequest(indexName);
            AcknowledgedResponse delete = restHighLevelClient.indices().delete(request, RequestOptions.DEFAULT);
            return delete.isAcknowledged();
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * 数据添加,自定义id
     * @param object 要增加的数据
     * @param index      索引,类似数据库
     * @param id         数据ID,为null时es随机生成
     * @return 返回添加数据结果
     */
    public String addData(Object object, String index, String id) {
        try{
            //创建请求
            IndexRequest request = new IndexRequest(index);
            //规则 put /test_index/_doc/1
            request.id(id);
            request.timeout(TimeValue.timeValueSeconds(1));
            //将数据放入请求 json
            IndexRequest source = request.source(JSON.toJSONString(object), XContentType.JSON);
            //客户端发送请求
            IndexResponse response = restHighLevelClient.index(request, RequestOptions.DEFAULT);
            return response.getId();
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 数据添加 随机id
     */
    public String addData(Object object, String index) {
        try{
            return addData(object, index, UUID.randomUUID().toString()
                    .replaceAll("-", "").toUpperCase());
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 通过ID删除数据
     */
    public boolean deleteDataById(String index,String id) {
        try{
            DeleteRequest request = new DeleteRequest(index, id);
            DeleteResponse deleteResponse = restHighLevelClient.delete(request, RequestOptions.DEFAULT);
            return deleteResponse.status().getStatus() == 200;
        } catch (Exception e){
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * 通过ID 更新数据
     */
    public boolean updateDataById(Object object, String index, String id) {
        try{
            UpdateRequest update = new UpdateRequest(index, id);
            update.timeout("1s");
            update.doc(JSON.toJSONString(object), XContentType.JSON);
            UpdateResponse updateResponse = restHighLevelClient.update(update, RequestOptions.DEFAULT);
            return updateResponse.status().getStatus() == 200;
        } catch (Exception e){
            log.error(e.getMessage() ,e);
            return false;
        }
    }

    /**
     * 通过ID获取数据
     */
    public Map<String,Object> searchDataById(String index, String id) {
        try {
            GetRequest request = new GetRequest(index, id);
            GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
            return response.getSource();
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 通过ID获取数据
     */
    public Map<String,Object> searchDataById(String index, String id, String fields) {
        try {
            GetRequest request = new GetRequest(index, id);
            if (StringUtils.isNotEmpty(fields)){
                //只查询特定字段。如果需要查询所有字段则不设置该项。
                request.fetchSourceContext(new FetchSourceContext(true,fields.split(",")
                        , Strings.EMPTY_ARRAY));
            }
            GetResponse response = restHighLevelClient.get(request, RequestOptions.DEFAULT);
            return response.getSource();
        }catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 通过ID判断文档是否存在
     */
    public  boolean existsById(String index,String id) {
        try{
            GetRequest request = new GetRequest(index, id);
            //不获取返回的_source的上下文
            request.fetchSourceContext(new FetchSourceContext(false));
            request.storedFields("_none_");
            return restHighLevelClient.exists(request, RequestOptions.DEFAULT);
        } catch (Exception e){
            log.error(e.getMessage(), e);
            return false;
        }
    }

    /**
     * 批量插入,true:成功 false: 失败
     */
    public boolean bulkPost(String index, List<?> objects, List<String> ids) {
        BulkRequest bulkRequest = new BulkRequest();
        BulkResponse response=null;
        //最大数量不得超过20万
        for(int i=0;i<objects.size();i++){
            IndexRequest request = new IndexRequest(index);
            request.id(ids.get(i));
            request.source(JSON.toJSONString(objects.get(i)), XContentType.JSON);
            bulkRequest.add(request);
        }
        try {
            response = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return !response.hasFailures();
    }

    /**
     * 批量插入,true:成功 false: 失败
     */
    public boolean bulkPost(String index, List<?> objects) {
        BulkRequest bulkRequest = new BulkRequest();
        BulkResponse response=null;
        //最大数量不得超过20万
        for (Object object: objects) {
            IndexRequest request = new IndexRequest(index);
            request.source(JSON.toJSONString(object), XContentType.JSON);
            bulkRequest.add(request);
        }
        try {
            response = restHighLevelClient.bulk(bulkRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            log.error(e.getMessage(), e);
        }
        return !response.hasFailures();
    }

    /**
     *  查询es列表(不分页)
     * @param index      索引
     * @param builder    查询条件
     *  SearchSourceBuilder sourceBuilder=new SearchSourceBuilder(); // 构造搜索条件
     *  sourceBuilder.query(QueryBuilders.matchAllQuery());
     * 	sourceBuilder.query(QueryBuilders.multiMatchQuery("java","name"));  // 模糊查询name属性中含有Java的
     * 	sourceBuilder.query(QueryBuilders.termsQuery("10001","opContent")) // 精准查询opContent属性中包含10001的
     * @param fields     返回的字段
     * @param sortField  排序字段
     * @param convertResultClass  返回结果对应的实体
     * @return  查询的es列表
     */
    public <T> List<T> queryListDataByCondition(String index, SearchSourceBuilder builder,
                                                String fields, String sortField,
                                                Class<T> convertResultClass) {
        try {
            SearchRequest request = new SearchRequest(index);
            if (StringUtils.isNotEmpty(fields)){
                // 只查询特定字段,如果需要查询所有字段则不设置该项
                builder.fetchSource(
                        new FetchSourceContext(true,fields.split(","), Strings.EMPTY_ARRAY));
            }
            if (StringUtils.isNotEmpty(sortField)){
                // 排序字段,注意如果proposal_no是text类型会默认带有keyword性质,需要拼接.keyword
                builder.sort(sortField+".keyword", SortOrder.ASC);
            }
            request.source(builder);
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().getStatus() == 200) {
                SearchHit[] hits = response.getHits().getHits();
                if(hits != null && hits.length > 0){
                    List<T> result = new ArrayList<>();
                    for(SearchHit hit : hits){
                        Map<String,Object> source = hit.getSourceAsMap();
                        result.add(BeanUtil.mapToBean(source, convertResultClass, true));
                    }
                    return result;
                }
            }
            return null;
        } catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

    /**
     * 查询es列表(分页不带高亮)
     * @param index          索引名称
     * @param query          查询条件
     * @param pageSize       每页多少条
     * @param pageNo         第几页
     * @param fields         需要显示的字段,逗号分隔(缺省为全部字段)
     * @param sortField      排序字段
     * @param convertResultClass  返回结果对应的实体
     * @return es列表
     */
    public <T> PageWrapperDTO<T> queryListPageDataByCondition(String index, SearchSourceBuilder query, Integer pageSize,
        Integer pageNo, String fields, String sortField, SortOrder sortOrder, Class<T> convertResultClass) {
        try {
            SearchRequest request = new SearchRequest(index);
            SearchSourceBuilder builder = query;
            if (StringUtils.isNotEmpty(fields)){
                // 只查询特定字段。如果需要查询所有字段则不设置该项。
                builder.fetchSource(new FetchSourceContext(true,fields.split(","),
                        Strings.EMPTY_ARRAY));
            }
            int size = (pageSize == null ? 10 : pageSize);
            int from = (pageNo == null || pageNo == 0 ? 0 : (pageNo-1) * size);
            builder.from(from).size(size);
            if (StringUtils.isNotEmpty(sortField)){
                // 排序字段,注意如果proposal_no是text类型会默认带有keyword性质,需要拼接.keyword
                builder.sort(sortField+".keyword", sortOrder);
            }
            request.source(builder);
            SearchResponse response = restHighLevelClient.search(request, RequestOptions.DEFAULT);
            if (response.status().getStatus() == 200) {
                SearchHits searchHits = response.getHits();
                SearchHit[] hits = searchHits.getHits();
                if(hits != null && hits.length > 0){
                    PageWrapperDTO<T> pageWrapperDTO = new PageWrapperDTO<>();
                    List<T> result = Lists.newArrayList();
                    for(SearchHit hit : hits){
                        Map<String,Object> source = hit.getSourceAsMap();
                        result.add(BeanUtil.mapToBean(source, convertResultClass, true));
                    }
                    pageWrapperDTO.setList(result);
                    pageWrapperDTO.setTotal(Convert.toInt(searchHits.getTotalHits().value));
                    return pageWrapperDTO;
                }
            }
            return null;
        } catch (Exception e){
            log.error(e.getMessage(), e);
            return null;
        }
    }

}
六、结果演示

产生日志前数据库数据

调用查询活动详情接口

数据库产生查看活动详情的日志记录

再次操作一下修改活动操作

最后根据es查询日志列表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值