当前位置: 首页 > news >正文

基于 mzt-biz-log 实现接口调用日志记录

🎯导读: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;}
}

相关文章:

基于 mzt-biz-log 实现接口调用日志记录

&#x1f3af;导读&#xff1a;mzt-biz-log 是一个用于记录操作日志的通用组件&#xff0c;旨在追踪系统中“谁”在“何时”对“何事”执行了“何种操作”。该组件通过简单的注解配置&#xff0c;如 LogRecord&#xff0c;即可实现接口调用的日志记录&#xff0c;支持成功与失败…...

docker容器的安装以及用法

1、了解docker 1.1、docker是什么 Docker 是一个开源的应用容器引擎&#xff0c;基于 Go 语言 并遵从 Apache2.0 协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中&#xff0c;然后发布到任何流行的 Linux 机器上&#xff0c;也可以实现…...

Java中的Consumer接口应该如何使用(通俗易懂图解)

应用场景&#xff1a; 第一次程序员A写好了个基础的遍历方法&#xff1a; public class Demo1 {public static void main(String[] args) {//假设main方法为程序员B写的,此时需要去调用A写好的一个遍历方法//1.如果此时B突然发现想将字符串以小写的形式打印出来&#xff0c;则…...

D102【python 接口自动化学习】- pytest进阶之fixture用法

day102 pytest的usefixtures方法 学习日期&#xff1a;20241219 学习目标&#xff1a;pytest基础用法 -- pytest的usefixtures方法 学习笔记&#xff1a; fixture调用方法 实际应用 总结 pytest.mark.usefixtures(func)&#xff0c;pytest的usefixtures方法&#xff0c;无…...

从零玩转CanMV-K230(4)-小核Linux驱动开发参考

前言 K230 芯片是一款基于 RISC-V 架构的端侧 AIoT 芯片&#xff0c;包含两个核心&#xff1a; CPU 1: RISC-V 处理器&#xff0c;1.6GHz&#xff0c;32KB I-cache, 32KB D-cache, 256KB L2 Cache&#xff0c;128bit RVV 1.0扩展 CPU 0: RISC-V 处理器&#xff0c;0.8GHz&am…...

上汽大通汽车CAN数据解析开发服务及技术商用领域详细解析

上汽大通G90是一款集豪华、科技与舒适于一身的中大型MPV&#xff0c;号称“国产埃尔法”。在国内市场&#xff0c;作为“卷王”的G90主要面向中大型MPV市场&#xff0c;满足家庭出行、商务接待和客运租赁等多元化场景需求&#xff0c;在国内市场上取得了不错的销售成绩。在海外…...

基于SCUI的后台管理系统

一、SCUI Admin 官方地址&#xff1a;https://python-abc.xyz/scui-doc/ 高性能中后台前端解决方案&#xff0c;基于 Vue3、elementPlus 持续性的提供独家组件和丰富的业务模板帮助你快速搭建企业级中后台前端任务。 预览地址&#xff1a;https://python-abc.xyz/scui-doc/de…...

使用频谱分析仪:RBW,Res BW,分辨率带宽;Sweep,扫描;noise floor,底噪,如何降低底噪?

RBW与Sweep的定义及其特性阐述&#xff1a; Res BW&#xff0c;即Resolution Bandwidth&#xff08;分辨率带宽&#xff09;&#xff0c;是衡量仪器分辨信号细节能力的重要参数。当RBW的数值越小&#xff0c;意味着像素点的尺寸更为精细&#xff0c;从而能够观察到更为细微的信…...

项目管理工具Maven(一)

Maven的概念 什么是Maven 翻译为“专家”&#xff0c;“内行”Maven是跨平台的项目管理工具。主要服务于基于Java平台的项目构建&#xff0c;依赖管理和项目信息管理。什么是理想的项目构建&#xff1f; 高度自动化&#xff0c;跨平台&#xff0c;可重用的组件&#xff0c;标准…...

阿里云ESC服务器一次性全部迁移到另一个ESC

摘要&#xff1a; 在云计算时代&#xff0c;服务器迁移是企业优化资源配置、提升业务灵活性的常见需求。本文将详细介绍如何将阿里云ECS&#xff08;Elastic Compute Service&#xff09;服务器一次性迁移到另一个ECS实例。整个迁移过程分为四个关键步骤&#xff1a; 创建自定义…...

搭建分布式Kafka集群

title: 搭建分布式Kafka集群 date: 2024-12-1 14:00:00 categories: - 服务器 tags: - Kafka - 大数据搭建分布式Kafka集群 在主节点上安装Kafka&#xff1b; Kafka使用Zookeeper服务器来存储元数据信息 本次实验环境&#xff1a;Centos 7-2009、Hadoop-3.1.4、JDK 8、Zookeep…...

【后端面试总结】深入解析进程和线程的区别

在操作系统和并发编程中&#xff0c;进程和线程是两个核心概念。它们各自承担着不同的职责&#xff0c;并在多任务处理中发挥着关键作用。本文将从定义、特性、应用场景以及优缺点等多个方面对进程和线程进行详细对比&#xff0c;帮助读者深入理解它们之间的区别。 一、进程和…...

java版电子招投标采购|投标|评标|竞标|邀标|评审招投标系统源码

招投标管理系统是一款适用于招标代理、政府采购、企业采购和工程交易等领域的企业级应用平台。该平台以项目为主线&#xff0c;从项目立项到项目归档&#xff0c;实现了全流程的高效沟通和协作。通过该平台&#xff0c;用户可以实时共享项目数据信息&#xff0c;实现规范化管理…...

SSM 赋能 Vue 助力:新锐台球厅管理系统的设计与实现的辉煌之路

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常适…...

C++ 并发专题 - C++线程同步的几种方法

一&#xff1a;概述 线程同步是多线程编程中的一个重要概念&#xff0c;它用于控制多个线程之间对共享资源的访问&#xff0c;避免竞态条件&#xff08;race condition&#xff09;和数据不一致的问题。线程同步确保在多线程环境中&#xff0c;多个线程访问共享数据时能够按照某…...

使用Python脚本进行编写批量根据源IP进行查询的语句用于态势感知攻击行为的搜索

使用Python脚本进行编写批量根据源IP进行查询的语句 以下根据ip-list集里面的IP地址&#xff08;可以自行扩充&#xff09;&#xff0c;然后采用srcaddress "{ip}" or 的形式进行打印并存储在路径为&#xff1a;桌面的IOC结果.txt --------------------------代码如…...

Python中的zip/unzip:像拉拉链一样组合数据的艺术

今天让我们一起探讨Python中一个优雅而强大的内置功能&#xff1a; zip 和 unzip 。听名字就知道&#xff0c;它就像我们衣服上的拉链一样&#xff0c;能把两边的数据完美地咬合在一起。 从一个有趣的例子开始 想象你正在开发一个班级管理系统。每个学生都有名字、成绩和评语…...

数电课设·简易数字钟(Quartus Ⅱ)

忽如一夜春风来&#xff0c;千树万树梨花开 —— 《白雪歌诵武判官归京》 岑参 【唐】 目录 简易数字钟 要点剖析&#xff1a; 逐步分析&#xff1a; 端口说明&#xff1a; 代码展示&#xff1a; 分部解释&#xff1a; 代码编译结果&#xff1a; 提醒 &#xff1a; …...

大模型中RAG模型的检索过程是如何实现的?(附最佳实践资料)

RAG模型的检索过程主要涉及以下几个步骤&#xff1a; 向量化&#xff08;Embedding&#xff09;&#xff1a;首先&#xff0c;需要将外部知识库中的文档转换为计算机能够理解的向量形式。这一步骤通常使用预训练的嵌入模型&#xff08;如BERT、GPT等&#xff09;将文本转换为高…...

python:用 sklearn.metrics 评价 K-Means 聚类模型

sklearn 的 metrics 模块提供的聚类模型评价指标如下&#xff1a; ARI 评价法&#xff08;兰德系数&#xff09;: adjusted_rand_score AMI 评价法&#xff08;相互信息&#xff09;: adjusted_mutual_info_score V-measure 评分 : completeness_score FMI 评价法 : fowlkes_m…...

eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)

说明&#xff1a; 想象一下&#xff0c;你正在用eNSP搭建一个虚拟的网络世界&#xff0c;里面有虚拟的路由器、交换机、电脑&#xff08;PC&#xff09;等等。这些设备都在你的电脑里面“运行”&#xff0c;它们之间可以互相通信&#xff0c;就像一个封闭的小王国。 但是&#…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战

前言 现在我们有个如下的需求&#xff0c;设计一个邮件发奖的小系统&#xff0c; 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式&#xff08;Decorator Pattern&#xff09;允许向一个现有的对象添加新的功能&#xff0c;同时又不改变其…...

K8S认证|CKS题库+答案| 11. AppArmor

目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作&#xff1a; 1&#xff09;、切换集群 2&#xff09;、切换节点 3&#xff09;、切换到 apparmor 的目录 4&#xff09;、执行 apparmor 策略模块 5&#xff09;、修改 pod 文件 6&#xff09;、…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

智慧工地云平台源码,基于微服务架构+Java+Spring Cloud +UniApp +MySql

智慧工地管理云平台系统&#xff0c;智慧工地全套源码&#xff0c;java版智慧工地源码&#xff0c;支持PC端、大屏端、移动端。 智慧工地聚焦建筑行业的市场需求&#xff0c;提供“平台网络终端”的整体解决方案&#xff0c;提供劳务管理、视频管理、智能监测、绿色施工、安全管…...

让AI看见世界:MCP协议与服务器的工作原理

让AI看见世界&#xff1a;MCP协议与服务器的工作原理 MCP&#xff08;Model Context Protocol&#xff09;是一种创新的通信协议&#xff0c;旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天&#xff0c;MCP正成为连接AI与现实世界的重要桥梁。…...

【HTTP三个基础问题】

面试官您好&#xff01;HTTP是超文本传输协议&#xff0c;是互联网上客户端和服务器之间传输超文本数据&#xff08;比如文字、图片、音频、视频等&#xff09;的核心协议&#xff0c;当前互联网应用最广泛的版本是HTTP1.1&#xff0c;它基于经典的C/S模型&#xff0c;也就是客…...

【论文阅读28】-CNN-BiLSTM-Attention-(2024)

本文把滑坡位移序列拆开、筛优质因子&#xff0c;再用 CNN-BiLSTM-Attention 来动态预测每个子序列&#xff0c;最后重构出总位移&#xff0c;预测效果超越传统模型。 文章目录 1 引言2 方法2.1 位移时间序列加性模型2.2 变分模态分解 (VMD) 具体步骤2.3.1 样本熵&#xff08;S…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...