您的位置:首页 > 新闻 > 会展 > 沈阳男科医院在哪里_企业查询官网入口查询免费_营销目标分为三个方面_搜索优化推广公司

沈阳男科医院在哪里_企业查询官网入口查询免费_营销目标分为三个方面_搜索优化推广公司

2024/12/22 19:14:12 来源:https://blog.csdn.net/laodanqiu/article/details/144501418  浏览:    关键词:沈阳男科医院在哪里_企业查询官网入口查询免费_营销目标分为三个方面_搜索优化推广公司
沈阳男科医院在哪里_企业查询官网入口查询免费_营销目标分为三个方面_搜索优化推广公司

🎯导读:mzt-biz-log 是一个用于记录操作日志的通用组件,旨在追踪系统中“谁”在“何时”对“何事”执行了“何种操作”。该组件通过简单的注解配置,如 @LogRecord,即可实现接口调用的日志记录,支持成功与失败场景下的差异化日志描述。它还提供了丰富的功能,包括但不限于租户隔离、日志子类型划分、条件性日志记录以及枚举值解析等。此外,mzt-biz-log 支持自定义日志存储逻辑,允许开发者根据业务需求将日志持久化到数据库或其他存储媒介。整体设计简洁高效,适用于微服务架构中的日志管理需求。

文章目录

  • mzt-biz-log介绍
  • 具体实现
    • 依赖
    • 添加注解
    • 枚举类型转化为具体值
      • 枚举类
      • 实现解析器类
      • 使用
    • 日志子类型划分
    • 日志过滤
    • 日志持久化
      • 数据库
      • 继承存储接口

mzt-biz-log介绍

mzt-biz-log:一套通用操作日志组件,用来记录「谁」在「什么时间」对「什么」做了「什么事」

  • github仓库:https://github.com/mouzt/mzt-biz-log

具体实现

依赖

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

添加注解

首先需要在具体的服务启动类中添加注解@EnableLogRecord(tenant = "venue"),其中tenant是租户标识,我这里设置为了服务的名称,一般一个服务或者一个业务下的多个服务都用一个 tenant 就可以了

在这里插入图片描述

然后在具体的接口添加注解@LogRecord,在调用相应的接口之后,就会触发日志

@Repeatable(LogRecords.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {String success();String fail() default "";String operator() default "";String type();String subType() default "";String bizNo();String extra() default "";String condition() default "";String successCondition() default "";
}
  • type:日志类型,可以用来区分不同的接口,我这里直接设置为接口名称,方便辨识
  • subType:日志子类型,可以用来区分不同的操作者身份
  • bizNo:日志ID,可以设置为具体的数据的ID,这样查询日志的时候,直接使用相应数据的ID来查询,例如说bizNo存储的是订单ID,后面可以凭借这个来查询该订单相关的日志
  • success:接口调用成功之后,action存放什么数据(action字段是什么,看日志持久化就知道了),一般通过描述语言拼接字段值来实现快速让用户知道日志的内容
  • fail:接口调用异常之后,action存放什么数据
  • extra:需要记录的额外信息,如直接将用户提交的数据的 json 进行存储,因为action存储的是简略的信息
  • operator:存储操作人信息,需要用户的系统已经实现了用户上下文

【成功调用示例】

/*** 增添数据*/
@PostMapping("/save")
@LogRecord(bizNo = "{{#id}}",type = "新增分区",success = """场馆ID{{#partitionDO.venueId}}, \分区名称:{{#partitionDO.name}}, \分区类型:{{#partitionDO.type}}, \描述:{{#partitionDO.description}}, \场区拥有的场数量:{{#partitionDO.num}}, \场区状态:{{#partitionDO.status}}; \结果:{{#_ret}}""",fail = "接口调用失败,失败原因:{{#_errorMsg}}",extra = "{{#partitionDO.toString()}}",operator = "{{T(com.vrs.common.context.UserContext).getUsername()}}"
)
public Result save(@Validated({AddGroup.class}) @RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的,@LogRecord 默认拿不到,需要我们将信息手动设置到上下文中LogRecordContext.putVariable("id", partitionDO.getId());return Results.success();
}

注意:

  • 获取接口返回的结果:{{#_ret}}
  • 通过日志上下文记录信息:因为 id 是存储到数据库中才生成的,@LogRecord 一开始拿不到,需要我们将信息手动设置到上下文中。可以通过LogRecordContext.putVariable("id", partitionDO.getId());来设置键值对,然后在注解中凭借键来获取值就可以,如bizNo = "{{#id}}"

在这里插入图片描述

接口调用成功的日志内容如下:

【logRecord】log=LogRecord(id=null, tenant=venue, type=新增分区, subType=, bizNo=1868205198032568320, operator=admin, action=场馆ID:12345, 分区名称:篮球场A区, 分区类型:营业中, 描述:提供标准篮球设施,包括篮球和球架。, 场区拥有的场数量:4, 场区状态:篮球; 结果:Result(code=0, message=null, data=null, requestId=null)
, fail=false, createTime=Sun Dec 15 16:03:23 CST 2024, extra=PartitionDO(venueId=1865271207637635072, name=篮球场A区, type=1, description=提供标准篮球设施,包括篮球和球架。, num=4, status=1), codeVariable={MethodName=save, ClassName=class com.vrs.controller.PartitionController})

【失败调用示例】

首先在接口中模拟一个除以 0 异常,即System.out.println(1/0);。然后在注解中添加fail = "接口调用失败,失败原因:{{#_errorMsg}}",其中#_errorMsg获取的是异常的信息

@PostMapping("/save")
@LogRecord(bizNo = "{{#id}}",type = "新增分区",success = """场馆ID{{#partitionDO.venueId}}, \分区名称:{{#partitionDO.name}}, \分区类型:{{#partitionDO.type}}, \描述:{{#partitionDO.description}}, \场区拥有的场数量:{{#partitionDO.num}}, \场区状态:{{#partitionDO.status}}; \结果:{{#_ret}}""",fail = "接口调用失败,失败原因:{{#_errorMsg}}",extra = "{{#partitionDO.toString()}}",operator = "{{T(com.vrs.common.context.UserContext).getUsername()}}"
)
public Result save(@Validated({AddGroup.class}) @RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的,@LogRecord 默认拿不到,需要我们将信息手动设置到上下文中LogRecordContext.putVariable("id", partitionDO.getId());System.out.println(1/0);return Results.success();
}

调用失败之后的日志如下:

【logRecord】log=LogRecord(id=null, tenant=venue, type=新增分区, subType=, bizNo=1868210725902950400, operator=admin, action=接口调用失败,失败原因:/ by zero, fail=true, createTime=Sun Dec 15 16:25:21 CST 2024, extra=PartitionDO(venueId=1865271207637635072, name=篮球场A, type=1, description=提供标准篮球设施,包括篮球和球架。, num=4, status=1), codeVariable={MethodName=save, ClassName=class com.vrs.controller.PartitionController})

枚举类型转化为具体值

上面日志输出中,分区类型和场区状态的是具体的数字值

在这里插入图片描述

如果说想要将类型对应为具体的值,应该如何实现呢?

枚举类

【场馆类型枚举】

package com.vrs.enums;import lombok.Getter;
import lombok.RequiredArgsConstructor;/*** 场馆类型枚举*/
@RequiredArgsConstructor
public enum PartitionStatusEnum {BASKET_BALL(1, "篮球"),FOOT_BALL(2, "足球"),BADMINTON(3, "羽毛球"),VOLLEYBALL(4, "排球"),TABLE_TENNIS(5, "乒乓球"),TENNIS(6, "网球"),SWIMMING(7, "游泳"),GYMNASTICS(8, "体操"),FITNESS_CENTER(9, "健身房"),HANDBALL(10, "手球"),ICE_SKATING(11, "滑冰"),SKATEBOARDING(12, "滑板"),CLIMBING(13, "攀岩"),CYCLING_INDOOR(14, "室内自行车"),YOGA(15, "瑜伽");@Getterprivate final int type;@Getterprivate final String value;/*** 根据 type 找到对应的 value** @param type 要查找的类型代码* @return 对应的描述值,如果没有找到抛异常*/public static String findValueByType(int type) {for (PartitionStatusEnum target : PartitionStatusEnum.values()) {if (target.getType() == type) {return target.getValue();}}throw new IllegalArgumentException();}
}

【场区状态枚举】

package com.vrs.enums;import lombok.Getter;
import lombok.RequiredArgsConstructor;/*** 场区状态枚举*/
@RequiredArgsConstructor
public enum VenueTypeEnum {CLOSED(0, "已关闭"),OPEN(1, "营业中"),MAINTAIN(2, "维护中");@Getterprivate final int type;@Getterprivate final String value;/*** 根据 type 找到对应的 value** @param type 要查找的类型代码* @return 对应的描述值,如果没有找到抛异常*/public static String findValueByType(int type) {for (VenueTypeEnum target : VenueTypeEnum.values()) {if (target.getType() == type) {return target.getValue();}}throw new IllegalArgumentException();}
}

实现解析器类

转换类需要继承IParseFunction接口,然后实现两个方法

  • functionName:返回解析器的标识,后面需要在注解中使用来辨识不同的解析器
  • apply:主要用来实现解析工作,如将枚举类型转化为具体的值
package com.vrs.biglog;import com.mzt.logapi.service.IParseFunction;
import com.vrs.enums.PartitionStatusEnum;
import org.springframework.stereotype.Component;/*** @Author dam* @create 2024/12/15 16:43*/
@Component
public class PartitionStatusEnumParse implements IParseFunction {@Overridepublic String functionName() {return "PartitionStatusEnumParse";}@Overridepublic String apply(Object value) {return PartitionStatusEnum.findValueByType(Integer.parseInt(value.toString()));}
}
package com.vrs.biglog;import com.mzt.logapi.service.IParseFunction;
import com.vrs.enums.VenueTypeEnum;
import org.springframework.stereotype.Component;/*** @Author dam* @create 2024/12/15 16:43*/
@Component
public class VenueTypeEnumParse implements IParseFunction {@Overridepublic String functionName() {return "VenueTypeEnumParse";}@Overridepublic String apply(Object value) {return VenueTypeEnum.findValueByType(Integer.parseInt(value.toString()));}
}

使用

@PostMapping("/save")
@LogRecord(bizNo = "{{#id}}",type = "新增分区",success = """场馆ID{{#partitionDO.venueId}}, \分区名称:{{#partitionDO.name}}, \分区类型:{VenueTypeEnumParse{#partitionDO.type}}, \描述:{{#partitionDO.description}}, \场区拥有的场数量:{{#partitionDO.num}}, \场区状态:{PartitionStatusEnumParse{#partitionDO.type}};\结果:{{#_ret}}""",fail = "接口调用失败,失败原因:{{#_errorMsg}}",extra = "{{#partitionDO.toString()}}",operator = "{{T(com.vrs.common.context.UserContext).getUsername()}}"
)
public Result save(@Validated({AddGroup.class}) @RequestBody PartitionDO partitionDO) {partitionService.save(partitionDO);// 因为 ID 是存储到数据库中才生成的,@LogRecord 默认拿不到,需要我们将信息手动设置到上下文中LogRecordContext.putVariable("id", partitionDO.getId());return Results.success();
}

注意分区类型:{VenueTypeEnumParse{#partitionDO.type}}中使用了解析器的标识

重新运行之后,发现枚举类型已经转化了具体值

在这里插入图片描述

日志子类型划分

日志子类型划分为了区分日志的所属身份,比如说普通用户修改了数据,管理员也修改了数据。但通常指允许管理员查看用户的操作日志,不允许普通用户查看管理员的操作日志。因此可以使用subType字段来做一些区分,后面实现日志查询的时候,针对用户的身份对该字段做一些处理即可

@LogRecord(bizNo = "{{#id}}",type = "新增分区",subType = "{{T(com.vrs.common.context.UserContext).getUserType()}}",success = """场馆ID{{#partitionDO.venueId}}, \分区名称:{{#partitionDO.name}}, \分区类型:{VenueTypeEnumParse{#partitionDO.type}}, \描述:{{#partitionDO.description}}, \场区拥有的场数量:{{#partitionDO.num}}, \场区状态:{PartitionStatusEnumParse{#partitionDO.type}}; \结果:{{#_ret}}""",fail = "接口调用失败,失败原因:{{#_errorMsg}}",extra = "{{#partitionDO.toString()}}",operator = "{{T(com.vrs.common.context.UserContext).getUsername()}}"
)

日志过滤

只有在满足一定条件的时候,才记录日志,可以使用condition字段,比如说用户提交的数量为null,才记录日志

@LogRecord(bizNo = "{{#id}}",type = "新增分区",subType = "{{T(com.vrs.common.context.UserContext).getUserType()}}",success = """场馆ID{{#partitionDO.venueId}}, \分区名称:{{#partitionDO.name}}, \分区类型:{VenueTypeEnumParse{#partitionDO.type}}, \描述:{{#partitionDO.description}}, \场区拥有的场数量:{{#partitionDO.num}}, \场区状态:{PartitionStatusEnumParse{#partitionDO.type}}; \结果:{{#_ret}}""",fail = "接口调用失败,失败原因:{{#_errorMsg}}",extra = "{{#partitionDO.toString()}}",operator = "{{T(com.vrs.common.context.UserContext).getUsername()}}",condition = "{{#partitionDO.num == null}}"
)

日志持久化

数据库

DROP TABLE IF EXISTS `mt_biz_log`;
CREATE TABLE `mt_biz_log` (`id` bigint NOT NULL COMMENT 'ID',`create_time` datetime,`update_time` datetime,`is_deleted` tinyint default 0 COMMENT '逻辑删除 0:没删除 1:已删除',`tenant` varchar(50) DEFAULT NULL COMMENT '租户',`type` varchar(50) DEFAULT NULL COMMENT '类型',`sub_type` varchar(50) DEFAULT NULL COMMENT '子类型',`class_name` varchar(100) DEFAULT NULL COMMENT '方法名称',`method_name` varchar(100) DEFAULT NULL COMMENT '方法名称',`operator` varchar(50) DEFAULT NULL COMMENT '操作人员',`action` longtext COMMENT '操作',`extra` longtext COMMENT '其他补充',`status` tinyint DEFAULT NULL COMMENT '操作状态 (0正常 1异常)',PRIMARY KEY (`id`) USING BTREE
) COMMENT='操作日志表';

【实体类】

package com.vrs.entity;import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.vrs.domain.base.BaseEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;/*** 操作日志表* @TableName mt_biz_log*/
@TableName(value ="mt_biz_log")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MtBizLog extends BaseEntity implements Serializable {/*** 租户*/private String tenant;/*** 类型*/private String type;/*** 子类型*/private String subType;/*** 方法名称*/private String className;/*** 方法名称*/private String methodName;/*** 操作人员*/private String operator;/*** 操作*/private String action;/*** 其他补充*/private String extra;/*** 操作状态 (0正常 1异常)*/private Integer status;@TableField(exist = false)private static final long serialVersionUID = 1L;
}

增删改查方法我这里就不再介绍了,请大家自行实现

继承存储接口

只需要实现ILogRecordService接口,然后重写record方法,然后在该方法里面调用持久化方法即可

我这里统一将所有日志记录到一个表中,如果想要根据业务分表存储,可以根据logRecord.getTenant()logRecord.getType()来判断存储到哪个表即可

package com.vrs.service.impl;import com.mzt.logapi.beans.CodeVariableType;
import com.mzt.logapi.beans.LogRecord;
import com.mzt.logapi.service.ILogRecordService;
import com.vrs.entity.MtBizLog;
import com.vrs.service.MtBizLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.util.List;/*** @Author dam* @create 2024/12/15 21:02*/
@Slf4j
@Service
@RequiredArgsConstructor
public class BizlogStoreService implements ILogRecordService {private final MtBizLogService mtBizLogService;@Overridepublic void record(LogRecord logRecord) {mtBizLogService.save(MtBizLog.builder().tenant(logRecord.getTenant()).type(logRecord.getType()).subType(logRecord.getSubType()).className(logRecord.getCodeVariable().get(CodeVariableType.ClassName).toString()).methodName(logRecord.getCodeVariable().get(CodeVariableType.MethodName).toString()).operator(logRecord.getOperator()).action(logRecord.getAction()).extra(logRecord.getExtra()).status(logRecord.isFail() ? 1 : 0).build());}@Overridepublic List<LogRecord> queryLog(String bizNo, String type) {return null;}@Overridepublic List<LogRecord> queryLogByBizNo(String bizNo, String type, String subType) {return null;}
}

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com