mirror of
https://github.com/ccmjga/zhilu-admin
synced 2026-03-21 18:33:41 +08:00
init aop log
This commit is contained in:
45
backend/src/main/java/com/zl/mjga/annotation/SkipAopLog.java
Normal file
45
backend/src/main/java/com/zl/mjga/annotation/SkipAopLog.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.zl.mjga.annotation;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 跳过AOP日志记录注解
|
||||
*
|
||||
* <p>在方法上添加此注解,该方法将不会被AOP日志切面拦截和记录。
|
||||
*
|
||||
* <p>使用场景:
|
||||
*
|
||||
* <ul>
|
||||
* <li>敏感操作方法,不希望记录日志
|
||||
* <li>高频调用方法,避免产生过多日志
|
||||
* <li>内部工具方法,不需要业务日志记录
|
||||
* </ul>
|
||||
*
|
||||
* <p>使用示例:
|
||||
*
|
||||
* <pre>{@code
|
||||
* @SkipAopLog
|
||||
* public void sensitiveMethod() {
|
||||
* // 此方法不会被AOP日志记录
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* @author AOP Log System
|
||||
* @since 1.0
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface SkipAopLog {
|
||||
|
||||
/**
|
||||
* 跳过日志记录的原因说明(可选)
|
||||
*
|
||||
* @return 跳过原因
|
||||
*/
|
||||
String reason() default "";
|
||||
}
|
||||
182
backend/src/main/java/com/zl/mjga/aspect/LoggingAspect.java
Normal file
182
backend/src/main/java/com/zl/mjga/aspect/LoggingAspect.java
Normal file
@@ -0,0 +1,182 @@
|
||||
package com.zl.mjga.aspect;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zl.mjga.annotation.SkipAopLog;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.AopLogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
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.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.jooq.generated.mjga.tables.pojos.User;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class LoggingAspect {
|
||||
|
||||
private final AopLogService aopLogService;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Around("execution(* com.zl.mjga.controller..*(..))")
|
||||
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
AopLog aopLog = new AopLog();
|
||||
setRequestInfo(aopLog);
|
||||
return processWithLogging(joinPoint, aopLog);
|
||||
}
|
||||
|
||||
@Around("execution(* com.zl.mjga.service..*(..))")
|
||||
public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
AopLog aopLog = new AopLog();
|
||||
return processWithLogging(joinPoint, aopLog);
|
||||
}
|
||||
|
||||
@Around("execution(* com.zl.mjga.repository..*(..))")
|
||||
public Object logRepository(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
AopLog aopLog = new AopLog();
|
||||
return processWithLogging(joinPoint, aopLog);
|
||||
}
|
||||
|
||||
private Object processWithLogging(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
|
||||
if (shouldSkipLogging(joinPoint) || !isUserAuthenticated()) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
return logMethodExecution(joinPoint, aopLog);
|
||||
}
|
||||
|
||||
private boolean shouldSkipLogging(ProceedingJoinPoint joinPoint) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
return method.isAnnotationPresent(SkipAopLog.class);
|
||||
}
|
||||
|
||||
private boolean isUserAuthenticated() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
return authentication != null
|
||||
&& authentication.isAuthenticated()
|
||||
&& !"anonymousUser".equals(authentication.getName());
|
||||
}
|
||||
|
||||
private Long getCurrentUserId() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String username = (String) authentication.getPrincipal();
|
||||
User user = userRepository.fetchOneByUsername(username);
|
||||
return user.getId();
|
||||
}
|
||||
|
||||
private Object logMethodExecution(ProceedingJoinPoint joinPoint, AopLog aopLog) throws Throwable {
|
||||
Instant startTime = Instant.now();
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
populateBasicLogInfo(aopLog, className, methodName, joinPoint.getArgs());
|
||||
|
||||
Object result = null;
|
||||
Exception executionException = null;
|
||||
|
||||
try {
|
||||
result = joinPoint.proceed();
|
||||
aopLog.setReturnValue(serializeReturnValue(result));
|
||||
} catch (Exception e) {
|
||||
executionException = e;
|
||||
aopLog.setErrorMessage(e.getMessage());
|
||||
log.error("Method execution failed: {}.{}", className, methodName, e);
|
||||
} finally {
|
||||
aopLog.setExecutionTime(Duration.between(startTime, Instant.now()).toMillis());
|
||||
aopLog.setSuccess(executionException == null);
|
||||
saveLogSafely(aopLog);
|
||||
}
|
||||
|
||||
if (executionException != null) {
|
||||
throw executionException;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void populateBasicLogInfo(
|
||||
AopLog aopLog, String className, String methodName, Object[] args) {
|
||||
aopLog.setClassName(className);
|
||||
aopLog.setMethodName(methodName);
|
||||
aopLog.setMethodArgs(serializeArgs(args));
|
||||
aopLog.setUserId(getCurrentUserId());
|
||||
}
|
||||
|
||||
private void saveLogSafely(AopLog aopLog) {
|
||||
try {
|
||||
aopLogService.saveLogAsync(aopLog);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Failed to save AOP log for {}.{}", aopLog.getClassName(), aopLog.getMethodName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setRequestInfo(AopLog aopLog) {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
aopLog.setIpAddress(getClientIpAddress(request));
|
||||
aopLog.setUserAgent(request.getHeader("User-Agent"));
|
||||
}
|
||||
|
||||
private String getClientIpAddress(HttpServletRequest request) {
|
||||
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (xForwardedFor != null
|
||||
&& !xForwardedFor.isEmpty()
|
||||
&& !"unknown".equalsIgnoreCase(xForwardedFor)) {
|
||||
return xForwardedFor.split(",")[0].trim();
|
||||
}
|
||||
|
||||
String xRealIp = request.getHeader("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
|
||||
private String serializeArgs(Object[] args) {
|
||||
if (ArrayUtils.isEmpty(args)) {
|
||||
return null;
|
||||
} else {
|
||||
return serializeObject(args);
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeReturnValue(Object returnValue) {
|
||||
if (returnValue == null) {
|
||||
return null;
|
||||
} else {
|
||||
return serializeObject(returnValue);
|
||||
}
|
||||
}
|
||||
|
||||
private String serializeObject(Object obj) {
|
||||
try {
|
||||
return objectMapper.writeValueAsString(obj);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize {} ", obj, e);
|
||||
return e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.zl.mjga.controller;
|
||||
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.PageResponseDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogRespDto;
|
||||
import com.zl.mjga.repository.AopLogRepository;
|
||||
import com.zl.mjga.service.AopLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/aop-log")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
@Tag(name = "AOP日志管理", description = "AOP日志查看和管理接口")
|
||||
public class AopLogController {
|
||||
|
||||
private final AopLogService aopLogService;
|
||||
private final AopLogRepository aopLogRepository;
|
||||
|
||||
@GetMapping("/page-query")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "分页查询AOP日志", description = "支持多种条件筛选的分页查询")
|
||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
|
||||
public PageResponseDto<List<AopLogRespDto>> pageQueryAopLogs(
|
||||
@ModelAttribute @Valid PageRequestDto pageRequestDto,
|
||||
@ModelAttribute AopLogQueryDto queryDto) {
|
||||
return aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "查询日志详情", description = "根据ID查询单条日志的详细信息")
|
||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).READ_USER_ROLE_PERMISSION)")
|
||||
public AopLogRespDto getAopLogById(@Parameter(description = "日志ID") @PathVariable Long id) {
|
||||
return aopLogService.getAopLogById(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/batch")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "批量删除日志", description = "根据ID列表批量删除日志")
|
||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
|
||||
public int deleteAopLogs(@Parameter(description = "日志ID列表") @RequestBody List<Long> ids) {
|
||||
return aopLogRepository.deleteByIds(ids);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "删除单条日志", description = "根据ID删除单条日志")
|
||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
|
||||
public void deleteAopLog(@Parameter(description = "日志ID") @PathVariable Long id) {
|
||||
aopLogRepository.deleteById(id);
|
||||
}
|
||||
|
||||
@DeleteMapping("/before")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@Operation(summary = "删除指定时间前的日志", description = "删除指定时间之前的所有日志")
|
||||
@PreAuthorize("hasAuthority(T(com.zl.mjga.model.urp.EPermission).WRITE_USER_ROLE_PERMISSION)")
|
||||
public int deleteLogsBeforeTime(
|
||||
@Parameter(description = "截止时间") @RequestParam OffsetDateTime beforeTime) {
|
||||
return aopLogService.deleteLogsBeforeTime(beforeTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.zl.mjga.dto.aoplog;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/** AOP日志查询DTO */
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AopLogQueryDto {
|
||||
|
||||
/** ID */
|
||||
private Long id;
|
||||
|
||||
/** 类名 */
|
||||
private String className;
|
||||
|
||||
/** 方法名 */
|
||||
private String methodName;
|
||||
|
||||
/** 是否成功 */
|
||||
private Boolean success;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** IP地址 */
|
||||
private String ipAddress;
|
||||
|
||||
/** 开始时间 */
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/** 结束时间 */
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/** 最小执行时间(毫秒) */
|
||||
private Long minExecutionTime;
|
||||
|
||||
/** 最大执行时间(毫秒) */
|
||||
private Long maxExecutionTime;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.zl.mjga.dto.aoplog;
|
||||
|
||||
import java.time.OffsetDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class AopLogRespDto {
|
||||
|
||||
/** 主键ID */
|
||||
private Long id;
|
||||
|
||||
/** 类名 */
|
||||
private String className;
|
||||
|
||||
/** 方法名 */
|
||||
private String methodName;
|
||||
|
||||
/** 方法参数 */
|
||||
private String methodArgs;
|
||||
|
||||
/** 返回值 */
|
||||
private String returnValue;
|
||||
|
||||
/** 执行时间(毫秒) */
|
||||
private Long executionTime;
|
||||
|
||||
/** 是否成功 */
|
||||
private Boolean success;
|
||||
|
||||
/** 错误信息 */
|
||||
private String errorMessage;
|
||||
|
||||
/** 用户ID */
|
||||
private Long userId;
|
||||
|
||||
/** 用户名 */
|
||||
private String username;
|
||||
|
||||
/** IP地址 */
|
||||
private String ipAddress;
|
||||
|
||||
/** 用户代理 */
|
||||
private String userAgent;
|
||||
|
||||
/** 创建时间 */
|
||||
private OffsetDateTime createTime;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.zl.mjga.model.urp;
|
||||
|
||||
public enum ChatMode {
|
||||
NORMAL,
|
||||
WITH_LIBRARY
|
||||
NORMAL,
|
||||
WITH_LIBRARY
|
||||
}
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.zl.mjga.repository;
|
||||
|
||||
import static org.jooq.generated.mjga.tables.AopLog.AOP_LOG;
|
||||
import static org.jooq.generated.mjga.tables.User.USER;
|
||||
import static org.jooq.impl.DSL.*;
|
||||
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jooq.*;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.generated.mjga.tables.daos.AopLogDao;
|
||||
import org.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.jooq.impl.DSL;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** AOP日志Repository */
|
||||
@Repository
|
||||
public class AopLogRepository extends AopLogDao {
|
||||
|
||||
@Autowired
|
||||
public AopLogRepository(Configuration configuration) {
|
||||
super(configuration);
|
||||
}
|
||||
|
||||
public Result<Record> pageFetchBy(PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
|
||||
return selectBy(queryDto)
|
||||
.orderBy(pageRequestDto.getSortFields())
|
||||
.limit(pageRequestDto.getSize())
|
||||
.offset(pageRequestDto.getOffset())
|
||||
.fetch();
|
||||
}
|
||||
|
||||
public List<AopLog> fetchBy(AopLogQueryDto queryDto) {
|
||||
return selectBy(queryDto).fetchInto(AopLog.class);
|
||||
}
|
||||
|
||||
public SelectConditionStep<Record> selectBy(AopLogQueryDto queryDto) {
|
||||
return ctx()
|
||||
.select(AOP_LOG.asterisk(), USER.USERNAME, DSL.count().over().as("total_count"))
|
||||
.from(AOP_LOG)
|
||||
.leftJoin(USER)
|
||||
.on(AOP_LOG.USER_ID.eq(USER.ID))
|
||||
.where(buildConditions(queryDto));
|
||||
}
|
||||
|
||||
private Condition buildConditions(AopLogQueryDto queryDto) {
|
||||
Condition condition = noCondition();
|
||||
|
||||
if (queryDto == null) {
|
||||
return condition;
|
||||
}
|
||||
|
||||
// ID精确查询
|
||||
if (queryDto.getId() != null) {
|
||||
condition = condition.and(AOP_LOG.ID.eq(queryDto.getId()));
|
||||
}
|
||||
|
||||
// 类名模糊查询
|
||||
if (StringUtils.isNotBlank(queryDto.getClassName())) {
|
||||
condition = condition.and(AOP_LOG.CLASS_NAME.like("%" + queryDto.getClassName() + "%"));
|
||||
}
|
||||
|
||||
// 方法名模糊查询
|
||||
if (StringUtils.isNotBlank(queryDto.getMethodName())) {
|
||||
condition = condition.and(AOP_LOG.METHOD_NAME.like("%" + queryDto.getMethodName() + "%"));
|
||||
}
|
||||
|
||||
// 成功状态
|
||||
if (queryDto.getSuccess() != null) {
|
||||
condition = condition.and(AOP_LOG.SUCCESS.eq(queryDto.getSuccess()));
|
||||
}
|
||||
|
||||
// 用户ID
|
||||
if (queryDto.getUserId() != null) {
|
||||
condition = condition.and(AOP_LOG.USER_ID.eq(queryDto.getUserId()));
|
||||
}
|
||||
|
||||
// IP地址模糊查询
|
||||
if (StringUtils.isNotBlank(queryDto.getIpAddress())) {
|
||||
condition = condition.and(AOP_LOG.IP_ADDRESS.like("%" + queryDto.getIpAddress() + "%"));
|
||||
}
|
||||
|
||||
// 时间范围查询
|
||||
if (queryDto.getStartTime() != null) {
|
||||
OffsetDateTime startTime = queryDto.getStartTime().atOffset(OffsetDateTime.now().getOffset());
|
||||
condition = condition.and(AOP_LOG.CREATE_TIME.ge(startTime));
|
||||
}
|
||||
|
||||
if (queryDto.getEndTime() != null) {
|
||||
OffsetDateTime endTime = queryDto.getEndTime().atOffset(OffsetDateTime.now().getOffset());
|
||||
condition = condition.and(AOP_LOG.CREATE_TIME.le(endTime));
|
||||
}
|
||||
|
||||
// 执行时间范围
|
||||
if (queryDto.getMinExecutionTime() != null) {
|
||||
condition = condition.and(AOP_LOG.EXECUTION_TIME.ge(queryDto.getMinExecutionTime()));
|
||||
}
|
||||
|
||||
if (queryDto.getMaxExecutionTime() != null) {
|
||||
condition = condition.and(AOP_LOG.EXECUTION_TIME.le(queryDto.getMaxExecutionTime()));
|
||||
}
|
||||
|
||||
return condition;
|
||||
}
|
||||
|
||||
public int deleteByIds(List<Long> ids) {
|
||||
return ctx().deleteFrom(AOP_LOG).where(AOP_LOG.ID.in(ids)).execute();
|
||||
}
|
||||
|
||||
public int deleteBeforeTime(OffsetDateTime beforeTime) {
|
||||
return ctx().deleteFrom(AOP_LOG).where(AOP_LOG.CREATE_TIME.lt(beforeTime)).execute();
|
||||
}
|
||||
}
|
||||
61
backend/src/main/java/com/zl/mjga/service/AopLogService.java
Normal file
61
backend/src/main/java/com/zl/mjga/service/AopLogService.java
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.zl.mjga.service;
|
||||
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.PageResponseDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogRespDto;
|
||||
import com.zl.mjga.repository.AopLogRepository;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.SelectConditionStep;
|
||||
import org.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class AopLogService {
|
||||
|
||||
private final AopLogRepository aopLogRepository;
|
||||
|
||||
@Async
|
||||
public void saveLogAsync(AopLog aopLog) {
|
||||
try {
|
||||
aopLogRepository.insert(aopLog);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save AOP log asynchronously", e);
|
||||
}
|
||||
}
|
||||
|
||||
public PageResponseDto<List<AopLogRespDto>> pageQueryAopLogs(
|
||||
PageRequestDto pageRequestDto, AopLogQueryDto queryDto) {
|
||||
Result<Record> records = aopLogRepository.pageFetchBy(pageRequestDto, queryDto);
|
||||
|
||||
if (records.isEmpty()) {
|
||||
return PageResponseDto.empty();
|
||||
}
|
||||
|
||||
List<AopLogRespDto> aopLogs = records.map((record -> record.into(AopLogRespDto.class)));
|
||||
Long totalCount = records.get(0).getValue("total_count", Long.class);
|
||||
|
||||
return new PageResponseDto<>(totalCount, aopLogs);
|
||||
}
|
||||
|
||||
public AopLogRespDto getAopLogById(Long id) {
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
queryDto.setId(id);
|
||||
SelectConditionStep<Record> selectStep = aopLogRepository.selectBy(queryDto);
|
||||
return selectStep.fetchOneInto(AopLogRespDto.class);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int deleteLogsBeforeTime(OffsetDateTime beforeTime) {
|
||||
return aopLogRepository.deleteBeforeTime(beforeTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
CREATE TABLE mjga.aop_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
class_name VARCHAR NOT NULL,
|
||||
method_name VARCHAR NOT NULL,
|
||||
method_args VARCHAR,
|
||||
return_value VARCHAR,
|
||||
execution_time BIGINT NOT NULL,
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_message VARCHAR,
|
||||
user_id BIGINT,
|
||||
ip_address VARCHAR,
|
||||
user_agent VARCHAR,
|
||||
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_aop_log_class_name ON mjga.aop_log(class_name);
|
||||
CREATE INDEX idx_aop_log_method_name ON mjga.aop_log(method_name);
|
||||
CREATE INDEX idx_aop_log_create_time ON mjga.aop_log(create_time);
|
||||
CREATE INDEX idx_aop_log_user_id ON mjga.aop_log(user_id);
|
||||
CREATE INDEX idx_aop_log_success ON mjga.aop_log(success);
|
||||
@@ -0,0 +1,440 @@
|
||||
package com.zl.mjga.integration.aspect;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zl.mjga.annotation.SkipAopLog;
|
||||
import com.zl.mjga.aspect.LoggingAspect;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.AopLogService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.lang.reflect.Method;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.jooq.generated.mjga.tables.pojos.User;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class LoggingAspectTest {
|
||||
|
||||
@Mock private AopLogService aopLogService;
|
||||
@Mock private ObjectMapper objectMapper;
|
||||
@Mock private UserRepository userRepository;
|
||||
@Mock private ProceedingJoinPoint joinPoint;
|
||||
@Mock private MethodSignature methodSignature;
|
||||
@Mock private SecurityContext securityContext;
|
||||
@Mock private Authentication authentication;
|
||||
@Mock private ServletRequestAttributes servletRequestAttributes;
|
||||
@Mock private HttpServletRequest httpServletRequest;
|
||||
|
||||
private LoggingAspect loggingAspect;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
loggingAspect = new LoggingAspect(aopLogService, objectMapper, userRepository);
|
||||
SecurityContextHolder.setContext(securityContext);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
SecurityContextHolder.clearContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenSuccessfulExecution_shouldSaveSuccessLog() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
Object[] args = {"arg1", "arg2"};
|
||||
String expectedResult = "success";
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "testMethod", args, expectedResult);
|
||||
setupSerialization("[\"arg1\",\"arg2\"]", "\"success\"");
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
setupRequestContext("192.168.1.1", "Test-Agent")) {
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getClassName()).isEqualTo("TestController");
|
||||
assertThat(log.getMethodName()).isEqualTo("testMethod");
|
||||
assertThat(log.getMethodArgs()).isEqualTo("[\"arg1\",\"arg2\"]");
|
||||
assertThat(log.getReturnValue()).isEqualTo("\"success\"");
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
assertThat(log.getUserId()).isEqualTo(123L);
|
||||
assertThat(log.getIpAddress()).isEqualTo("192.168.1.1");
|
||||
assertThat(log.getUserAgent()).isEqualTo("Test-Agent");
|
||||
assertThat(log.getExecutionTime()).isGreaterThanOrEqualTo(0L);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenFailedExecution_shouldSaveFailLog() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
Object[] args = {"arg1"};
|
||||
RuntimeException exception = new RuntimeException("Test error");
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "failMethod", args, exception);
|
||||
setupSerialization("[\"arg1\"]", null);
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
setupRequestContext("192.168.1.1", "Test-Agent")) {
|
||||
// action & assert
|
||||
assertThatThrownBy(() -> loggingAspect.logController(joinPoint))
|
||||
.isInstanceOf(RuntimeException.class)
|
||||
.hasMessage("Test error");
|
||||
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getClassName()).isEqualTo("TestController");
|
||||
assertThat(log.getMethodName()).isEqualTo("failMethod");
|
||||
assertThat(log.getSuccess()).isFalse();
|
||||
assertThat(log.getErrorMessage()).isEqualTo("Test error");
|
||||
assertThat(log.getReturnValue()).isNull();
|
||||
assertThat(log.getUserId()).isEqualTo(123L);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logService_givenSuccessfulExecution_shouldSaveSuccessLogWithoutRequestInfo()
|
||||
throws Throwable {
|
||||
// arrange
|
||||
TestService target = new TestService();
|
||||
Object[] args = {"serviceArg"};
|
||||
String expectedResult = "serviceResult";
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "serviceMethod", args, expectedResult);
|
||||
setupSerialization("[\"serviceArg\"]", "\"serviceResult\"");
|
||||
|
||||
// action
|
||||
Object result = loggingAspect.logService(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getClassName()).isEqualTo("TestService");
|
||||
assertThat(log.getMethodName()).isEqualTo("serviceMethod");
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
assertThat(log.getUserId()).isEqualTo(123L);
|
||||
// Service层不应该有请求信息
|
||||
assertThat(log.getIpAddress()).isNull();
|
||||
assertThat(log.getUserAgent()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void logRepository_givenSuccessfulExecution_shouldSaveSuccessLogWithoutRequestInfo()
|
||||
throws Throwable {
|
||||
// arrange
|
||||
TestRepository target = new TestRepository();
|
||||
Object[] args = {1L};
|
||||
Object expectedResult = new Object();
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "findById", args, expectedResult);
|
||||
setupSerialization("[1]", "{}");
|
||||
|
||||
// action
|
||||
Object result = loggingAspect.logRepository(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getClassName()).isEqualTo("TestRepository");
|
||||
assertThat(log.getMethodName()).isEqualTo("findById");
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
assertThat(log.getUserId()).isEqualTo(123L);
|
||||
// Repository层不应该有请求信息
|
||||
assertThat(log.getIpAddress()).isNull();
|
||||
assertThat(log.getUserAgent()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenUnauthenticatedUser_shouldNotLog() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
String expectedResult = "success";
|
||||
Method testMethod = TestController.class.getMethod("testMethod");
|
||||
|
||||
when(joinPoint.getTarget()).thenReturn(target);
|
||||
when(joinPoint.proceed()).thenReturn(expectedResult);
|
||||
when(joinPoint.getSignature()).thenReturn(methodSignature);
|
||||
when(methodSignature.getMethod()).thenReturn(testMethod);
|
||||
|
||||
// Mock SecurityContextHolder to return null authentication
|
||||
when(securityContext.getAuthentication()).thenReturn(null);
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
mockStatic(RequestContextHolder.class)) {
|
||||
mockedRequestContextHolder.when(RequestContextHolder::getRequestAttributes).thenReturn(null);
|
||||
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verify(aopLogService, never()).saveLogAsync(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenAnonymousUser_shouldNotLog() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
String expectedResult = "success";
|
||||
Method testMethod = TestController.class.getMethod("testMethod");
|
||||
|
||||
when(authentication.isAuthenticated()).thenReturn(true);
|
||||
when(authentication.getName()).thenReturn("anonymousUser");
|
||||
when(joinPoint.getTarget()).thenReturn(target);
|
||||
when(joinPoint.proceed()).thenReturn(expectedResult);
|
||||
when(joinPoint.getSignature()).thenReturn(methodSignature);
|
||||
when(methodSignature.getMethod()).thenReturn(testMethod);
|
||||
|
||||
// Mock SecurityContextHolder to return anonymous authentication
|
||||
when(securityContext.getAuthentication()).thenReturn(authentication);
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
mockStatic(RequestContextHolder.class)) {
|
||||
mockedRequestContextHolder.when(RequestContextHolder::getRequestAttributes).thenReturn(null);
|
||||
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verify(aopLogService, never()).saveLogAsync(any());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenSkipAopLogAnnotation_shouldNotLog() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
String expectedResult = "success";
|
||||
|
||||
when(joinPoint.getTarget()).thenReturn(target);
|
||||
when(joinPoint.getSignature()).thenReturn(methodSignature);
|
||||
when(methodSignature.getMethod()).thenReturn(getSkipLogMethod());
|
||||
when(joinPoint.proceed()).thenReturn(expectedResult);
|
||||
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verify(aopLogService, never()).saveLogAsync(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenNullArgs_shouldHandleGracefully() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "noArgsMethod", null, "result");
|
||||
setupSerialization(null, "\"result\"");
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
setupRequestContext("127.0.0.1", "Test-Agent")) {
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo("result");
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getMethodArgs()).isNull();
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenEmptyArgs_shouldHandleGracefully() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
Object[] emptyArgs = {};
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "noArgsMethod", emptyArgs, "result");
|
||||
setupSerialization(null, "\"result\"");
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
setupRequestContext("127.0.0.1", "Test-Agent")) {
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo("result");
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getMethodArgs()).isNull();
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void logController_givenSerializationError_shouldHandleGracefully() throws Throwable {
|
||||
// arrange
|
||||
TestController target = new TestController();
|
||||
Object[] args = {"arg1"};
|
||||
String expectedResult = "success";
|
||||
User mockUser = createMockUser(123L, "testUser");
|
||||
|
||||
setupAuthenticatedUser("testUser", mockUser);
|
||||
setupJoinPoint(target, "testMethod", args, expectedResult);
|
||||
|
||||
// Mock serialization error
|
||||
when(objectMapper.writeValueAsString(args))
|
||||
.thenThrow(new JsonProcessingException("Serialization failed") {});
|
||||
when(objectMapper.writeValueAsString(expectedResult)).thenReturn("\"success\"");
|
||||
|
||||
try (MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
setupRequestContext("127.0.0.1", "Test-Agent")) {
|
||||
// action
|
||||
Object result = loggingAspect.logController(joinPoint);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(expectedResult);
|
||||
verifyLogSaved(
|
||||
log -> {
|
||||
assertThat(log.getMethodArgs()).isEqualTo("Serialization failed");
|
||||
assertThat(log.getSuccess()).isTrue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
private User createMockUser(Long id, String username) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername(username);
|
||||
return user;
|
||||
}
|
||||
|
||||
private void setupAuthenticatedUser(String username, User user) {
|
||||
when(securityContext.getAuthentication()).thenReturn(authentication);
|
||||
when(authentication.isAuthenticated()).thenReturn(true);
|
||||
when(authentication.getName()).thenReturn(username);
|
||||
when(authentication.getPrincipal()).thenReturn(username);
|
||||
when(userRepository.fetchOneByUsername(username)).thenReturn(user);
|
||||
}
|
||||
|
||||
private void setupJoinPoint(Object target, String methodName, Object[] args, Object result)
|
||||
throws Throwable {
|
||||
when(joinPoint.getTarget()).thenReturn(target);
|
||||
when(joinPoint.getSignature()).thenReturn(methodSignature);
|
||||
when(methodSignature.getName()).thenReturn(methodName);
|
||||
when(methodSignature.getMethod()).thenReturn(getTestMethod());
|
||||
when(joinPoint.getArgs()).thenReturn(args);
|
||||
|
||||
if (result instanceof Throwable) {
|
||||
when(joinPoint.proceed()).thenThrow((Throwable) result);
|
||||
} else {
|
||||
when(joinPoint.proceed()).thenReturn(result);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSerialization(String argsJson, String resultJson)
|
||||
throws JsonProcessingException {
|
||||
if (argsJson != null) {
|
||||
when(objectMapper.writeValueAsString(any(Object[].class))).thenReturn(argsJson);
|
||||
}
|
||||
if (resultJson != null) {
|
||||
when(objectMapper.writeValueAsString(argThat(arg -> !(arg instanceof Object[]))))
|
||||
.thenReturn(resultJson);
|
||||
}
|
||||
}
|
||||
|
||||
private MockedStatic<RequestContextHolder> setupRequestContext(
|
||||
String ipAddress, String userAgent) {
|
||||
MockedStatic<RequestContextHolder> mockedRequestContextHolder =
|
||||
mockStatic(RequestContextHolder.class);
|
||||
mockedRequestContextHolder
|
||||
.when(RequestContextHolder::getRequestAttributes)
|
||||
.thenReturn(servletRequestAttributes);
|
||||
when(servletRequestAttributes.getRequest()).thenReturn(httpServletRequest);
|
||||
when(httpServletRequest.getHeader("X-Forwarded-For")).thenReturn(ipAddress);
|
||||
when(httpServletRequest.getHeader("User-Agent")).thenReturn(userAgent);
|
||||
when(httpServletRequest.getRemoteAddr()).thenReturn("127.0.0.1");
|
||||
return mockedRequestContextHolder;
|
||||
}
|
||||
|
||||
private void verifyLogSaved(java.util.function.Consumer<AopLog> logVerifier) {
|
||||
ArgumentCaptor<AopLog> logCaptor = ArgumentCaptor.forClass(AopLog.class);
|
||||
verify(aopLogService, times(1)).saveLogAsync(logCaptor.capture());
|
||||
logVerifier.accept(logCaptor.getValue());
|
||||
}
|
||||
|
||||
private java.lang.reflect.Method getTestMethod() throws NoSuchMethodException {
|
||||
return TestController.class.getMethod("testMethod");
|
||||
}
|
||||
|
||||
private java.lang.reflect.Method getSkipLogMethod() throws NoSuchMethodException {
|
||||
return TestController.class.getMethod("skipLogMethod");
|
||||
}
|
||||
|
||||
// Test classes for mocking
|
||||
private static class TestController {
|
||||
public String testMethod() {
|
||||
return "test";
|
||||
}
|
||||
|
||||
@SkipAopLog(reason = "测试跳过日志记录")
|
||||
public String skipLogMethod() {
|
||||
return "test";
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestService {
|
||||
public String testMethod() {
|
||||
return "test";
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestRepository {
|
||||
public String testMethod() {
|
||||
return "test";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.zl.mjga.integration.mvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.zl.mjga.config.minio.MinIoConfig;
|
||||
import com.zl.mjga.config.security.HttpFireWallConfig;
|
||||
import com.zl.mjga.controller.AopLogController;
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.PageResponseDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogRespDto;
|
||||
import com.zl.mjga.repository.AopLogRepository;
|
||||
import com.zl.mjga.repository.PermissionRepository;
|
||||
import com.zl.mjga.repository.RoleRepository;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.AopLogService;
|
||||
import io.minio.MinioClient;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.test.context.support.WithMockUser;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
@WebMvcTest(value = {AopLogController.class})
|
||||
@Import({HttpFireWallConfig.class})
|
||||
public class AopLogControllerTest {
|
||||
|
||||
@Autowired private MockMvc mockMvc;
|
||||
|
||||
@Autowired private ObjectMapper objectMapper;
|
||||
|
||||
@MockBean private AopLogService aopLogService;
|
||||
@MockBean private AopLogRepository aopLogRepository;
|
||||
|
||||
@MockBean private UserRepository userRepository;
|
||||
@MockBean private RoleRepository roleRepository;
|
||||
@MockBean private PermissionRepository permissionRepository;
|
||||
@MockBean private MinioClient minioClient;
|
||||
@MockBean private MinIoConfig minIoConfig;
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
|
||||
void pageQueryAopLogs_givenValidRequest_shouldReturnOk() throws Exception {
|
||||
// arrange
|
||||
PageResponseDto<List<AopLogRespDto>> mockResponse =
|
||||
new PageResponseDto<>(1L, List.of(createTestAopLogRespDto()));
|
||||
when(aopLogService.pageQueryAopLogs(any(PageRequestDto.class), any(AopLogQueryDto.class)))
|
||||
.thenReturn(mockResponse);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(
|
||||
get("/aop-log/page-query")
|
||||
.param("page", "1")
|
||||
.param("size", "10")
|
||||
.param("className", "TestController"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.total").value(1))
|
||||
.andExpect(jsonPath("$.data").isArray())
|
||||
.andExpect(jsonPath("$.data[0].className").value("TestController"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageQueryAopLogs_givenNoAuth_shouldReturnUnauthorized() throws Exception {
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(get("/aop-log/page-query").param("page", "1").param("size", "10"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
|
||||
void getAopLogById_givenValidId_shouldReturnOk() throws Exception {
|
||||
// arrange
|
||||
Long id = 1L;
|
||||
AopLogRespDto mockResponse = createTestAopLogRespDto();
|
||||
when(aopLogService.getAopLogById(id)).thenReturn(mockResponse);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(get("/aop-log/{id}", id))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.id").value(1))
|
||||
.andExpect(jsonPath("$.className").value("TestController"))
|
||||
.andExpect(jsonPath("$.methodName").value("testMethod"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "READ_USER_ROLE_PERMISSION")
|
||||
void getAopLogById_givenNonExistingId_shouldReturnOkWithNull() throws Exception {
|
||||
// arrange
|
||||
Long id = 999L;
|
||||
when(aopLogService.getAopLogById(id)).thenReturn(null);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(get("/aop-log/{id}", id))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
|
||||
void deleteAopLogs_givenValidIds_shouldReturnOk() throws Exception {
|
||||
// arrange
|
||||
List<Long> ids = List.of(1L, 2L, 3L);
|
||||
when(aopLogRepository.deleteByIds(ids)).thenReturn(3);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(
|
||||
delete("/aop-log/batch")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(ids))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteAopLogs_givenNoAuth_shouldReturnUnauthorized() throws Exception {
|
||||
// arrange
|
||||
List<Long> ids = List.of(1L, 2L, 3L);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(
|
||||
delete("/aop-log/batch")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(objectMapper.writeValueAsString(ids))
|
||||
.with(csrf()))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
|
||||
void deleteAopLog_givenValidId_shouldReturnOk() throws Exception {
|
||||
// arrange
|
||||
Long id = 1L;
|
||||
when(aopLogRepository.deleteByIds(List.of(id))).thenReturn(1);
|
||||
|
||||
// action & assert
|
||||
mockMvc.perform(delete("/aop-log/{id}", id).with(csrf())).andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(authorities = "WRITE_USER_ROLE_PERMISSION")
|
||||
void deleteLogsBeforeTime_givenValidTime_shouldReturnOk() throws Exception {
|
||||
// arrange
|
||||
OffsetDateTime beforeTime = OffsetDateTime.now().minusDays(7);
|
||||
when(aopLogService.deleteLogsBeforeTime(beforeTime)).thenReturn(5);
|
||||
|
||||
// action & assert
|
||||
mockMvc
|
||||
.perform(delete("/aop-log/before").param("beforeTime", beforeTime.toString()).with(csrf()))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("5"));
|
||||
}
|
||||
|
||||
private AopLogRespDto createTestAopLogRespDto() {
|
||||
return AopLogRespDto.builder()
|
||||
.id(1L)
|
||||
.className("TestController")
|
||||
.methodName("testMethod")
|
||||
.methodArgs("[\"arg1\"]")
|
||||
.returnValue("\"result\"")
|
||||
.executionTime(100L)
|
||||
.success(true)
|
||||
.userId(1L)
|
||||
.username("testUser")
|
||||
.ipAddress("127.0.0.1")
|
||||
.userAgent("Test Agent")
|
||||
.createTime(OffsetDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import com.zl.mjga.repository.PermissionRepository;
|
||||
import com.zl.mjga.repository.RoleRepository;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.IdentityAccessService;
|
||||
import com.zl.mjga.service.UploadService;
|
||||
import io.minio.MinioClient;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -38,6 +39,7 @@ public class JacksonAnnotationMvcTest {
|
||||
@MockBean private PermissionRepository permissionRepository;
|
||||
@MockBean private MinioClient minioClient;
|
||||
@MockBean private MinIoConfig minIoConfig;
|
||||
@MockBean private UploadService uploadService;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.zl.mjga.repository.PermissionRepository;
|
||||
import com.zl.mjga.repository.RoleRepository;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.IdentityAccessService;
|
||||
import com.zl.mjga.service.UploadService;
|
||||
import io.minio.MinioClient;
|
||||
import java.util.List;
|
||||
import org.jooq.generated.mjga.tables.pojos.User;
|
||||
@@ -40,6 +41,7 @@ class UserRolePermissionMvcTest {
|
||||
@MockBean private PermissionRepository permissionRepository;
|
||||
@MockBean private MinioClient minioClient;
|
||||
@MockBean private MinIoConfig minIoConfig;
|
||||
@MockBean private UploadService uploadService;
|
||||
|
||||
@Test
|
||||
@WithMockUser
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.zl.mjga.integration.persistence;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import com.zl.mjga.repository.AopLogRepository;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
|
||||
public class AopLogRepositoryTest extends AbstractDataAccessLayerTest {
|
||||
|
||||
@Autowired private AopLogRepository aopLogRepository;
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.user (id, username, password) VALUES (1, 'testUser', 'password')",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, method_args, return_value,"
|
||||
+ " execution_time, success, user_id, ip_address) VALUES (1, 'TestController',"
|
||||
+ " 'testMethod', '[\"arg1\"]', '\"result\"', 100, true, 1, '127.0.0.1')",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, method_args, return_value,"
|
||||
+ " execution_time, success, error_message) VALUES (2, 'TestService', 'failMethod',"
|
||||
+ " '[\"arg2\"]', null, 200, false, 'Test error')",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
|
||||
+ " 'TestRepository', 'queryMethod', 50, true)"
|
||||
})
|
||||
void pageFetchBy_givenValidQuery_shouldReturnCorrectResults() {
|
||||
// arrange
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
queryDto.setClassName("Test");
|
||||
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
|
||||
|
||||
// action
|
||||
Result<Record> result = aopLogRepository.pageFetchBy(pageRequestDto, queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).hasSize(3);
|
||||
assertThat(result.get(0).getValue("total_count", Long.class)).isEqualTo(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
|
||||
+ " 'TestController', 'method1', 100, true)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
|
||||
+ " 'TestService', 'method2', 200, false)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
|
||||
+ " 'TestRepository', 'method3', 50, true)"
|
||||
})
|
||||
void fetchBy_givenClassNameQuery_shouldReturnFilteredResults() {
|
||||
// arrange
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
queryDto.setClassName("TestController");
|
||||
|
||||
// action
|
||||
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getClassName()).isEqualTo("TestController");
|
||||
assertThat(result.get(0).getMethodName()).isEqualTo("method1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
|
||||
+ " 'TestController', 'method1', 100, true)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
|
||||
+ " 'TestService', 'method2', 200, false)"
|
||||
})
|
||||
void fetchBy_givenSuccessQuery_shouldReturnOnlySuccessfulLogs() {
|
||||
// arrange
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
queryDto.setSuccess(true);
|
||||
|
||||
// action
|
||||
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getSuccess()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
|
||||
+ " 'TestController', 'method1', 50, true)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
|
||||
+ " 'TestService', 'method2', 150, false)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
|
||||
+ " 'TestRepository', 'method3', 250, true)"
|
||||
})
|
||||
void fetchBy_givenExecutionTimeRange_shouldReturnFilteredResults() {
|
||||
// arrange
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
queryDto.setMinExecutionTime(100L);
|
||||
queryDto.setMaxExecutionTime(200L);
|
||||
|
||||
// action
|
||||
List<AopLog> result = aopLogRepository.fetchBy(queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).hasSize(1);
|
||||
assertThat(result.get(0).getExecutionTime()).isEqualTo(150L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (1,"
|
||||
+ " 'TestController', 'method1', 100, true)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (2,"
|
||||
+ " 'TestService', 'method2', 200, false)",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success) VALUES (3,"
|
||||
+ " 'TestRepository', 'method3', 50, true)"
|
||||
})
|
||||
void deleteByIds_givenValidIds_shouldDeleteCorrectRecords() {
|
||||
// arrange
|
||||
List<Long> idsToDelete = List.of(1L, 3L);
|
||||
|
||||
// action
|
||||
int deletedCount = aopLogRepository.deleteByIds(idsToDelete);
|
||||
|
||||
// assert
|
||||
assertThat(deletedCount).isEqualTo(2);
|
||||
|
||||
// verify remaining record
|
||||
List<AopLog> remaining = aopLogRepository.findAll();
|
||||
assertThat(remaining).hasSize(1);
|
||||
assertThat(remaining.get(0).getId()).isEqualTo(2L);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Sql(
|
||||
statements = {
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
|
||||
+ " create_time) VALUES (1, 'TestController', 'method1', 100, true, '2023-01-01"
|
||||
+ " 00:00:00+00')",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
|
||||
+ " create_time) VALUES (2, 'TestService', 'method2', 200, false, '2023-06-01"
|
||||
+ " 00:00:00+00')",
|
||||
"INSERT INTO mjga.aop_log (id, class_name, method_name, execution_time, success,"
|
||||
+ " create_time) VALUES (3, 'TestRepository', 'method3', 50, true, '2023-12-01"
|
||||
+ " 00:00:00+00')"
|
||||
})
|
||||
void deleteBeforeTime_givenValidTime_shouldDeleteOldRecords() {
|
||||
// arrange
|
||||
OffsetDateTime cutoffTime = OffsetDateTime.parse("2023-07-01T00:00:00Z");
|
||||
|
||||
// action
|
||||
int deletedCount = aopLogRepository.deleteBeforeTime(cutoffTime);
|
||||
|
||||
// assert
|
||||
assertThat(deletedCount).isEqualTo(2);
|
||||
|
||||
// verify remaining record
|
||||
List<AopLog> remaining = aopLogRepository.findAll();
|
||||
assertThat(remaining).hasSize(1);
|
||||
assertThat(remaining.get(0).getId()).isEqualTo(3L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByIds_givenEmptyList_shouldReturnZero() {
|
||||
// arrange
|
||||
List<Long> emptyIds = List.of();
|
||||
|
||||
// action
|
||||
int deletedCount = aopLogRepository.deleteByIds(emptyIds);
|
||||
|
||||
// assert
|
||||
assertThat(deletedCount).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteByIds_givenNullList_shouldReturnZero() {
|
||||
// arrange & action
|
||||
int deletedCount = aopLogRepository.deleteByIds(null);
|
||||
|
||||
// assert
|
||||
assertThat(deletedCount).isEqualTo(0);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import com.zl.mjga.repository.RoleRepository;
|
||||
import com.zl.mjga.repository.UserRepository;
|
||||
import com.zl.mjga.service.IdentityAccessService;
|
||||
import com.zl.mjga.service.SignService;
|
||||
import com.zl.mjga.service.UploadService;
|
||||
import io.minio.MinioClient;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Collections;
|
||||
@@ -53,6 +54,7 @@ public class AuthenticationAndAuthorityTest {
|
||||
@MockBean private PermissionRepository permissionRepository;
|
||||
@MockBean private MinioClient minioClient;
|
||||
@MockBean private MinIoConfig minIoConfig;
|
||||
@MockBean private UploadService uploadService;
|
||||
|
||||
@Test
|
||||
public void givenRequestOnPublicService_shouldSucceedWith200() throws Exception {
|
||||
|
||||
134
backend/src/test/java/com/zl/mjga/unit/AopLogServiceTest.java
Normal file
134
backend/src/test/java/com/zl/mjga/unit/AopLogServiceTest.java
Normal file
@@ -0,0 +1,134 @@
|
||||
package com.zl.mjga.unit;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import com.zl.mjga.dto.PageRequestDto;
|
||||
import com.zl.mjga.dto.PageResponseDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogQueryDto;
|
||||
import com.zl.mjga.dto.aoplog.AopLogRespDto;
|
||||
import com.zl.mjga.repository.AopLogRepository;
|
||||
import com.zl.mjga.service.AopLogService;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.List;
|
||||
import org.jooq.Record;
|
||||
import org.jooq.Result;
|
||||
import org.jooq.generated.mjga.tables.pojos.AopLog;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class AopLogServiceTest {
|
||||
|
||||
@Mock private AopLogRepository aopLogRepository;
|
||||
|
||||
@Mock private Result<Record> mockResult;
|
||||
|
||||
@Mock private Record mockRecord;
|
||||
|
||||
@InjectMocks private AopLogService aopLogService;
|
||||
|
||||
@Test
|
||||
void saveLogAsync_givenValidAopLog_shouldCallRepositoryInsert() {
|
||||
// arrange
|
||||
AopLog aopLog = createTestAopLog();
|
||||
|
||||
// action
|
||||
aopLogService.saveLogAsync(aopLog);
|
||||
|
||||
// assert
|
||||
verify(aopLogRepository, times(1)).insert(aopLog);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageQueryAopLogs_givenValidRequest_shouldReturnPageResponse() {
|
||||
// arrange
|
||||
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
|
||||
when(aopLogRepository.pageFetchBy(pageRequestDto, queryDto)).thenReturn(mockResult);
|
||||
when(mockResult.isEmpty()).thenReturn(false);
|
||||
when(mockResult.map(any())).thenReturn(List.of(createTestAopLogRespDto()));
|
||||
when(mockResult.get(0)).thenReturn(mockRecord);
|
||||
when(mockRecord.getValue("total_count", Long.class)).thenReturn(1L);
|
||||
|
||||
// action
|
||||
PageResponseDto<List<AopLogRespDto>> result =
|
||||
aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotal()).isEqualTo(1L);
|
||||
assertThat(result.getData()).hasSize(1);
|
||||
verify(aopLogRepository, times(1)).pageFetchBy(pageRequestDto, queryDto);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pageQueryAopLogs_givenEmptyResult_shouldReturnEmptyPage() {
|
||||
// arrange
|
||||
PageRequestDto pageRequestDto = PageRequestDto.of(1, 10);
|
||||
AopLogQueryDto queryDto = new AopLogQueryDto();
|
||||
|
||||
when(aopLogRepository.pageFetchBy(pageRequestDto, queryDto)).thenReturn(mockResult);
|
||||
when(mockResult.isEmpty()).thenReturn(true);
|
||||
|
||||
// action
|
||||
PageResponseDto<List<AopLogRespDto>> result =
|
||||
aopLogService.pageQueryAopLogs(pageRequestDto, queryDto);
|
||||
|
||||
// assert
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotal()).isEqualTo(0L);
|
||||
assertThat(result.getData()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteLogsBeforeTime_givenValidTime_shouldReturnDeletedCount() {
|
||||
// arrange
|
||||
OffsetDateTime beforeTime = OffsetDateTime.now().minusDays(30);
|
||||
when(aopLogRepository.deleteBeforeTime(beforeTime)).thenReturn(10);
|
||||
|
||||
// action
|
||||
int result = aopLogService.deleteLogsBeforeTime(beforeTime);
|
||||
|
||||
// assert
|
||||
assertThat(result).isEqualTo(10);
|
||||
verify(aopLogRepository, times(1)).deleteBeforeTime(beforeTime);
|
||||
}
|
||||
|
||||
private AopLog createTestAopLog() {
|
||||
AopLog aopLog = new AopLog();
|
||||
aopLog.setClassName("TestController");
|
||||
aopLog.setMethodName("testMethod");
|
||||
aopLog.setMethodArgs("[\"arg1\"]");
|
||||
aopLog.setReturnValue("\"result\"");
|
||||
aopLog.setExecutionTime(100L);
|
||||
aopLog.setSuccess(true);
|
||||
aopLog.setUserId(1L);
|
||||
aopLog.setIpAddress("127.0.0.1");
|
||||
aopLog.setUserAgent("Test Agent");
|
||||
aopLog.setCreateTime(OffsetDateTime.now());
|
||||
return aopLog;
|
||||
}
|
||||
|
||||
private AopLogRespDto createTestAopLogRespDto() {
|
||||
return AopLogRespDto.builder()
|
||||
.id(1L)
|
||||
.className("TestController")
|
||||
.methodName("testMethod")
|
||||
.methodArgs("[\"arg1\"]")
|
||||
.returnValue("\"result\"")
|
||||
.executionTime(100L)
|
||||
.success(true)
|
||||
.userId(1L)
|
||||
.username("testUser")
|
||||
.ipAddress("127.0.0.1")
|
||||
.userAgent("Test Agent")
|
||||
.createTime(OffsetDateTime.now())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
-- AOP日志表
|
||||
CREATE TABLE mjga.aop_log (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
class_name VARCHAR NOT NULL,
|
||||
method_name VARCHAR NOT NULL,
|
||||
method_args TEXT,
|
||||
return_value TEXT,
|
||||
execution_time BIGINT NOT NULL,
|
||||
success BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
error_message TEXT,
|
||||
user_id BIGINT,
|
||||
ip_address VARCHAR(45),
|
||||
user_agent TEXT,
|
||||
create_time TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES mjga.user(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX idx_aop_log_class_name ON mjga.aop_log(class_name);
|
||||
CREATE INDEX idx_aop_log_method_name ON mjga.aop_log(method_name);
|
||||
CREATE INDEX idx_aop_log_create_time ON mjga.aop_log(create_time);
|
||||
CREATE INDEX idx_aop_log_user_id ON mjga.aop_log(user_id);
|
||||
CREATE INDEX idx_aop_log_success ON mjga.aop_log(success);
|
||||
@@ -45,4 +45,30 @@ export default [
|
||||
message: "Llm updated successfully",
|
||||
});
|
||||
}),
|
||||
http.post("/ai/chat/refresh", () => {
|
||||
return HttpResponse.json({
|
||||
success: true,
|
||||
message: "Conversation cleared successfully",
|
||||
});
|
||||
}),
|
||||
http.post("/ai/action/execute", () => {
|
||||
const response = new HttpResponse(`data: ${faker.lorem.paragraph()}\n\n`, {
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
},
|
||||
});
|
||||
return response;
|
||||
}),
|
||||
http.delete("/ai/action/department", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
http.delete("/ai/action/position", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
http.delete("/ai/action/role", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
http.delete("/ai/action/permission", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
];
|
||||
|
||||
92
frontend/src/api/mocks/knowledgeHandlers.ts
Normal file
92
frontend/src/api/mocks/knowledgeHandlers.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
import { http, HttpResponse } from "msw";
|
||||
|
||||
// 生成模拟的知识库数据
|
||||
const generateLibrary = () => ({
|
||||
id: faker.number.int({ min: 1, max: 100 }),
|
||||
name: faker.lorem.words(2),
|
||||
description: faker.lorem.sentence(),
|
||||
createTime: faker.date.recent().toISOString()
|
||||
});
|
||||
|
||||
// 生成模拟的文档数据
|
||||
const generateDoc = (libId: number) => ({
|
||||
id: faker.number.int({ min: 1, max: 1000 }),
|
||||
libId,
|
||||
name: faker.system.fileName(),
|
||||
identify: faker.string.uuid(),
|
||||
path: faker.system.filePath(),
|
||||
meta: {},
|
||||
enable: faker.datatype.boolean(),
|
||||
status: faker.helpers.arrayElement(["SUCCESS", "INDEXING"]),
|
||||
createTime: faker.date.recent().toISOString(),
|
||||
updateTime: faker.date.recent().toISOString()
|
||||
});
|
||||
|
||||
// 生成模拟的文档段落数据
|
||||
const generateSegment = (docId: number) => ({
|
||||
id: faker.number.int({ min: 1, max: 10000 }),
|
||||
docId,
|
||||
embeddingId: faker.string.uuid(),
|
||||
content: faker.lorem.paragraphs(),
|
||||
tokenUsage: faker.number.int({ min: 10, max: 1000 })
|
||||
});
|
||||
|
||||
export default [
|
||||
// 获取知识库列表
|
||||
http.get("/knowledge/libraries", () => {
|
||||
const libraries = faker.helpers.multiple(generateLibrary, { count: 5 });
|
||||
return HttpResponse.json(libraries);
|
||||
}),
|
||||
|
||||
// 获取文档列表
|
||||
http.get("/knowledge/docs", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const libraryId = Number(url.searchParams.get("libraryId"));
|
||||
|
||||
if (Number.isNaN(libraryId)) {
|
||||
return new HttpResponse(null, { status: 400 });
|
||||
}
|
||||
|
||||
const docs = faker.helpers.multiple(() => generateDoc(libraryId), { count: 8 });
|
||||
return HttpResponse.json(docs);
|
||||
}),
|
||||
|
||||
// 获取文档段落
|
||||
http.get("/knowledge/segments", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const libraryDocId = Number(url.searchParams.get("libraryDocId"));
|
||||
|
||||
if (Number.isNaN(libraryDocId)) {
|
||||
return new HttpResponse(null, { status: 400 });
|
||||
}
|
||||
|
||||
const segments = faker.helpers.multiple(() => generateSegment(libraryDocId), { count: 12 });
|
||||
return HttpResponse.json(segments);
|
||||
}),
|
||||
|
||||
// 创建/更新知识库
|
||||
http.post("/knowledge/library", async () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// 删除知识库
|
||||
http.delete("/knowledge/library", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// 更新文档
|
||||
http.put("/knowledge/doc", async () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// 删除文档
|
||||
http.delete("/knowledge/doc", () => {
|
||||
return HttpResponse.json({ success: true });
|
||||
}),
|
||||
|
||||
// 上传文档
|
||||
http.post("/knowledge/doc/upload", async () => {
|
||||
return HttpResponse.text("upload-success");
|
||||
}),
|
||||
];
|
||||
@@ -7,6 +7,8 @@ import userHandlers from "./iamHandlers";
|
||||
import departmentHandlers from "./departmentHandlers";
|
||||
import positionHandlers from "./positionHandlers";
|
||||
import aiHandlers from "./aiHandlers";
|
||||
import knowledgeHandlers from "./knowledgeHandlers";
|
||||
|
||||
export const worker = setupWorker(
|
||||
...userHandlers,
|
||||
...authHandlers,
|
||||
@@ -16,4 +18,5 @@ export const worker = setupWorker(
|
||||
...departmentHandlers,
|
||||
...positionHandlers,
|
||||
...aiHandlers,
|
||||
...knowledgeHandlers,
|
||||
);
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
<Avatar :src="user.avatar" size="sm" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-sm shadow-sm "
|
||||
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-sm shadow-sm"
|
||||
id="dropdown-user">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm font-medium text-gray-900 truncate " role="none">
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
<template>
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th v-if="hasCheckbox" scope="col" class="p-4 w-4">
|
||||
<div class="flex items-center">
|
||||
<input id="checkbox-all-search" type="checkbox" v-model="allChecked" @change="handleAllCheckedChange"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
|
||||
<label for="checkbox-all-search" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</th>
|
||||
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[
|
||||
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-gray-500 whitespace-nowrap">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
||||
<tr>
|
||||
<th v-if="hasCheckbox" scope="col" class="p-4 w-4">
|
||||
<div class="flex items-center">
|
||||
<input id="checkbox-all-search" type="checkbox" v-model="allChecked" @change="handleAllCheckedChange"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
|
||||
<label for="checkbox-all-search" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</th>
|
||||
<th v-for="(column, index) in columns" :key="index" scope="col" :class="[
|
||||
'px-6 py-3',
|
||||
column.sortable ? 'cursor-pointer' : '',
|
||||
column.class || ''
|
||||
]" @click="column.sortable ? handleSortClick(column.field) : null">
|
||||
<div class="flex items-center">
|
||||
<span>{{ column.title }}</span>
|
||||
<slot v-if="column.sortable" name="sort-icon" :field="column.field"></slot>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, rowIndex) in items" :key="getItemKey(item, rowIndex)"
|
||||
class="bg-white border-b border-gray-200 hover:bg-gray-50">
|
||||
<td v-if="hasCheckbox" class="w-4 p-4">
|
||||
<div class="flex items-center">
|
||||
<input :id="`checkbox-table-search-${rowIndex}`" :value="getItemId(item)" type="checkbox"
|
||||
v-model="checkedItems"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
|
||||
<label :for="`checkbox-table-search-${rowIndex}`" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</td>
|
||||
<td v-for="(column, colIndex) in columns" :key="colIndex" :class="[
|
||||
<div class="flex items-center">
|
||||
<span>{{ column.title }}</span>
|
||||
<slot v-if="column.sortable" name="sort-icon" :field="column.field"></slot>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, rowIndex) in items" :key="getItemKey(item, rowIndex)"
|
||||
class="bg-white border-b border-gray-200 hover:bg-gray-50">
|
||||
<td v-if="hasCheckbox" class="w-4 p-4">
|
||||
<div class="flex items-center">
|
||||
<input :id="`checkbox-table-search-${rowIndex}`" :value="getItemId(item)" type="checkbox"
|
||||
v-model="checkedItems"
|
||||
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded-sm focus:ring-blue-500 focus:ring-2">
|
||||
<label :for="`checkbox-table-search-${rowIndex}`" class="sr-only">checkbox</label>
|
||||
</div>
|
||||
</td>
|
||||
<td v-for="(column, colIndex) in columns" :key="colIndex" :class="[
|
||||
'px-6 py-4',
|
||||
column.class || '',
|
||||
colIndex === 0 ? 'font-medium text-gray-900' : ''
|
||||
]">
|
||||
<slot :name="column.field" :item="item" :index="rowIndex">
|
||||
<div class="max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ getItemValue(item, column.field) }}
|
||||
</div>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<slot :name="column.field" :item="item" :index="rowIndex">
|
||||
<div class="max-w-sm whitespace-nowrap overflow-hidden text-ellipsis">
|
||||
{{ getItemValue(item, column.field) }}
|
||||
</div>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup generic="T" lang="ts">
|
||||
|
||||
Reference in New Issue
Block a user