1.简介/定位
该组件主要定位是提供通用和个性化的 记录操作日志 功能,接入简单、可拔插、可扩展、不和业务逻辑耦合。
2.背景
系统没有进操作日志的记录(例如:某个人在xx时间进行了xx功能的操作,产生了xx结果。或者其他个性化的日志需求)。
又由于每个业务系统都有记录操作日志的需求,如果都单独开发一套操作日志记录功能,耗时耗力。
故进行该公共组件的抽取实现。
3.技术选型
- SpringBoot(aop)
- Spring (SPEL表达式)
- RocketMQ(异步解耦消峰)
- MongoDB(日志数据存储)
4.设计思路
4.1 操作日志记录内容
/**
* 操作用户id
*/
private Long userId;
/**
* 操作用户名称
*/
private String username;
/**
* 操作用户账号
*/
private String account;
/**
* 操作类型(增删查改)
*/
private String operateType;
/**
* 操作功能简述(例如:新增员工)
*/
private String description;
/**
* 操作结果(成功或者失败)
*/
private String operateResult;
/**
* 操作详情(例如:员工名称不能为空)
*/
private String detail;
/**
* 操作时间
*/
private Date operateTime;
/**
* 操作应用
*/
private String appName;
/**
* 机构id
*/
private Long orgId;
/**
* 租户id
*/
private Long tenantId;
/**
* 备注
*/
private String remark;
4.2 操作日志实现思路
项目启动效果
以下是默认版本实现流程。如果不想让日志存入公共的 mogodb 库中,可以自己扩展实现
实现思路
4.3 业务系统接入方式
1.引入依赖:
<dependency>
<groupId>com.gwx</groupId>
<artifactId>gwx-operatelog-starter</artifactId>
<version>1.0.1-SNAPSHOT</version>
</dependency>
2.增加yml配置
gwx-operatelog:
enable: true
platform: exmaple
banner: true
rocketmq:
#rocketmq服务器地址
name-server-addr:
#rocketmq生产者组
producer-group:
3.实现IOperatorService接口,放入当前操作人信息:
/**
* 获取操作者
*/
public interface IOperatorService {
/**
* 当前用户id
*
* @return
*/
Long getUserId();
/**
* 当前用户名称
*
* @return
*/
String getUserName();
/**
* 当前用户账号
*
* @return
*/
String getAccount();
/**
* 获取当前操作者
*
* @return
*/
Long getOrgId();
/**
* 当前租户
*
* @return
*/
Long getTenantId();
}
示例:
public class DefaultOperatorServiceImpl implements IOperatorService {
@Override
public Long getTenantId() {
return 0L;
}
@Override
public Long getUserId() {
return 0L;
}
@Override
public String getUserName() {
return "gwx";
}
@Override
public String getAccount() {
return "123";
}
@Override
public Long getOrgId() {
return 0L;
}
}
4.使用:接口方法增加注解
@OperateLog(description = "测试", operateType = OperateTypeEnum.QUERY, success = "测试 {{#name}}",appName = AppEnum.SAAS)
@GetMapping("/test")
public String test(@RequestParam String name) {
return name;
}
5.扩展点
5.1日志数据个性化处理
当然:如果你想对操作日志做个性化处理存储,该组件也提供了扩展接口:【ILogSaveService】
/**
* 保存日志接口
*/
public interface ILogSaveService {
/**
* 保存日志
* @param operateLogDTO
*/
void save(OperateLogDTO operateLogDTO);
}
以下是默认实现类:
让操作日志放入rocketmq,日志服务会自动进行拉取消费入库mogodb
@Slf4j
public class DefaultLogSaveServiceImpl implements ILogSaveService {
@Resource
private MqProducer mqProducer;
@Override
public void save(OperateLogDTO operateLogDTO) {
try {
mqProducer.asyncSend(JSON.toJSONString(operateLogDTO),"gwx-operate-log-topic", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("-->gwx-operatelog操作日志消息发送成功:{}", JSON.toJSONString(sendResult));
}
@Override
public void onException(Throwable throwable) {
log.error("-->gwx-operatelog操作日志消息发送失败", throwable.getMessage());
}
}, 15000L);
}catch (Exception e){
log.error("-->gwx-operatelog操作日志消息发送异常", e);
}
}
}
5.2动态日志
如果你想记录的日志是动态日志:例如我们要记录这样一条日志:员工名称由xx改为xx。
5.2.1 你需要实现自定义函数接口:
/**
* 自定义函数接口
*
* @Author guwx
* @Date 2022-06-06
*/
public interface IPraseFunction {
/**
* 自定义函数是否在业务代码执行之前解析
*
* @return
*/
default boolean executeBefore(){
return false;
};
/**
* 自定义函数名称
*
* @return
*/
String functionName();
/**
* 执行
* @param param
* @return
*/
String apply(String param);
}
获取旧员工名称函数例子:
/**
* 获取修改前的员工名称
* @author gwx
* @date 2022/06/06 14:38
*/
@Component
public class GetBeforeStaffNameByStaffId implements IPraseFunction {
@Override
public boolean executeBefore() {
return true;
}
@Override
public String functionName() {
return "getBeforeStaffNameByStaffId";
}
@Override
public String apply(String value) {
//此处可以写根据staffId查询stffName的逻辑代码
return value;
}
}
获取新员工名称函数例子:
/**
* 获取修改后的员工名称
* @author gwx
* @date 2022/06/06 14:38
*/
@Component
public class GetAfterStaffName implements IPraseFunction {
@Override
public boolean executeBefore() {
return false;
}
@Override
public String functionName() {
return "getAfterStaffName";
}
@Override
public String apply(String value) {
//获取新员工名称逻辑
return value;
}
}
5.2.2 使用方式
@OperateLog(description = "修改员工", operateType = OperateTypeEnum.QUERY,
success = "员工名称由【{getBeforeStaffNameByStaffId{#userDto.id}}】更改为【{getAfterStaffName{#userDto.name}}】",
condition = "{{#userDto.name == 'oplog'}}",appName = AppEnum.SAAS)
@PostMapping("/test")
public String test(@RequestBody UserDto userDto) {
// 代码中修改了名称,getAfterStaffName可以拿到修改后的名称
userDto.setName(userDto.getName() + "新");
return userDto.getName();
}