package com.jxyunge.sign.aspect;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.NullNode;
import com.jxyunge.sign.enums.sys.OperationTypeEnum;
import com.jxyunge.sign.mybatis.entity.sys.Admin;
import com.jxyunge.sign.mybatis.entity.sys.SysLog;
import com.jxyunge.sign.mybatis.service.sys.AdminService;
import com.jxyunge.sign.mybatis.service.sys.EntityService;
import com.jxyunge.sign.mybatis.service.sys.SysLogService;
import com.jxyunge.sign.request.input.common.PageParam;
import com.jxyunge.sign.tools.IpHelper;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
* @author lgh
*/
@Slf4j
@Aspect
@AllArgsConstructor
@Component
public class SysLogAspect {
private final AdminService adminService;
private final SysLogService sysLogService;
private final EntityService entityService;
private ObjectMapper objectMapper;
@Around("@annotation(sysLogAnnotation)")
public Object logAround(ProceedingJoinPoint joinPoint, com.jxyunge.sign.annotation.SysLog sysLogAnnotation) throws Throwable {
log.info("开始执行日志记录");
long beginTime = System.currentTimeMillis();
OperationTypeEnum operationType = sysLogAnnotation.type();
// 获取旧数据
Object oldEntity = getOldEntity(joinPoint, sysLogAnnotation);
// 执行目标方法
Object result = joinPoint.proceed();
long time = System.currentTimeMillis() - beginTime;
// 获取新数据
Object newEntity = extractEntityFromResponse(result);
// 构建日志记录
SysLog sysLog = buildSysLog(joinPoint, sysLogAnnotation, time, oldEntity, newEntity, operationType);
// 异步保存日志
CompletableFuture.runAsync(() -> sysLogService.save(sysLog));
return result;
}
private Object getOldEntity(ProceedingJoinPoint joinPoint, com.jxyunge.sign.annotation.SysLog annotation) {
if (annotation.type() == OperationTypeEnum.CREATE) return null;
try {
Object idValue = getArgValue(joinPoint, annotation.idParam());
if (idValue == null) return null;
// 必须指定 entityClass,否则无法获取完整实体
if (annotation.entityClass() == null || annotation.entityClass() == Object.class) {
log.warn("SysLog 注解中未指定 entityClass,无法获取旧实体数据");
return null;
}
return entityService.getById(annotation.entityClass(), idValue.toString());
} catch (Exception e) {
log.error("获取旧实体失败", e);
return null;
}
}
private Object getArgValue(ProceedingJoinPoint joinPoint, String paramName) {
if (StringUtils.isBlank(paramName)) return null;
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
Object[] args = joinPoint.getArgs();
// 支持嵌套属性,如"entity.id"
if (paramName.contains(".")) {
String[] parts = paramName.split("\\.");
String rootParam = parts[0];
Object rootObj = null;
for (int i = 0; i < paramNames.length; i++) {
if (rootParam.equals(paramNames[i])) {
rootObj = args[i];
break;
}
}
if (rootObj == null) return null;
// 递归获取嵌套属性
try {
for (int i = 1; i < parts.length; i++) {
String fieldName = parts[i];
Field field = findField(rootObj.getClass(), fieldName);
if (field == null) {
log.warn("字段 {} 在类 {} 及其父类中未找到", fieldName, rootObj.getClass().getName());
rootObj = null;
break;
}
field.setAccessible(true);
rootObj = field.get(rootObj);
if (rootObj == null) break;
}
return rootObj;
} catch (Exception e) {
log.error("获取嵌套参数失败: {}", paramName, e);
return null;
}
} else {
// 原有逻辑,直接匹配参数名
for (int i = 0; i < paramNames.length; i++) {
if (paramName.equals(paramNames[i])) {
return args[i];
}
}
return null;
}
}
private Field findField(Class<?> clazz, String fieldName) {
while (clazz != null) {
try {
return clazz.getDeclaredField(fieldName);
} catch (NoSuchFieldException e) {
clazz = clazz.getSuperclass();
}
}
return null;
}
private Object extractEntityFromResponse(Object result) {
if (result == null) return null;
// 1. 尝试从通用响应结构获取
try {
Method getData = result.getClass().getMethod("getData");
Object data = getData.invoke(result);
if (data != null) return data;
} catch (NoSuchMethodException ignored) {
} catch (Exception e) {
log.warn("getData调用异常", e);
}
// 2. 尝试直接访问data字段
try {
Field dataField = result.getClass().getDeclaredField("data");
dataField.setAccessible(true);
Object data = dataField.get(result);
if (data != null) return data;
} catch (NoSuchFieldException ignored) {
} catch (Exception e) {
log.warn("data字段访问异常", e);
}
// 3. 返回原始结果
return result;
}
private SysLog buildSysLog(ProceedingJoinPoint joinPoint,
com.jxyunge.sign.annotation.SysLog annotation,
long time,
Object oldEntity,
Object newEntity,
OperationTypeEnum operationType) {
SysLog sysLog = new SysLog();
// 1. 设置基础字段(原有逻辑)
// 注解描述
sysLog.setOperation(annotation.value());
// 请求方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
sysLog.setMethod(className + "." + methodName + "()");
// 请求参数
Object[] args = joinPoint.getArgs();
if(args != null && args.length > 0) {
String params = null;
try {
if(args.length > 0 && args[0] != null &&
Objects.equals(args[0].getClass().getName(), PageParam.class.getName())){
PageParam<Object> pageParam = (PageParam<Object>) args[0];
List<Object> records = pageParam.getRecords();
List<Object> filteredRecords = filterSerializableObjects(records);
params = objectMapper.writeValueAsString(filteredRecords);
} else {
Object[] filteredArgs = filterSerializableObjects(args);
params = objectMapper.writeValueAsString(filteredArgs);
}
} catch (Exception e) {
params = getSafeParamRepresentation(args);
log.warn("参数序列化失败: {}", e.getMessage());
}
sysLog.setParams(params);
}
// IP地址
sysLog.setIp(IpHelper.getIpAddr());
// 业务ID(从新实体中提取)
String businessId = extractIdFromResult(newEntity != null ? newEntity : oldEntity);
if (businessId != null) {
sysLog.setBizId(businessId);
}
// 用户名
Admin admin = adminService.getAdminFromReq();
if (!Objects.isNull(admin)) {
sysLog.setUsername(admin.getUsername());
}
// 执行时间和创建时间
sysLog.setTime(time);
sysLog.setCreateDate(new Date());
// 2. 新增操作日志字段
sysLog.setOperationType(OperationTypeEnum.valueOf(operationType.name()));
sysLog.setBeforeData(toJsonSafe(oldEntity));
sysLog.setAfterData(toJsonSafe(newEntity));
sysLog.setChangeDetails(compareChanges(oldEntity, newEntity));
return sysLog;
}
private String toJsonSafe(Object obj) {
if (obj == null) return null;
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
return "序列化失败: " + e.getMessage();
}
}
private String compareChanges(Object oldObj, Object newObj) {
if (oldObj == null && newObj == null) return "[]";
try {
JsonNode oldTree = oldObj != null ? objectMapper.valueToTree(oldObj) : objectMapper.createObjectNode();
JsonNode newTree = newObj != null ? objectMapper.valueToTree(newObj) : objectMapper.createObjectNode();
List<Map<String, Object>> changes = new ArrayList<>();
Set<String> allFields = new HashSet<>();
oldTree.fieldNames().forEachRemaining(allFields::add);
newTree.fieldNames().forEachRemaining(allFields::add);
for (String field : allFields) {
JsonNode oldVal = oldTree.get(field);
JsonNode newVal = newTree.get(field);
if (oldVal == null) oldVal = NullNode.getInstance();
if (newVal == null) newVal = NullNode.getInstance();
if (!oldVal.equals(newVal)) {
// 使用 HashMap 替代 Map.of,避免 null 值导致的 NullPointerException
Map<String, Object> change = new HashMap<>();
change.put("field", field != null ? field : "null");
// 处理可能的 null 值
String oldText = oldVal.isNull() ? null : (oldVal.isTextual() ? oldVal.asText() : oldVal.toString());
String newText = newVal.isNull() ? null : (newVal.isTextual() ? newVal.asText() : newVal.toString());
change.put("oldValue", oldText);
change.put("newValue", newText);
changes.add(change);
}
}
return objectMapper.writeValueAsString(changes);
} catch (Exception e) {
log.error("比较变更详情时发生异常: {}", e.getMessage(), e); // 更详细的异常记录
return String.format("[{\"error\":\"%s\"}]", e.getMessage());
}
}
private String extractIdFromResult(Object result) {
if (result == null) return null;
// 尝试从常见ID字段获取
String[] idFieldNames = {"id", "Id", "ID", "uuid", "key"};
for (String fieldName : idFieldNames) {
try {
Field field = result.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(result);
if (value != null) return value.toString();
} catch (NoSuchFieldException ignored) {
} catch (Exception e) {
log.warn("ID字段[{}]访问异常", fieldName, e);
}
}
// 尝试通过Getter方法获取
try {
Method getId = result.getClass().getMethod("getId");
Object idValue = getId.invoke(result);
if (idValue != null) return idValue.toString();
} catch (NoSuchMethodException ignored) {
} catch (Exception e) {
log.warn("getId方法调用异常", e);
}
return null;
}
// 过滤可序列化对象的方法
private List<Object> filterSerializableObjects(List<Object> objects) {
if (objects == null) return new ArrayList<>();
return objects.stream()
.filter(this::isSerializable)
.collect(Collectors.toList());
}
private Object[] filterSerializableObjects(Object[] objects) {
if (objects == null) return new Object[0];
return Arrays.stream(objects)
.filter(this::isSerializable)
.toArray();
}
// 检查对象是否可序列化
private boolean isSerializable(Object obj) {
if (obj == null) return true;
try {
// 尝试快速判断是否为常见不可序列化类型
Class<?> clazz = obj.getClass();
String className = clazz.getName();
// 过滤常见的不可序列化类
if (className.contains("java.util.Collections$") ||
className.contains("HttpServletRequest") ||
className.contains("HttpServletResponse") ||
className.contains("Session") ||
className.contains("ServletContext") ||
className.startsWith("org.apache.catalina") ||
className.startsWith("org.springframework.security")) {
return false;
}
// 简单测试序列化
objectMapper.writeValueAsString(obj);
return true;
} catch (Exception e) {
return false;
}
}
// 获取安全的参数表示
private String getSafeParamRepresentation(Object[] args) {
if (args == null || args.length == 0) {
return "[]";
}
List<String> paramInfos = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
if (args[i] != null) {
String className = args[i].getClass().getSimpleName();
paramInfos.add("arg" + i + ": " + className);
} else {
paramInfos.add("arg" + i + ": null");
}
}
try {
return objectMapper.writeValueAsString(paramInfos);
} catch (Exception e) {
return paramInfos.toString();
}
}
}
package com.jxyunge.sign.annotation;
import com.jxyunge.sign.enums.sys.OperationTypeEnum;
import java.lang.annotation.*;
/**
* 自定义日志注解
* @author linyan
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
String value() default "";
OperationTypeEnum type() default OperationTypeEnum.UPDATE; // 操作类型
// 新增ID参数名(用于获取旧数据)
String idParam() default "";
// 新增实体类参数
Class<?> entityClass() default Object.class;
}
package com.jxyunge.sign.mybatis.entity.sys;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.jxyunge.sign.annotation.SearchParam;
import com.jxyunge.sign.enums.sys.OperationTypeEnum;
import com.jxyunge.sign.enums.sys.SearchConditionEnum;
import com.jxyunge.sign.mybatis.entity.BaseEntity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
import java.util.List;
/**
* 系统日志
* @author zanmall
*/
@Data
@TableName("tb_sys_log")
public class SysLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@SearchParam(name = {"主键"},condition = {SearchConditionEnum.eq})
@TableId(type = IdType.ASSIGN_UUID)
private String id;
/**
* 用户名
*/
private String username;
/**
* 用户操作
*/
private String operation;
/**
* 请求方法
*/
private String method;
/**
* 请求参数
*/
private String params;
/**
* 执行时长(毫秒)
*/
private Long time;
/**
* IP地址
*/
private String ip;
/**
* 创建时间
*/
private Date createDate;
/**
* 业务id
*/
private String bizId;
// 新增字段
private OperationTypeEnum operationType; // 操作类型
private String beforeData; // 变更前数据(JSON)
private String afterData; // 变更后数据(JSON)
private String changeDetails; // 变更详情(JSON)
}
// 文件路径: D:\develop-tools\code\ideaworkplace\zhongbang-scm\zhongbang-scm-service\src\main\java\com\jxyunge\sign\mybatis\service\impl\sys\EntityServiceImpl.java
package com.jxyunge.sign.mybatis.service.impl.sys;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jxyunge.sign.mybatis.entity.scm.sys.Order;
import com.jxyunge.sign.mybatis.service.sys.EntityService;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* @author xiaoda
* @create 2025-07-30 10:38
*/
@Service
public class EntityServiceImpl implements EntityService, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public <T> T getById(Class<T> entityClass, Serializable id) {
try {
// 尝试多种方式获取 Mapper
BaseMapper<T> mapper = findMapperForEntity(entityClass);
if (mapper != null) {
return mapper.selectById(id);
}
throw new RuntimeException("未找到处理实体 " + entityClass.getSimpleName() + " 的 Mapper");
} catch (Exception e) {
throw new RuntimeException("获取实体失败: " + entityClass.getSimpleName() + ", ID: " + id, e);
}
}
@Override
public Object getById(String id) {
// 对于没有前缀的普通ID,我们无法自动识别实体类型
// 这种情况应该始终通过带 entityClass 参数的 getById 方法来调用
throw new UnsupportedOperationException("无法通过普通ID自动识别实体类型,请使用 getById(Class<T> entityClass, Serializable id) 方法");
}
/**
* 查找处理指定实体的 Mapper
*/
private <T> BaseMapper<T> findMapperForEntity(Class<T> entityClass) {
// 尝试多种可能的命名方式,按照项目实际命名规则排序
String[] possibleNames = getPossibleMapperNames(entityClass);
for (String mapperBeanName : possibleNames) {
if (applicationContext.containsBean(mapperBeanName)) {
try {
return applicationContext.getBean(mapperBeanName, BaseMapper.class);
} catch (Exception ignored) {
// 如果获取失败,继续尝试下一个名称
}
}
}
return null;
}
/**
* 获取可能的 Mapper Bean 名称(按照项目实际命名规则排序)
*/
private String[] getPossibleMapperNames(Class<?> entityClass) {
String simpleName = entityClass.getSimpleName();
return new String[] {
"sys" + simpleName + "Mapper", // sys前缀: sysOrderMapper, sysGoodsMapper
Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1) + "Mapper", // 首字母小写: orderMapper, goodsMapper
simpleName.toLowerCase() + "Mapper", // 全小写: ordermapper, goodsmapper
"tb" + simpleName + "Mapper", // tb前缀: tbOrderMapper, tbGoodsMapper
simpleName + "Mapper" // 原样: OrderMapper, GoodsMapper
};
}
// 简单实体类(如果需要的话,但建议移除)
private static class SimpleEntity {
private String id;
public SimpleEntity(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
}
@SysLog(value = "修改订单", type = OperationTypeEnum.UPDATE,idParam = "entity.id",entityClass = Order.class)
@PostMapping(value = "/updateOrder")
@Operation(summary = "修改订单")
public ApiOutput<OrderAndDetailsOutput> updateOrder(HttpServletRequest request, @RequestBody OrderAddInput entity) {
Admin admin = getAdminFromReq(request);
if (BeanUtil.isEmpty(entity)){
return ApiOutput.err(ErrorCodeEnum.PARAMETER_IS_EMPTY);
}
OrderAndDetailsOutput save = sysOrderService.updateOrder(entity,admin);
return ApiOutput.ok(save);
}
这些是我的操作日志实现相关的代码,可以查看新增,删除,编辑记录,点击详情可查看明细(查看变更字段的变化),请你帮我分析一下,这些代码是否能实现这些功能,下面的数据是我在测试修改订单时数据库中的数据,看看有什么问题
修改日志的before_data里面的数据是:{"id":"ORD4103888","createTime":"2025-07-30 13:37:04","modifyTime":"2025-07-30 17:03:38","merchantId":"83210837","operationTimeId":"83953091","deliveryStartTime":"2025-07-24T06:00:27.000+00:00","deliveryEndTime":"2025-07-24T06:00:27.000+00:00","status":0,"payStatus":0,"outStatus":0,"payType":0,"type":0,"remark":"","deliveryMethod":0,"total":180.00,"receiver":"达","receiverPhone":"78945632112","receiverAddress":"","lastOperation":"","signMethod":"0","sortingTaskId":"","source":0,"printCount":0,"printTime":null,"receiveTime":null,"creatorId":"1922052782395863042","loadingStatus":0,"pickupPointId":null,"refundStatus":0,"dateType":0,"dateStart":null,"dateEnd":null,"searchType":0,"search":null,"geotags":null,"lineId":null,"quotationId":null,"driverId":null,"printed":null,"hasRemark":null,"isCurrentPrice":null,"creator":null,"operatorId":"1922052782395863042","operator":null,"merchantName":null,"itemCount":null,"storeCode":null,"accountId":null,"account":null,"merchantLabel":null,"settlementMethod":null,"driverName":null,"packageStatus":0}
而after_data里面的数据是:,{"id":"ORD4103888","createTime":"2025-07-30 13:37:04","modifyTime":"2025-07-30 17:10:32","merchantId":"83210837","operationTimeId":"83953091","deliveryStartTime":"2025-07-24T06:00:01.861+00:00","deliveryEndTime":"2025-07-24T06:00:01.861+00:00","status":0,"payStatus":0,"outStatus":0,"payType":0,"type":0,"remark":"","deliveryMethod":0,"total":180,"receiver":"达","receiverPhone":"78945632112","receiverAddress":"","lastOperation":"","signMethod":"0","sortingTaskId":"","source":0,"printCount":0,"printTime":null,"receiveTime":null,"creatorId":"1922052782395863042","loadingStatus":0,"pickupPointId":null,"refundStatus":0,"dateType":0,"dateStart":null,"dateEnd":null,"searchType":0,"search":null,"geotags":null,"lineId":null,"quotationId":null,"driverId":null,"printed":null,"hasRemark":null,"isCurrentPrice":null,"creator":null,"operatorId":"1922052782395863042","operator":null,"merchantName":null,"itemCount":null,"storeCode":null,"accountId":null,"account":null,"merchantLabel":null,"settlementMethod":null,"driverName":null,"orderDetailsList":[{"id":"84158442","createTime":"2025-07-30 13:44:12","modifyTime":"2025-07-3017:10:32","orderId":"ORD4103888","goodsId":"83222762","goodsSpecificationId":"83271360","quotationId":"83257135","number":9.00,"basicNumber":18,"salesUnit":"斤","categoryId":"1940302497016995842","basicUnit":"斤","price":20.00,"basicPrice":null,"remark":"","amount":180.00,"goodsName":"排骨","specificationName":null,"merchantName":null,"quotationName":"测试报价单01","image":null,"goodsSpecificationName":"排骨","salesUnitQuantity":1.00,"classificationName":"肉类/猪肉/排骨"}],"packageStatus":0}
change_details里面的数据是:[{"newValue":"2025-07-30 17:10:32","field":"modifyTime","oldValue":"2025-07-30 17:03:38"},{"newValue":"2025-07-24T06:00:01.861+00:00","field":"deliveryEndTime","oldValue":"2025-07-24T06:00:27.000+00:00"},{"newValue":"2025-07-24T06:00:01.861+00:00","field":"deliveryStartTime","oldValue":"2025-07-24T06:00:27.000+00:00"},{"newValue":"[{\"id\":\"84158442\",\"createTime\":\"2025-07-30 13:44:12\",\"modifyTime\":\"2025-07-30 17:10:32\",\"orderId\":\"ORD4103888\",\"goodsId\":\"83222762\",\"goodsSpecificationId\":\"83271360\",\"quotationId\":\"83257135\",\"number\":9,\"basicNumber\":18,\"salesUnit\":\"斤\",\"categoryId\":\"1940302497016995842\",\"basicUnit\":\"斤\",\"price\":2E+1,\"basicPrice\":null,\"remark\":\"\",\"amount\":1.8E+2,\"goodsName\":\"排骨\",\"specificationName\":null,\"merchantName\":null,\"quotationName\":\"测试报价单01\",\"image\":null,\"goodsSpecificationName\":\"排骨\",\"salesUnitQuantity\":1,\"classificationName\":\"肉类/猪肉/排骨\"}]","field":"orderDetailsList","oldValue":null}]
最新发布