diff --git a/src/main/java/com/example/demo/RabbitMQ/LogAspect.java b/src/main/java/com/example/demo/RabbitMQ/LogAspect.java new file mode 100644 index 0000000..f7181d9 --- /dev/null +++ b/src/main/java/com/example/demo/RabbitMQ/LogAspect.java @@ -0,0 +1,108 @@ +package com.example.demo.RabbitMQ; + +import com.example.demo.Util.SecurityUtils; +import com.example.demo.config.RabbitMQConfig; +import com.example.demo.config.interfac.Log; +import com.example.demo.domain.DTO.OperationLogDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +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.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.time.LocalDateTime; + +// com.example.demo.aspect.LogAspect.java +@Aspect +@Component +@Slf4j +public class LogAspect { + + @Autowired + private RabbitTemplate rabbitTemplate; + + @Around("@annotation(com.example.demo.config.interfac.Log)") + public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { + long startTime = System.currentTimeMillis(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + String methodName = signature.getName(); + String className = signature.getDeclaringTypeName(); + Object[] args = joinPoint.getArgs(); + + Log logAnnotation = signature.getMethod().getAnnotation(Log.class); + String action = logAnnotation.value(); + + // ✅ 使用 Spring Security 获取真实用户 + String username = SecurityUtils.getCurrentUsername(); + Integer userId = SecurityUtils.getCurrentUserId(); + + String ip = getClientIp(); + + ObjectMapper mapper = new ObjectMapper(); + String argsJson = "[]"; + try { + argsJson = mapper.writeValueAsString(args); + } catch (Exception e) { + argsJson = "serialize failed"; + } + + Object result; + try { + result = joinPoint.proceed(); + } catch (Exception e) { + log.error("方法执行异常: {}", e.getMessage()); + throw e; + } + + long duration = System.currentTimeMillis() - startTime; + + // ✅ 构造日志消息 DTO + OperationLogDTO logDTO = new OperationLogDTO(); + logDTO.setUserId(userId); + logDTO.setUsername(username); + logDTO.setAction(action); + logDTO.setIp(ip); + logDTO.setMethod(className + "." + methodName); + logDTO.setArgs(argsJson); + logDTO.setCreateTime(LocalDateTime.now()); + System.out.println(logDTO); + + // ✅ 发送消息到 RabbitMQ(不等待) + try { + rabbitTemplate.convertAndSend(RabbitMQConfig.LOG_EXCHANGE, "log.save", logDTO); + log.info("📩 日志消息已发送到 RabbitMQ: {}", action); + } catch (Exception e) { + log.error("发送日志消息到 RabbitMQ 失败", e); + } + + log.info("✅ AOP 拦截完成: {} 执行 [{}] 耗时 {}ms", username, action, duration); + return result; + } + + private String getClientIp() { + try { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String ip = request.getHeader("X-Forwarded-For"); + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getHeader("WL-Proxy-Client-IP"); + } + if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { + ip = request.getRemoteAddr(); + } + return ip.split(",")[0].trim(); // 多代理时取第一个 + } catch (Exception e) { + return "unknown"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/RabbitMQ/LogConsumer.java b/src/main/java/com/example/demo/RabbitMQ/LogConsumer.java new file mode 100644 index 0000000..e8587b2 --- /dev/null +++ b/src/main/java/com/example/demo/RabbitMQ/LogConsumer.java @@ -0,0 +1,39 @@ +package com.example.demo.RabbitMQ; + +import com.example.demo.config.RabbitMQConfig; +import com.example.demo.domain.DTO.OperationLogDTO; +import com.example.demo.domain.entity.OperationLog; +import com.example.demo.mapper.coin.OperationLogMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +// com.example.demo.consumer.LogConsumer.java +@Component +@Slf4j +public class LogConsumer { + + @Autowired + private OperationLogMapper operationLogMapper; + + @RabbitListener(queues = RabbitMQConfig.LOG_QUEUE) + public void consumeLog(OperationLogDTO logDTO) { + try { + OperationLog log = new OperationLog(); + log.setUserId(logDTO.getUserId()); + log.setUsername(logDTO.getUsername()); + log.setAction(logDTO.getAction()); + log.setIp(logDTO.getIp()); + log.setMethod(logDTO.getMethod()); + log.setArgs(logDTO.getArgs()); + log.setCreateTime(logDTO.getCreateTime()); + System.out.println("consumer" + log); + operationLogMapper.insertLog(log); + + } catch (Exception e) { + log.error("持久化日志失败", e); + // 可以重试或记录到文件 + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/Util/SecurityUtils.java b/src/main/java/com/example/demo/Util/SecurityUtils.java new file mode 100644 index 0000000..703a81e --- /dev/null +++ b/src/main/java/com/example/demo/Util/SecurityUtils.java @@ -0,0 +1,28 @@ +package com.example.demo.Util; + +import com.example.demo.domain.entity.Admin; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +// com.example.demo.utils.SecurityUtils.java +public class SecurityUtils { + + public static String getCurrentUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return "anonymous"; + } + return authentication.getName(); + } + + public static Integer getCurrentUserId() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + if (authentication.getPrincipal() instanceof Admin userDetails) { + return userDetails.getId(); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/RabbitMQConfig.java b/src/main/java/com/example/demo/config/RabbitMQConfig.java new file mode 100644 index 0000000..84dca8d --- /dev/null +++ b/src/main/java/com/example/demo/config/RabbitMQConfig.java @@ -0,0 +1,48 @@ +package com.example.demo.config; + + +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.TopicExchange; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMQConfig { + + public static final String LOG_QUEUE = "operation_log_queue"; + public static final String LOG_EXCHANGE = "operation_log_exchange"; + + @Bean + public Queue logQueue() { + return new Queue(LOG_QUEUE, true); + } + + @Bean + public TopicExchange logExchange() { + return new TopicExchange(LOG_EXCHANGE); + } + + @Bean + public Binding binding() { + return BindingBuilder.bind(logQueue()) + .to(logExchange()) + .with("log.*"); + } + @Bean + public MessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) { + RabbitTemplate template = new RabbitTemplate(connectionFactory); + template.setMessageConverter(messageConverter()); + return template; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/interfac/Log.java b/src/main/java/com/example/demo/config/interfac/Log.java new file mode 100644 index 0000000..de4f976 --- /dev/null +++ b/src/main/java/com/example/demo/config/interfac/Log.java @@ -0,0 +1,15 @@ +package com.example.demo.config.interfac; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Documented; + +// com.example.demo.annotation.Log.java +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Log { + String value() default ""; // 描述操作,如 "修改用户" +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/controller/coin/GoldDetailController.java b/src/main/java/com/example/demo/controller/coin/GoldDetailController.java index a498e85..4eb9770 100644 --- a/src/main/java/com/example/demo/controller/coin/GoldDetailController.java +++ b/src/main/java/com/example/demo/controller/coin/GoldDetailController.java @@ -3,6 +3,7 @@ package com.example.demo.controller.coin; import com.example.demo.Util.BusinessException; import com.example.demo.Util.JWTUtil; import com.example.demo.Util.RedisLockUtil; +import com.example.demo.config.interfac.Log; import com.example.demo.domain.DTO.GoldDetailDTO; import com.example.demo.domain.DTO.GoldUserDTO; import com.example.demo.domain.entity.Admin; @@ -54,6 +55,7 @@ public class GoldDetailController { @Autowired MarketService marketService; + @Log("获取金币明细") @PostMapping("/getGoldDetail") public Result getGoldDetail(@RequestBody Page page) throws Exception { diff --git a/src/main/java/com/example/demo/domain/DTO/OperationLogDTO.java b/src/main/java/com/example/demo/domain/DTO/OperationLogDTO.java new file mode 100644 index 0000000..4ba419a --- /dev/null +++ b/src/main/java/com/example/demo/domain/DTO/OperationLogDTO.java @@ -0,0 +1,21 @@ +package com.example.demo.domain.DTO; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OperationLogDTO{ + private Integer userId; + private String username; + private String action; + private String ip; + private String method; + private String args; + private LocalDateTime createTime; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/entity/Log.java b/src/main/java/com/example/demo/domain/entity/Log.java deleted file mode 100644 index acf9039..0000000 --- a/src/main/java/com/example/demo/domain/entity/Log.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.example.demo.domain.entity; - -import com.fasterxml.jackson.annotation.JsonFormat; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.io.Serializable; -import java.util.Date; - -@Data -@NoArgsConstructor -@JsonIgnoreProperties(ignoreUnknown = true) -public class Log implements Serializable { - private static final long serialVersionUID = 1L; - - private Integer id; // 日志id - private String ip; // ip地址 - private Integer adminId; // 用户id - private String method; // 方法名 - private String operation; // 操作 - private Object params; // 参数 - private String path; // 请求url - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") - private Date createTime; // 创建时间(操作时间) - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") - private Date updateTime; // 更新时间 -} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/entity/OperationLog.java b/src/main/java/com/example/demo/domain/entity/OperationLog.java new file mode 100644 index 0000000..728cc39 --- /dev/null +++ b/src/main/java/com/example/demo/domain/entity/OperationLog.java @@ -0,0 +1,25 @@ +package com.example.demo.domain.entity; + +import lombok.AllArgsConstructor; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +public class OperationLog { + @Id + private Integer id; + + private Integer userId; + private String username; + private String action; // 操作描述 + private String ip; + private String method; // 调用的方法名 + private String args; // 参数(JSON字符串) + private LocalDateTime createTime = LocalDateTime.now(); + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/domain/vo/coin/Page.java b/src/main/java/com/example/demo/domain/vo/coin/Page.java index 5143c69..1fd6576 100644 --- a/src/main/java/com/example/demo/domain/vo/coin/Page.java +++ b/src/main/java/com/example/demo/domain/vo/coin/Page.java @@ -18,7 +18,6 @@ import lombok.NoArgsConstructor; public class Page { private static final long serialVersionUID = 1L; - private String token; private Integer pageNum; private Integer pageSize; private GoldDetail goldDetail; diff --git a/src/main/java/com/example/demo/mapper/coin/OperationLogMapper.java b/src/main/java/com/example/demo/mapper/coin/OperationLogMapper.java new file mode 100644 index 0000000..099908b --- /dev/null +++ b/src/main/java/com/example/demo/mapper/coin/OperationLogMapper.java @@ -0,0 +1,13 @@ +package com.example.demo.mapper.coin;// com.example.demo.mapper.OperationLogMapper.java +import com.example.demo.domain.entity.OperationLog; +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface OperationLogMapper { + + @Insert("INSERT INTO operation_log (user_id, username, action, ip, method, args, create_time) " + + "VALUES (#{userId}, #{username}, #{action}, #{ip}, #{method}, #{args}, NOW())") + void insertLog(OperationLog log); +} \ No newline at end of file diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml index 0fdc73a..cb6952d 100644 --- a/src/main/resources/application-test.yml +++ b/src/main/resources/application-test.yml @@ -64,6 +64,17 @@ spring: max-file-size: 10MB max-request-size: 10MB + + rabbitmq: + host: localhost + port: 5672 + username: guest + password: guest + virtual-host: / + listener: + simple: + trusted-packages: com.example.demo.domain.DTO + data: redis: database: 0 @@ -76,10 +87,6 @@ spring: max-active: 20 max-wait: -1 max-idle: 5 -http://192: - 168: - 1: - 50:8081: file: upload: url: http://39.101.133.168:8828/hljw/api/aws/upload