作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
有时候我们所谓的“记录一下日志”,可能有两种含义:
一般来说,前者的数据被保存在log文件中,只是面向开发者,普通用户不易阅读,而后者被称为“用户行为日志”,通常还会额外提供查询接口和专门的页面,方便管理员查看操作记录。今天,我们就来聊聊所谓的“用户行为日志”。
之前已经写过很多AOP和枚举的代码了,这里就不再重复介绍,直接展示代码(MyBatis-Plus):
CREATE TABLE `sys_user_log` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`module_code` varchar(255) NOT NULL DEFAULT '',
`type` tinyint(2) NOT NULL,
`title` varchar(255) NOT NULL DEFAULT '',
`operator_id` bigint(20) NOT NULL,
`operate_time` datetime NOT NULL,
`content` varchar(255) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8mb4;
@Data
@TableName("sys_user_log")
public class UserLogDO {
private Long id;
/**
* 本次操作的系统模块
*
* @see com.bravo.demo.aspect.ModuleEnum
*/
private String moduleCode;
/**
* 操作类型
*
* @see com.bravo.demo.aspect.OperationEnum
*/
private Integer type;
/**
* 标题
*/
private String title;
/**
* 操作人
*/
private Long operatorId;
/**
* 操作时间
*/
private Date operateTime;
/**
* 操作内容
*/
private String content;
}
@Data
public class UserLogDTO {
private Long id;
private String moduleCode;
private Integer type;
private String title;
private Long operatorId;
private Date operateTime;
private String content;
}
public interface UserLogService {
/**
* 插入用户操作日志
*
* @param userLogDTO
* @return
*/
Boolean addSysLog(UserLogDTO userLogDTO);
}
@Service("UserLogService")
public class UserLogServiceImpl implements UserLogService {
@Autowired
private UserLogMapper userLogMapper;
@Override
public boolean addSysLog(UserLogDTO userLogDTO) {
UserLogDO userLogDO = new UserLogDO();
userLogDO.setModuleCode(userLogDTO.getModuleCode());
userLogDO.setType(userLogDTO.getType());
userLogDO.setTitle(userLogDTO.getTitle());
userLogDO.setOperatorId(userLogDTO.getOperatorId());
userLogDO.setOperateTime(userLogDTO.getOperateTime());
userLogDO.setContent(userLogDTO.getContent());
return userLogMapper.insert(userLogDO) > 0;
}
}
public interface UserLogMapper extends BaseMapper<UserLogDO> {
}
/**
* 用户操作日志注解
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface UserLog {
/**
* 所属模块名
*/
ModuleEnum module();
/**
* 操作标题
*/
String title();
/**
* 操作类型
*/
OperationEnum type();
}
/**
* 开启用户日志记录(还需要在相关方法上添加{@link UserLog})
*
* @author sunting
* @date 2021-02-11 14:11
*/
@Import(UserLogAspect.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface EnableUserLog {
}
/**
* 操作类型枚举
*/
@Getter
public enum OperationEnum {
/**
* 新建
*/
ADD(1, "新建"),
/**
* 修改
*/
MODIFY(2, "修改"),
/**
* 删除
*/
DELETE(3, "删除"),
/**
* 导入
*/
IMPORT(4, "导入"),
/**
* 导出
*/
EXPORT(5, "导出");
private final Integer value;
private final String operationType;
OperationEnum(Integer value, String operationType) {
this.value = value;
this.operationType = operationType;
}
}
/**
* 系统模块枚举类
* 可以根据自己系统的实际情况增添模块
*/
@Getter
public enum ModuleEnum {
/**
* 课程
*/
COURSE("课程"),
/**
* 用户
*/
USER("用户"),
/**
* 消息
*/
MESSAGE("消息");
private final String moduleCode;
ModuleEnum(String moduleCode) {
this.moduleCode = moduleCode;
}
}
/**
* 操作日志注解切面实现
*/
@Slf4j
@Aspect
public class UserLogAspect {
// 这次不用RequestContextHolder了,改成直接注入
@Autowired
private HttpServletRequest request;
@Autowired
private ObjectMapper objectMapper;
@Autowired
UserLogService userLogService;
@Pointcut("@annotation(com.bravo.demo.aspect.UserLog)")
public void pointcut() {
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint point) {
saveSysUserLog(point);
}
private void saveSysUserLog(JoinPoint point) {
// 获取当前登录用户
UserInfoDTO userInfoDTO = getUserInfoDTO();
// 目标方法、以及方法上的@UserLog注解
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
UserLog userLogAnnotation = method.getAnnotation(UserLog.class);
if (userLogAnnotation == null) {
return;
}
// 收集相关信息并保存
UserLogDTO userLogDTO = new UserLogDTO();
userLogDTO.setModuleCode(userLogAnnotation.module().getModuleCode());
userLogDTO.setContent(getContentJson(point));
userLogDTO.setTitle(userLogAnnotation.title());
userLogDTO.setOperatorId(userInfoDTO.getId());
userLogDTO.setOperateTime(new Date());
userLogDTO.setType(userLogAnnotation.type().getValue());
userLogService.addSysLog(userLogDTO);
}
private UserInfoDTO getUserInfoDTO() {
// UserInfoDTO userInfoDTO = (UserInfoDTO) ThreadLocalMap.get(WebConst.USER_INFO_DTO);
// 模拟从ThreadLocal获取用户信息,关于ThreadLocal请参考小册相关章节
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setId(10086L);
return userInfoDTO;
}
private String getContentJson(JoinPoint point) {
String requestType = request.getMethod();
if ("GET".equals(requestType)) {
// 如果是GET请求,直接返回QueryString(目前没有针对查询操作进行日志记录,先留着吧)
return request.getQueryString();
}
Object[] args = point.getArgs();
Object[] arguments = new Object[args.length];
for (int i = 0; i < args.length; i++) {
// 只打印客户端传递的参数,排除Spring注入的参数,比如HttpServletRequest
if (args[i] instanceof ServletRequest
|| args[i] instanceof ServletResponse
|| args[i] instanceof MultipartFile) {
continue;
}
arguments[i] = args[i];
}
try {
return objectMapper.writeValueAsString(arguments);
} catch (JsonProcessingException e) {
log.error("UserLogAspect#getContentJson JsonProcessingException", e);
}
return "";
}
}
@Slf4j
@RestController
public class UserController {
@UserLog(module = ModuleEnum.USER, title = "批量更新用户", type = OperationEnum.MODIFY)
@PostMapping("updateBatchUser")
public Result<Boolean> updateBatchUser(@Validated @RequestBody ValidationList<User> userList) {
return Result.success(null);
}
@UserLog(module = ModuleEnum.USER, title = "新增用户", type = OperationEnum.ADD)
@PostMapping("insertUser")
public Result<Boolean> insertUser(@RequestBody User user) {
return Result.success(null);
}
}
上面的代码主要提供一个思路,大家可以根据实际需求扩展或改编。
注意:
本文的处理是把所有参数都转为JSON存储,遇到保存文章等大文本数据时会报错。处理办法是:代码中进行长度处理或者调大content的长度(也可以改为text)
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬
?