学习记录-操作日志

学习记录-操作日志

1.背景

系统操作日志是用于记录系统中用户或系统本身所执行的各类操作的日志信息。这些日志通常包括操作的时间、操作的用户、具体操作内容、操作结果以及其他相关信息。

安全审计:记录用户操作以防止恶意行为,确保系统的安全性。

问题排查:在系统出现问题时,可以通过操作日志快速定位问题来源。

2.数据库表设计

统一管理:t_operation_log,比如优惠券操作、权限操作等放在一张表。

细粒度拆分:t_coupon_template_log,操作记录随着业务隔离。

CREATE TABLE `t_coupon_template_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `shop_number` bigint(20) DEFAULT NULL COMMENT '店铺编号',
  `coupon_template_id` bigint(20) DEFAULT NULL COMMENT '优惠券模板ID',
  `operator_id` bigint(20) DEFAULT NULL COMMENT '操作人',
  `operation_log` text COMMENT '操作日志',
  `original_data` varchar(1024) DEFAULT NULL COMMENT '原始数据',
  `modified_data` varchar(1024) DEFAULT NULL COMMENT '修改后数据',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_shop_number` (`shop_number`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1817866003552428034 DEFAULT CHARSET=utf8mb4 COMMENT='优惠券模板操作日志表';

3.记录操作日志

@Override
public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
    // ......
    
    
    
    //业务代码
    try {
        String operationLog = String.format("%s 用户创建优惠券:%s,优惠对象:%s,优惠类型:%s,库存数量:%d,优惠商品编码:%s,有效期开始时间:%s,有效期结束时间:%s,领取规则:%s,消耗规则:%s;",
                UserContext.getUsername(),
                requestParam.getName(),
                DiscountTargetEnum.findValueByType(requestParam.getTarget()),
                DiscountTypeEnum.findValueByType(requestParam.getType()),
                requestParam.getStock(),
                requestParam.getGoods() == null ? "" : requestParam.getGoods(),
                requestParam.getValidStartTime(),
                requestParam.getValidEndTime(),
                requestParam.getReceiveRule(),
                requestParam.getConsumeRule());CouponTemplateLogDO couponTemplateLogDO = CouponTemplateLogDO.builder()
                .couponTemplateId(String.valueOf(couponTemplateDO.getId()))
                .operatorId(UserContext.getUserId())
                .shopNumber(UserContext.getShopNumber())
                .operationLog(operationLog)
                .modifiedData(JSON.toJSONString(couponTemplateDO))
                .build();
        couponTemplateLogMapper.insert(couponTemplateLogDO);
    } catch (Exception ex) {
        log.error("记录操作日志错误", ex);
    }
}

缺点如下

当业务变得复杂后,记录操作日志放在业务代码中会导致业务的逻辑比较繁琐。

对于代码的可读性和可维护性来说是一个灾难,因为不止这一处使用,存在大量的冗余代码。

4.通过 SpringAOP 和 SpEL 优雅记录

4.1什么是 Spring AOP (面向切面编程)

定义: Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架的一个重要特性,它允许开发人员在不修改原有业务逻辑的情况下,通过预定义的规则增强应用程序的功能。Spring AOP 使得横切关注点(如事务管理、日志记录、安全控制等)可以与业务逻辑分离,使代码更加模块化。

主要概念

  • 切面(Aspect):切面是一个关注横切关注点的模块。它是业务逻辑与横切关注点的一个结合点。例如,日志记录、事务管理等就是横切关注点。

  • 连接点(JoinPoint):在应用程序执行过程中能够插入切面的地方。通常是方法的执行。

  • 通知(Advice)

    :切面执行的动作。它可以定义在连接点执行之前、之后或抛出异常时要做的事。常见的通知类型有:

    • @Before:在方法执行之前执行
    • @After:在方法执行之后执行
    • @AfterReturning:方法正常执行后执行
    • @AfterThrowing:方法抛出异常后执行
    • @Around:围绕方法执行前后进行增强(可以自定义方法执行的逻辑)
  • 切点(Pointcut):定义了通知应用在哪些连接点上,通常是某些特定方法的执行。切点通过表达式来定义。

  • 目标对象(Target Object):被增强的对象,即原有业务逻辑所在的对象。

  • 代理(Proxy):由 AOP 框架创建的对象,它负责拦截方法调用并在调用前后应用通知。

Spring AOP 工作原理: Spring AOP 基于代理模式,通常有两种代理方式:

  • JDK 动态代理:基于接口创建代理对象,只能为实现了接口的类创建代理。
  • CGLIB 代理:基于类创建代理对象,通过继承的方式生成目标对象的子类,可以为没有接口的类创建代理。

4.2什么是 Spring EL (Spring Expression Language)

定义: Spring EL(Spring Expression Language)是 Spring 框架提供的一种表达式语言,用于在 Spring 配置文件、注解或代码中动态地解析和计算字符串。它提供了对对象属性、方法调用、集合操作、逻辑运算等功能的支持,可以用于动态决策和配置。

主要特性

  • 对象操作:可以访问和操作对象的属性、调用对象的方法。
  • 集合操作:支持对集合、数组、Map 等的操作。
  • 运算符支持:支持算数运算符、逻辑运算符、比较运算符等。
  • 条件判断:支持条件判断和逻辑控制。
  • 支持函数:可以调用自定义的函数或 Spring 提供的函数。
  • 内置变量:Spring EL 支持内置变量,如 #root#this#context 等,帮助在表达式中引用上下文信息。

案例:

/**
 * SpEL 表达式测试类
 */
public class CouponTemplateLogSpELTests {/**
     * 调用静态类方法
     */
    @Test
    public void testSpELGetRandom() {
        String spELKey = "T(java.lang.Math).random()";
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spELKey);
        Assert.isTrue(expression.getValue() instanceof Double);
    }/**
     * 调用静态类方法并运算
     */
    @Test
    public void testSpELGetRandomV2() {
        String spELKey = "T(java.lang.Math).random() * 100.0";
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spELKey);
        Assert.isTrue(expression.getValue() instanceof Double);
    }/**
     * 调用当前登录用户静态类方法
     */
    @Test
    public void testSpELGetCurrentUser() {
        // 初始化数据
        String userid = "1810518709471555585";
        UserContext.setUser(new UserInfoDTO(userid, "pdd45305558318", 1810714735922956666L));// 调用用户上下文获取当前用户 ID
        String spELKey = "T(com.nageoffer.onecoupon.merchant.admin.common.context.UserContext).getUserId()";
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spELKey);
        try {
            Assert.equals(expression.getValue(), userid);
        } finally {
            UserContext.removeUser();
        }
    }/**
     * 调用当前登录用户静态类方法,如果为空取默认值
     */
    @Test
    public void testSpELGetCurrentUserDefaultValue() {
        // 调用用户上下文获取当前用户 ID,如果为空,取默认值
        String spELKey = "T(com.nageoffer.onecoupon.merchant.admin.common.context.UserContext).getUserId() ?: 'ding.ma'";
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(spELKey);
        Assert.equals(expression.getValue(), "ding.ma");
    }
}

5.美团 mzt-biz-log 操作日志框架

https://juejin.cn/post/7009116644031070244

https://github.com/mouzt/mzt-biz-log

5.1依赖引入

<dependency>
    <groupId>io.github.mouzt</groupId>
    <artifactId>bizlog-sdk</artifactId>
    <version>3.0.6</version>
</dependency>

5.2启动类加上注解

应用启动类添加 @EnableLogRecord 注解,并配置租户。tenant 是代表租户的标识,一般一个服务或者一个业务下的多个服务都固定一个 tenant 就可以。

5.3添加注解 @LogRecord

删除原来的记录日志的逻辑,修改为

@LogRecord(
        success = """
                创建优惠券:{{#requestParam.name}}, \
                优惠对象:{{#requestParam.target}}, \
                优惠类型:{{#requestParam.type}}, \
                库存数量:{{#requestParam.stock}}, \
                优惠商品编码:{{#requestParam.goods}}, \
                有效期开始时间:{{#requestParam.validStartTime}}, \
                有效期结束时间:{{#requestParam.validEndTime}}, \
                领取规则:{{#requestParam.receiveRule}}, \
                消耗规则:{{#requestParam.consumeRule}};
                """,
        type = "CouponTemplate",
        bizNo = "{{#bizNo}}",
        extra = "{{#requestParam.toString()}}"
)
@Override
public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
    // ......
}

success:方法执行成功后的日志模版。

type:操作日志的类型,比如:订单类型、商品类型。

bizNo:日志绑定的业务标识,需要是我们优惠券模板的 ID,但是目前拿不到,放一个占位符。

extra:日志的额外信息。

问题:惠对象和优惠券类型都是(枚举) type 值(0/1),我们希望展示的时候是具体值,这种应该怎么解决?

5.4 自定义函数

实现 IParseFunction 接口即可完成自定义函数

@Component
public class CommonEnumParseFunction implements IParseFunction {//需要使用到的枚举
    public static final String DISCOUNT_TARGET_ENUM_NAME = DiscountTargetEnum.class.getSimpleName();
    private static final String DISCOUNT_TYPE_ENUM_NAME = DiscountTypeEnum.class.getSimpleName();@Override
    public String functionName() {
        return "COMMON_ENUM_PARSE";
    }@Override
    public String apply(Object value) {
        try {
            List<String> parts = StrUtil.split(value.toString(), "_");
            if (parts.size() != 2) {
                throw new IllegalArgumentException("格式错误,需要 '枚举类_具体值' 的形式。");
            }String enumClassName = parts.get(0);
            int enumValue = Integer.parseInt(parts.get(1));return findEnumValueByName(enumClassName, enumValue);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("第二个下划线后面的值需要是整数。", e);
        }
    }private String findEnumValueByName(String enumClassName, int enumValue) {
        if (DISCOUNT_TARGET_ENUM_NAME.equals(enumClassName)) {
            return DiscountTargetEnum.findValueByType(enumValue);
        } else if (DISCOUNT_TYPE_ENUM_NAME.equals(enumClassName)) {
            return DiscountTypeEnum.findValueByType(enumValue);
        } else {
            throw new IllegalArgumentException("未知的枚举类名: " + enumClassName);
        }
    }
}

COMMON_ENUM_PARSE 是这个函数的标识,加到 success 字符串变量中,即可自动完成解析。如果检查到 success 包含自定义函数,交由 IParseFunction#apply 方法执行。

此时修改为

@LogRecord(
        success = """
                创建优惠券:{{#requestParam.name}}, \
                优惠对象:{COMMON_ENUM_PARSE{'DiscountTargetEnum' + '_' + #requestParam.target}}, \
                优惠类型:{COMMON_ENUM_PARSE{'DiscountTypeEnum' + '_' + #requestParam.type}}, \
                库存数量:{{#requestParam.stock}}, \
                优惠商品编码:{{#requestParam.goods}}, \
                有效期开始时间:{{#requestParam.validStartTime}}, \
                有效期结束时间:{{#requestParam.validEndTime}}, \
                领取规则:{{#requestParam.receiveRule}}, \
                消耗规则:{{#requestParam.consumeRule}};
                """,
        type = "CouponTemplate",
        bizNo = "{{#bizNo}}",
        extra = "{{#requestParam.toString()}}"
)
5.5 日志记录上下文

缺少主键id,biz-log 为我们提供了日志记录上下文功能,将值放到上下文 LogRecordContext 里面,我们就能在运行时拿到。

并且LogRecordContext 会在方法结束后自动 Remove,不需要我们手动操作。

代码如下所示:

@LogRecord(
        success = """
                创建优惠券:{{#requestParam.name}}, \
                优惠对象:{COMMON_ENUM_PARSE{'DiscountTargetEnum' + '_' + #requestParam.target}}, \
                优惠类型:{COMMON_ENUM_PARSE{'DiscountTypeEnum' + '_' + #requestParam.type}}, \
                库存数量:{{#requestParam.stock}}, \
                优惠商品编码:{{#requestParam.goods}}, \
                有效期开始时间:{{#requestParam.validStartTime}}, \
                有效期结束时间:{{#requestParam.validEndTime}}, \
                领取规则:{{#requestParam.receiveRule}}, \
                消耗规则:{{#requestParam.consumeRule}};
                """,
        type = "CouponTemplate",
        bizNo = "{{#bizNo}}",
        extra = "{{#requestParam.toString()}}"
)
@Override
public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
    // ......
    // 新增优惠券模板信息到数据库
    CouponTemplateDO couponTemplateDO = BeanUtil.toBean(requestParam, CouponTemplateDO.class);
    couponTemplateDO.setStatus(CouponTemplateStatusEnum.ACTIVE.getStatus());
    couponTemplateDO.setShopNumber(UserContext.getShopNumber());
    couponTemplateMapper.insert(couponTemplateDO);
    
    // 因为模板 ID 是运行中生成的,@LogRecord 默认拿不到,所以我们需要手动设置
    LogRecordContext.putVariable("bizNo", couponTemplateDO.getId());
}
5.6 保存数据库

biz-log 中为我们预留了扩展接口,实现 ILogRecordService 接口就可以自定义保存方法。

@Slf4j
@Service
@RequiredArgsConstructor
public class DBLogRecordServiceImpl implements ILogRecordService {private final CouponTemplateLogMapper couponTemplateLogMapper;@Override
    public void record(LogRecord logRecord) {
        try {
            switch (logRecord.getType()) {
                case "CouponTemplate": {
                    CouponTemplateLogDO couponTemplateLogDO = CouponTemplateLogDO.builder()
                            .couponTemplateId(logRecord.getBizNo())
                            .shopNumber(UserContext.getShopNumber())
                            .operatorId(UserContext.getUserId())
                            .operationLog(logRecord.getAction())
                            .originalData(Optional.ofNullable(LogRecordContext.getVariable("originalData")).map(Object::toString).orElse(null))
                            .modifiedData(StrUtil.isBlank(logRecord.getExtra()) ? null : logRecord.getExtra())
                            .build();
                    couponTemplateLogMapper.insert(couponTemplateLogDO);
                }
            }
        } catch (Exception ex) {
            log.error("记录[{}]操作日志失败", logRecord.getType(), ex);
        }
    }@Override
    public List<LogRecord> queryLog(String bizNo, String type) {
        return List.of();
    }@Override
    public List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {
        return List.of();
    }
}

如果并发量比较小,可以同步执行。如果并发量比较大,在 DBLogRecordServiceImpl#record 方法中调用消息队列异步。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/953571.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

正点原子STM32F103战舰版电容触摸键学习

一、tpad.h代码 #ifndef __TPAD_H #define __TPAD_H#include "./SYSTEM/sys/sys.h"/******************************************************************************************/ /* TPAD 引脚 及 定时器 定义 *//* 我们使用定时器的输入捕获功能, 对TPAD进行检…

JVM:ZGC详解(染色指针,内存管理,算法流程,分代ZGC)

1&#xff0c;ZGC&#xff08;JDK21之前&#xff09; ZGC 的核心是一个并发垃圾收集器&#xff0c;所有繁重的工作都在Java 线程继续执行的同时完成。这极大地降低了垃圾收集对应用程序响应时间的影响。 ZGC为了支持太字节&#xff08;TB&#xff09;级内存&#xff0c;设计了基…

zerox - 使用视觉模型将 PDF 转换为 Markdown

7900 Stars 478 Forks 39 Issues 17 贡献者 MIT License Python 语言 代码: https://github.com/getomni-ai/zerox 主页: OmniAI. Automate document workflows 更多AI开源软件&#xff1a;AI开源 - 小众AI zerox基于视觉模型 API 服务&#xff0c;提供了将 PDF 文档转化为 Mar…

JAVA:Spring Boot 集成 JWT 实现身份验证的技术指南

1、简述 在现代Web开发中&#xff0c;安全性尤为重要。为了确保用户的身份&#xff0c;JSON Web Token&#xff08;JWT&#xff09;作为一种轻量级且无状态的身份验证方案&#xff0c;广泛应用于微服务和分布式系统中。本篇博客将讲解如何在Spring Boot 中集成JWT实现身份验证…

[论文阅读] (35)TIFS24 MEGR-APT:基于攻击表示学习的高效内存APT猎杀系统

《娜璋带你读论文》系列主要是督促自己阅读优秀论文及听取学术讲座&#xff0c;并分享给大家&#xff0c;希望您喜欢。由于作者的英文水平和学术能力不高&#xff0c;需要不断提升&#xff0c;所以还请大家批评指正&#xff0c;非常欢迎大家给我留言评论&#xff0c;学术路上期…

目标检测中的Bounding Box(边界框)介绍:定义以及不同表示方式

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

openEuler22.03系统使用Kolla-ansible搭建OpenStack

Kolla-ansible 是一个利用 Ansible 自动化工具来搭建 OpenStack 云平台的开源项目&#xff0c;它通过容器化的方式部署 OpenStack 服务&#xff0c;能够简化安装过程、提高部署效率并增强系统的可维护性。 前置环境准备&#xff1a; 系统:openEuler-22.03-LTS-SP4 配置&…

Leecode刷题C语言之统计重新排列后包含另一个字符串的子字符串数目②

执行结果:通过 执行用时和内存消耗如下&#xff1a; void update(int *diff, int c, int add, int *cnt) {diff[c] add;if (add 1 && diff[c] 0) {// 表明 diff[c] 由 -1 变为 0(*cnt)--;} else if (add -1 && diff[c] -1) {// 表明 diff[c] 由 0 变为 -…

uniapp 微信小程序webview与h5双向实时通信交互

描述&#xff1a; 小程序webview内嵌的h5需要向小程序实时发送消息&#xff0c;有人说postMessage可以实现&#xff0c;所以试验一下&#xff0c;结果是实现不了实时&#xff0c;只能在特定时机后退、组件销毁、分享时小程序才能接收到信息&#xff08;小程序为了安全等考虑做了…

pycharm-pyspark 环境安装

1、环境准备&#xff1a;java、scala、pyspark、python-anaconda、pycharm vi ~/.bash_profile export SCALA_HOME/Users/xunyongsun/Documents/scala-2.13.0 export PATH P A T H : PATH: PATH:SCALA_HOME/bin export SPARK_HOME/Users/xunyongsun/Documents/spark-3.5.4-bin…

fast-crud select下拉框 实现多选功能及下拉框数据动态获取(通过接口获取)

教程 fast-crud select示例配置需求:需求比较复杂 1. 下拉框选项需要通过后端接口获取 2. 实现多选功能 由于这个前端框架使用逻辑比较复杂我也是第一次使用,所以只记录核心问题 环境:vue3,typescript,fast-crud ,elementPlus 效果 代码 // crud.tsx文件(/.ts也行 js应…

高性能现代PHP全栈框架 Spiral

概述 Spiral Framework 诞生于现实世界的软件开发项目是一个现代 PHP 框架&#xff0c;旨在为更快、更清洁、更卓越的软件开发提供动力。 特性 高性能 由于其设计以及复杂精密的应用服务器&#xff0c;Spiral Framework框架在不影响代码质量以及与常用库的兼容性的情况下&a…

天机学堂笔记1

FeignClient(contextId "course", value "course-service") public interface CourseClient {/*** 根据老师id列表获取老师出题数据和讲课数据* param teacherIds 老师id列表* return 老师id和老师对应的出题数和教课数*/GetMapping("/course/infoB…

lobechat搭建本地知识库

本文中&#xff0c;我们提供了完全基于开源自建服务的 Docker Compose 配置&#xff0c;你可以直接使用这份配置文件来启动 LobeChat 数据库版本&#xff0c;也可以对之进行修改以适应你的需求。 我们默认使用 MinIO 作为本地 S3 对象存储服务&#xff0c;使用 Casdoor 作为本…

沸点 | 聚焦嬴图Cloud V2.1:具备水平可扩展性+深度计算的云原生嬴图动力站!

近日&#xff0c;嬴图正式推出嬴图Cloud V2.1&#xff0c;此次发布专注于提供无与伦比的用户体验&#xff0c;包括具有水平可扩展性的嬴图Powerhouse的一键部署、具有灵活定制功能的管理控制台、VPC / 专用链接等&#xff0c;旨在满足用户不断变化需求的各项前沿功能&#xff0…

Linux---shell脚本练习

要求&#xff1a; 1、shell 脚本写出检测 /tmp/size.log 文件如果存在显示它的内容&#xff0c;不存在则创建一个文件将创建时间写入。 2、写一个 shel1 脚本,实现批量添加 20个用户,用户名为user01-20,密码为user 后面跟5个随机字符。 3、编写个shel 脚本将/usr/local 日录下…

LiveNVR监控流媒体Onvif/RTSP常见问题-二次开发接口jquery调用示例如何解决JS|axios调用接口时遇到的跨域问题

LiveNVR二次开发接口jquery调用示例如何解决JS|axios调用接口时遇到的跨域问题 1、接口调用示例2、JS调用遇到跨域解决示例3、axios请求接口遇到跨域问题3.1、post请求3.2、get请求 4、RTSP/HLS/FLV/RTMP拉流Onvif流媒体服务 1、接口调用示例 下面是完整的 jquery 调用示例 $.a…

Canvas简历编辑器-选中绘制与拖拽多选交互方案

Canvas简历编辑器-选中绘制与拖拽多选交互方案 在之前我们聊了聊如何基于Canvas与基本事件组合实现了轻量级DOM&#xff0c;并且在此基础上实现了如何进行管理事件以及多层级渲染的能力设计。那么此时我们就依然在轻量级DOM的基础上&#xff0c;关注于实现选中绘制与拖拽多选交…

服务器数据恢复—raid5故障导致上层ORACLE无法启动的数据恢复案例

服务器数据恢复环境&故障&#xff1a; 一台服务器上的8块硬盘组建了一组raid5磁盘阵列。上层安装windows server操作系统&#xff0c;部署了oracle数据库。 raid5阵列中有2块硬盘的硬盘指示灯显示异常报警。服务器操作系统无法启动&#xff0c;ORACLE数据库也无法启动。 服…

LabVIEW光流算法的应用

该VI展示了如何使用NI Vision Development Module中的光流算法来计算图像序列中像素的运动矢量。通过该方法&#xff0c;可以实现目标跟踪、运动检测等功能&#xff0c;适用于视频处理、机器人视觉和监控领域。程序采用模块化设计&#xff0c;包含图像输入、算法处理、结果展示…