后端
实体
src/main/java/com/weiyu/modules/common/entity/PerformanceLog.java
package com.weiyu.modules.common.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.weiyu.modules.common.enums.PerformanceType;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 性能日志实体
*/
@Data
@NoArgsConstructor
public class PerformanceLog {
/**
* 主键 ID
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
* 类型
*/
private PerformanceType performanceType;
/**
* 操作时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operationTime;
/**
* 包名
*/
private String packageName;
/**
* 类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
/**
* 方法参数类型
*/
private String methodParamTypes;
/**
* 方法参数名
*/
private String methodParamNames;
/**
* 方法参数值
*/
private String methodParamValues;
/**
* 方法返回类型
*/
private String methodReturnType;
/**
* 方法返回值
*/
private String methodReturnValue;
/**
* 执行耗时
*/
private Long executionTimeDuration;
}
DTO
request
src/main/java/com/weiyu/modules/common/dto/request/PerformanceLogCreateRequest.java
package com.weiyu.modules.common.dto.request;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.weiyu.modules.common.enums.PerformanceType;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 性能日志创建请求对象
*/
@Schema(description = "性能日志创建请求对象")
@Data
@NoArgsConstructor
public class PerformanceLogCreateRequest {
/**
* 类型
*/
@Schema(description = "类型")
@NotNull(message = "类型不能为 null")
private PerformanceType performanceType;
/**
* 操作时间
*/
@Schema(description = "操作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operationTime;
/**
* 包名
*/
@Schema(description = "包名")
@NotBlank(message = "包名不能为空")
@Size(max = 100, message = "包名长度不能超过100个字符")
private String packageName;
/**
* 类名
*/
@Schema(description = "类名")
@NotBlank(message = "类名不能为空")
@Size(max = 100, message = "类名长度不能超过100个字符")
private String className;
/**
* 方法名
*/
@Schema(description = "方法名")
@NotBlank(message = "方法名不能为空")
@Size(max = 100, message = "方法名长度不能超过100个字符")
private String methodName;
/**
* 方法参数类型
*/
@Schema(description = "方法参数类型")
@Size(max = 100, message = "方法参数类型长度不能超过1000个字符")
private String methodParamTypes;
/**
* 方法参数名
*/
@Schema(description = "方法参数名")
@Size(max = 100, message = "方法参数名长度不能超过100个字符")
private String methodParamNames;
/**
* 方法参数值
*/
@Schema(description = "方法参数值")
private String methodParamValues;
/**
* 方法返回类型
*/
@Schema(description = "方法返回类型")
@Size(max = 100, message = "方法返回类型长度不能超过100个字符")
private String methodReturnType;
/**
* 方法返回值
*/
@Schema(description = "方法返回值")
private String methodReturnValue;
/**
* 执行耗时
*/
@Schema(description = "执行耗时")
@NotNull(message = "执行耗时不能为 null")
@Min(value = 0, message = "执行耗时不能小于0")
private Long executionTimeDuration;
}
src/main/java/com/weiyu/modules/common/dto/request/PerformanceLogQueryRequest.java
package com.weiyu.modules.common.dto.request;
import com.weiyu.framework.dto.BasePageQuery;
import com.weiyu.framework.dto.DateRange;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* 性能日志查询请求对象
* <p>支持按类型、方法名、执行耗时及创建日期范围进行分页查询。所有查询条件均为可选。</p>
*/
@Schema(description = "性能日志查询请求对象")
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true) // equals() 和 hashCode() 方法才会包含父类的字段,默认 callSuper = false 不会包含父类的字段
// toString() 方法才会包含父类的字段(分页参数),默认 callSuper = false 不会包含父类的字段,
// 如实例序列化内容:queryRequest=LogQueryRequest(super=BasePageQuery(current=1, size=100), performanceType=, methodName=, executionTimeDuration=, createDateRange=DateRange(beginDate=2026-04-01, endDate=2026-04-30))]
@ToString(callSuper = true)
public class PerformanceLogQueryRequest extends BasePageQuery {
/**
* 类型 1:SQL
* <p>使用 Integer 代替枚举 PerformanceType,确保 OpenAPI 生成 number 类型,nullable = true 使其可为 null</p>
*/
// 🚀 DTO 里,坚决、绝对、永远不要使用带 @JsonValue 的枚举!
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: '1';
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: '1' | '1';
// @Schema(description = "类型 1:SQL", type = "integer") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🚀 int 不支持 null
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🔥最佳实践:✅ DTO 字段类型:Integer(数字)、String(字符串)、Boolean(布尔)、日期类型、自定义DTO;❌ DTO 字段严禁使用:带 @JsonValue 的枚举、带 @EnumValue 的枚举
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", examples = {"1"}) // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // // API 生成的前端类型是 performanceType?: 1;
@Schema(description = "类型 1:SQL", nullable = true) // API 生成的前端类型是 performanceType?: number | null;
private Integer performanceType;
/**
* 方法名
*/
@Schema(description = "方法名") // 生成前端类型:methodName?: string;
private String methodName;
/**
* 执行耗时(毫秒)
*/
@Schema(description = "执行耗时(毫秒)", nullable = true) // 生成前端类型:executionTimeDuration?: number | null;
private Long executionTimeDuration;
/**
* 创建日期范围
* <p>传入 yyyy-MM-dd 格式的起止日期,后端自动转为当天 00:00:00 至 23:59:59.999999999 的时间区间。</p>
*/
@Schema(description = "创建日期范围") // 生成前端类型:createDateRange: DateRange; // createDateRange 的上面没有生成 javaDoc 注释
@NotNull(message = "查询日期不能为 null")
@Valid // 启用嵌套校验,DateRange 中的 @Pattern 和 @AssertTrue 会生效
private DateRange createDateRange;
}
response
src/main/java/com/weiyu/modules/common/dto/response/PerformanceLogBriefInfoResponse.java
package com.weiyu.modules.common.dto.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 性能日志简略信息响应对象
*/
@Schema(description = "性能日志简略信息响应对象")
@Data
@NoArgsConstructor
public class PerformanceLogBriefInfoResponse {
/**
* 主键 ID
*/
@Schema(description = "主键 ID")
private Integer id;
/**
* 类型 1:SQL
* <p>使用 Integer 代替枚举 PerformanceType,确保 OpenAPI 生成 number 类型</p>
*/
// 🚀 DTO 里,坚决、绝对、永远不要使用带 @JsonValue 的枚举!
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: '1';
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: '1' | '1';
// @Schema(description = "类型 1:SQL", type = "integer") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🚀 int 不支持 null
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🔥最佳实践:✅ DTO 字段类型:Integer(数字)、String(字符串)、Boolean(布尔)、日期类型、自定义DTO;❌ DTO 字段严禁使用:带 @JsonValue 的枚举、带 @EnumValue 的枚举
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", examples = {"1"}) // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // // API 生成的前端类型是 performanceType?: 1;
@Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: number;
private Integer performanceType;
/**
* 操作时间
*/
@Schema(description = "操作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operationTime;
/**
* 包名
*/
@Schema(description = "包名")
private String packageName;
/**
* 类名
*/
@Schema(description = "类名")
private String className;
/**
* 方法名
*/
@Schema(description = "方法名")
private String methodName;
/**
* 方法参数类型
*/
@Schema(description = "方法参数类型")
private String methodParamTypes;
/**
* 方法参数名
*/
@Schema(description = "方法参数名")
private String methodParamNames;
/**
* 方法返回类型
*/
@Schema(description = "方法返回类型")
private String methodReturnType;
/**
* 执行耗时
*/
@Schema(description = "执行耗时")
private Long executionTimeDuration;
}
src/main/java/com/weiyu/modules/common/dto/response/PerformanceLogDetailInfoResponse.java
package com.weiyu.modules.common.dto.response;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* 性能日志详情信息响应对象
*/
@Schema(description = "性能日志详情信息响应对象")
@Data
@NoArgsConstructor
public class PerformanceLogDetailInfoResponse {
/**
* 主键 ID
*/
@Schema(description = "主键 ID")
private Integer id;
/**
* 类型 1:SQL
* <p>使用 Integer 代替枚举 PerformanceType,确保 OpenAPI 生成 number 类型</p>
*/
// 🚀 DTO 里,坚决、绝对、永远不要使用带 @JsonValue 的枚举!
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: '1';
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: '1' | '1';
// @Schema(description = "类型 1:SQL", type = "integer") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🚀 int 不支持 null
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // API 生成的前端类型是 performanceType?: 1;
// private PerformanceType performanceType;
// 🔥最佳实践:✅ DTO 字段类型:Integer(数字)、String(字符串)、Boolean(布尔)、日期类型、自定义DTO;❌ DTO 字段严禁使用:带 @JsonValue 的枚举、带 @EnumValue 的枚举
// @Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: number;
// @Schema(description = "类型 1:SQL", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", allowableValues = "1") // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", type = "integer", examples = {"1"}) // API 生成的前端类型是 performanceType?: never;
// @Schema(description = "类型 1:SQL", allowableValues = {"1"}) // // API 生成的前端类型是 performanceType?: 1;
@Schema(description = "类型 1:SQL") // API 生成的前端类型是 performanceType?: number;
private Integer performanceType;
/**
* 操作时间
*/
@Schema(description = "操作时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime operationTime;
/**
* 包名
*/
@Schema(description = "包名")
private String packageName;
/**
* 类名
*/
@Schema(description = "类名")
private String className;
/**
* 方法名
*/
@Schema(description = "方法名")
private String methodName;
/**
* 方法参数类型
*/
@Schema(description = "方法参数类型")
private String methodParamTypes;
/**
* 方法参数名
*/
@Schema(description = "方法参数名")
private String methodParamNames;
/**
* 方法参数值
*/
@Schema(description = "方法参数值")
private String methodParamValues;
/**
* 方法返回类型
*/
@Schema(description = "方法返回类型")
private String methodReturnType;
/**
* 方法返回值
*/
@Schema(description = "方法返回值")
private String methodReturnValue;
/**
* 执行耗时
*/
@Schema(description = "执行耗时")
private Long executionTimeDuration;
}
枚举
src/main/java/com/weiyu/modules/common/enums/PerformanceType.java
package com.weiyu.modules.common.enums;
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
/**
* 性能类型枚举
*/
@Getter
public enum PerformanceType {
/**
* SQL
*/
SQL(1, "SQL");
@EnumValue // MyBatis-Plus 数据库映射,标记使用该字段的值存入数据库,保存到数据库就是 0、1、2 等整数
@JsonValue // Jackson JSON 序列化,标记使用该字段的值进行序列化,前端接收到的就是 0、1、2 等数字
private final Integer value;
private final String displayName;
PerformanceType(Integer value, String displayName) {
this.value = value;
this.displayName = displayName;
}
/**
* 根据值获取枚举实例
*/
public static PerformanceType fromValue(Integer value) {
if (value == null) return null;
for (PerformanceType type : values()) {
if (type.value.equals(value)) {
return type;
}
}
return null;
}
}
控制器
src/main/java/com/weiyu/modules/common/controller/PerformanceLogController.java
package com.weiyu.modules.common.controller;
import com.weiyu.framework.dto.PageResult;
import com.weiyu.framework.dto.Result;
import com.weiyu.modules.common.annotation.LogDetail;
import com.weiyu.modules.common.annotation.LogModule;
import com.weiyu.modules.common.dto.request.PerformanceLogQueryRequest;
import com.weiyu.modules.common.dto.response.PerformanceLogBriefInfoResponse;
import com.weiyu.modules.common.dto.response.PerformanceLogDetailInfoResponse;
import com.weiyu.modules.common.service.PerformanceLogService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 性能日志
*/
@RestController
@RequestMapping("/performance-logs")
@RequiredArgsConstructor
// @Validated:激活方法参数校验(如 @PathVariable、@RequestParam 上的 @Min、@NotNull 和自定义校验 @FileType、@FileSize 等)
// @Validated 对 @RequestBody 无效,请求体嵌套校验仍需在参数前加 @Valid
@Validated
@LogModule("性能日志")
public class PerformanceLogController {
private final PerformanceLogService performanceLogService;
/**
* 分页查询(简略信息)
*
* @param queryRequest 性能日志查询请求对象
* @return 分页结果 {@link Result}<{@link PageResult}<{@link PerformanceLogBriefInfoResponse}>>
*/
@PostMapping("/query")
@LogDetail("分页查询(简略信息)")
public Result<PageResult<PerformanceLogBriefInfoResponse>> queryPerformanceLogBriefPage(
@RequestBody @Valid PerformanceLogQueryRequest queryRequest) {
PageResult<PerformanceLogBriefInfoResponse> pageResult =
performanceLogService.queryPerformanceLogBriefPage(queryRequest);
return Result.success(pageResult);
}
/**
* 查询详情
*
* @param id 主键 ID
* @return 性能日志详情 {@link Result}<{@link PerformanceLogDetailInfoResponse}>
*/
@GetMapping("/{id}")
@LogDetail("查询详情")
public Result<PerformanceLogDetailInfoResponse> queryPerformanceLogDetailInfoById(
@PathVariable @Min(value = 1, message = "主键 ID 不能少于 1") Integer id) {
PerformanceLogDetailInfoResponse response = performanceLogService.queryPerformanceLogDetailInfoById(id);
return Result.success(response);
}
}
服务层
src/main/java/com/weiyu/modules/common/service/PerformanceLogService.java
package com.weiyu.modules.common.service;
import com.weiyu.framework.dto.PageResult;
import com.weiyu.modules.common.dto.request.PerformanceLogCreateRequest;
import com.weiyu.modules.common.dto.request.PerformanceLogQueryRequest;
import com.weiyu.modules.common.dto.response.PerformanceLogBriefInfoResponse;
import com.weiyu.modules.common.dto.response.PerformanceLogDetailInfoResponse;
/**
* 性能日志
*/
public interface PerformanceLogService {
/**
* 分页查询(简略信息)
*
* @param queryRequest 性能日志查询请求对象
* @return 分页结果
*/
PageResult<PerformanceLogBriefInfoResponse> queryPerformanceLogBriefPage(PerformanceLogQueryRequest queryRequest);
/**
* 查询详情
*
* @param id 主键 ID
* @return 性能日志对象
*/
PerformanceLogDetailInfoResponse queryPerformanceLogDetailInfoById(Integer id);
/**
* 新增性能日志
*
* @param createRequest 性能日志创建请求对象
*/
void add(PerformanceLogCreateRequest createRequest);
}
src/main/java/com/weiyu/modules/common/service/impl/PerformanceLogServiceImpl.java
package com.weiyu.modules.common.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.weiyu.framework.dto.DateRange;
import com.weiyu.framework.dto.PageResult;
import com.weiyu.framework.util.BeanConvertUtils;
import com.weiyu.modules.common.converter.PerformanceLogConverter;
import com.weiyu.modules.common.dto.request.PerformanceLogCreateRequest;
import com.weiyu.modules.common.dto.request.PerformanceLogQueryRequest;
import com.weiyu.modules.common.dto.response.PerformanceLogBriefInfoResponse;
import com.weiyu.modules.common.dto.response.PerformanceLogDetailInfoResponse;
import com.weiyu.modules.common.entity.PerformanceLog;
import com.weiyu.modules.common.mapper.PerformanceLogMapper;
import com.weiyu.modules.common.service.PerformanceLogService;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
/**
* 性能日志
*/
@Service
@RequiredArgsConstructor
public class PerformanceLogServiceImpl implements PerformanceLogService {
private final PerformanceLogMapper performanceLogMapper;
private final PerformanceLogConverter performanceLogConverter;
/**
* 分页查询(简略信息)
*
* @param queryRequest 性能日志查询请求对象
* @return 分页结果
*/
@Override
public PageResult<PerformanceLogBriefInfoResponse> queryPerformanceLogBriefPage(PerformanceLogQueryRequest queryRequest) {
// 处理日期范围
DateRange dateRange = queryRequest.getCreateDateRange();
LocalDateTime createBeginTime = dateRange != null ? dateRange.getBeginDateTime().orElse(null) : null;
LocalDateTime createEndTime = dateRange != null ? dateRange.getEndDateTime().orElse(null) : null;
// 构建查询
LambdaQueryWrapper<PerformanceLog> wrapper = Wrappers.lambdaQuery();
wrapper
// 日期范围 between ... and ...
.between(
createBeginTime != null && createEndTime != null,
PerformanceLog::getOperationTime,
createBeginTime,
createEndTime
)
// 类型 = ?
.eq(
queryRequest.getPerformanceType() != null,
PerformanceLog::getPerformanceType,
queryRequest.getPerformanceType()
)
// 方法名 like %?%
.like(
StringUtils.isNotBlank(queryRequest.getMethodName()),
PerformanceLog::getMethodName,
queryRequest.getMethodName()
)
// 执行耗时 > ?
.gt(
queryRequest.getExecutionTimeDuration() != null,
PerformanceLog::getExecutionTimeDuration,
queryRequest.getExecutionTimeDuration()
)
// 排序 desc
.orderByDesc(PerformanceLog::getId);
// ========== PageHelper 分页查询 ==========
// 1、分页查询,手动组装
// 1.1、启用分页
// PageHelper.startPage(queryRequest.getCurrent(), queryRequest.getSize());
// 1.2、查询列表
// List<PerformanceLog> PerformanceLogs = performanceLogMapper.selectList(wrapper);
// 1.3、转换成分页,使用 Page 包装查询结果
// Page<PerformanceLog> page = (Page<PerformanceLog>) PerformanceLogs;
// 1.3、转换成分页,使用 PageInfo 包装查询结果(推荐,更安全)
// PageInfo<PerformanceLog> page = new PageInfo<>(PerformanceLogs);
// 1、启动分页 + 查询列表,自动组装,使用 Lambda 表达式(更简洁)
PageInfo<PerformanceLog> page = PageHelper.startPage(queryRequest.getCurrent(), queryRequest.getSize())
.doSelectPageInfo(() -> performanceLogMapper.selectList(wrapper));
// 2、分页结果
return new PageResult<>(
page.getTotal(),
BeanConvertUtils.convertList(page.getList(), performanceLogConverter::toBriefInfoResponse)
);
}
/**
* 查询详情
*
* @param id 主键 ID
* @return 性能日志对象
*/
@Override
public PerformanceLogDetailInfoResponse queryPerformanceLogDetailInfoById(Integer id) {
PerformanceLog performanceLog = performanceLogMapper.selectById(id);
return performanceLogConverter.toDetailInfoResponse(performanceLog);
}
/**
* 新增性能日志
*
* @param createRequest 性能日志创建请求对象
*/
@Override
@Async // 异步执行,这个注解会导致新线程,ThreadLocal 丢失
public void add(PerformanceLogCreateRequest createRequest) {
PerformanceLog performanceLog = performanceLogConverter.toEntity(createRequest);
performanceLogMapper.insert(performanceLog);
}
}
Mapper
src/main/java/com/weiyu/modules/common/mapper/PerformanceLogMapper.java
package com.weiyu.modules.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.weiyu.modules.common.entity.PerformanceLog;
/**
* 性能日志
*/
public interface PerformanceLogMapper extends BaseMapper<PerformanceLog> {
}
转换器
src/main/java/com/weiyu/modules/common/converter/PerformanceLogConverter.java
package com.weiyu.modules.common.converter;
import com.weiyu.modules.common.dto.request.PerformanceLogCreateRequest;
import com.weiyu.modules.common.dto.response.PerformanceLogBriefInfoResponse;
import com.weiyu.modules.common.dto.response.PerformanceLogDetailInfoResponse;
import com.weiyu.modules.common.entity.PerformanceLog;
import com.weiyu.modules.common.enums.PerformanceType;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
import org.mapstruct.ReportingPolicy;
/**
* 性能日志转换器
* <p>1、只定义单个对象的转换方法</p>
* <p>2、使用 @Mapping 处理字段映射</p>
* <p>3、使用 @Named 定义自定义转换方法</p>
* <p>4、批量转换在 Service/Controller 中使用 BeanConvertUtils</p>
*
* <p>示例:</p>
* <p>List<Target> list = BeanConvertUtils.convertList(sourceList, convert::toTarget);</p>
* <p>PageResult<Target> page = BeanConvertUtils.convertPage(sourcePage, convert::toTarget);</p>
*/
// componentModel = "spring",指定生成的 Mapper 实现类使用 Spring 组件模型(生成 Spring Bean)
// unmappedTargetPolicy = ReportingPolicy.WARN,定义当目标属性无法从源属性映射时的处理策略,WARN:编译时会输出警告信息,提示有字段未被映射
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.WARN) // 生成 Spring Bean
public interface PerformanceLogConverter {
// ========== 基本转换 ==========
/**
* Request -> Entity
* <p>自动映射所有同名字段,无需额外配置。</p>
*/
PerformanceLog toEntity(PerformanceLogCreateRequest source);
/**
* Entity -> BriefInfoResponse
* <p>自动映射所有同名字段,无需额外配置。</p>
* <p>显式映射 performanceType,使用枚举的 value 字段而非 ordinal()。</p>
*/
@Mapping(target = "performanceType", source = "performanceType", qualifiedByName = "performanceTypeToInteger")
PerformanceLogBriefInfoResponse toBriefInfoResponse(PerformanceLog source);
/**
* Entity -> DetailInfoResponse
* <p>自动映射所有同名字段,无需额外配置。</p>
* <p>显式映射 performanceType,使用枚举的 value 字段而非 ordinal()。</p>
*/
@Mapping(target = "performanceType", source = "performanceType", qualifiedByName = "performanceTypeToInteger")
PerformanceLogDetailInfoResponse toDetailInfoResponse(PerformanceLog source);
// ========== 自定义转换方法 ==========
/**
* 将 PerformanceType 枚举转换为 Integer(使用其 value 字段)
*/
@Named("performanceTypeToInteger")
default Integer performanceTypeToInteger(PerformanceType performanceType) {
return performanceType == null ? null : performanceType.getValue();
}
}
前端
页面组件
src\views\query\PerformanceLogQuery.vue
<script setup lang="ts">
/**
* 性能日志查询
*/
defineOptions({
name: "PerformanceLogQuery"
});
import { performanceLogApi } from "@/api";
import { BasePreventReClickButton, CommonPageTitle, LayoutBasePage } from "@/components";
import { useModuleName } from "@/hooks";
import type {
PerformanceLogBriefInfoResponse,
PerformanceLogDetailInfoResponse,
PerformanceLogQueryRequest
} from "@/openapi/types.gen";
import { ModuleType } from "@/types";
import { formatDate } from "@/utils";
import { ElMessage } from "element-plus";
import { computed, nextTick, onBeforeUnmount, ref } from "vue";
// 模块名称
const { moduleName } = useModuleName(ModuleType.PERFORMANCE_LOG_QUERY);
// ========== 状态定义 ==========
// 表格数据
const tableData = ref<PerformanceLogBriefInfoResponse[]>([]);
// 表格加载标识
const tableLoading = ref(false);
// 查询请求对象
const queryRequest = ref<PerformanceLogQueryRequest>({
current: 1,
size: 100,
performanceType: null,
methodName: "",
executionTimeDuration: null,
createDateRange: { beginDate: "", endDate: "" } // 显式初始化,保持与 DateRange 结构一致
});
// 总条数
const total = ref(0);
// 更多筛选显示标识
const moreFilterVisible = ref(false);
/**
* 屏蔽标志(用于阻止程序修改分页参数时引发的不必要查询)
* - 重置时使用
* - 查询前将 current 重置为 1 时使用
*/
const isResetting = ref(false);
// 用于取消请求的 AbortController 实例(非响应式普通变量)
let abortController: AbortController | null = null;
// 详情
const detailData = ref<PerformanceLogDetailInfoResponse>();
// 抽屉显示标识
const drawerVisible = ref(false);
// ========== 计算属性 ==========
/**
* 日期范围双向绑定转换
* 将 el-date-picker 的 [string, string] 与后端 DateRange 对象相互转换
*/
const dateRangeValue = computed({
get: () => {
const { createDateRange } = queryRequest.value;
if (createDateRange?.beginDate && createDateRange?.endDate) {
return [createDateRange.beginDate, createDateRange.endDate];
}
return [];
},
set: (value: [string, string] | null) => {
if (value && value.length === 2) {
queryRequest.value.createDateRange = {
beginDate: value[0],
endDate: value[1]
};
} else {
queryRequest.value.createDateRange = { beginDate: "", endDate: "" };
}
}
});
// ========== 工具方法 ==========
/**
* 重置/创建新的 AbortController,用于取消上一次请求
*/
function resetAbortController(): AbortController {
abortController?.abort();
abortController = new AbortController();
return abortController;
}
// ========== 数据获取 ==========
// 获取数据
async function getData() {
// 取消上次请求,创建本次请求的控制器
const controller = resetAbortController();
try {
if (tableLoading.value) return;
tableData.value = [];
tableLoading.value = true;
// 查询日志
let result = await performanceLogApi.queryBriefPage(queryRequest.value);
tableData.value = result.data.rows;
total.value = result.data.total;
} catch (error: any) {
// 忽略取消请求的错误,对真实错误进行提示
if (error?.name !== "CanceledError" && error?.code !== "ERR_CANCELED") {
console.error("日志查询失败:", error);
ElMessage.error("查询失败,请稍后重试");
}
tableData.value = [];
total.value = 0;
} finally {
tableLoading.value = false;
abortController = null;
}
}
// ========== 事件处理 ==========
/**
* 分页改变
*/
const handlePageChange = async (currentPage: number, pageSize: number) => {
// 若正处于重置流程,忽略分页变化触发的查询
if (isResetting.value) return;
queryRequest.value.current = currentPage;
queryRequest.value.size = pageSize;
await getData();
};
/**
* 查询
*/
const handleQuery = async () => {
// 检查查询参数:日期范围必填
if (!queryRequest.value.createDateRange?.beginDate && !queryRequest.value.createDateRange?.endDate) {
ElMessage.warning("必须输入查询日期");
return;
}
// 抑制分页 change 事件,避免重复请求
isResetting.value = true;
queryRequest.value.current = 1;
// 重要:使用 setTimeout(0) 将 isResetting 恢复延迟至宏任务,
// 确保 Element Plus 分页组件可能异步触发的 change 事件在 isResetting 仍为 true 时执行完毕,
// 从而被 handlePageChange 中的判断拦截。
setTimeout(() => {
isResetting.value = false;
}, 0);
await getData();
};
/**
* 重置
* 终止当前请求,重置所有查询参数并清空表格,但不触发查询
*/
const handleReset = () => {
// 取消正在进行的请求
abortController?.abort();
abortController = null;
// 开启重置保护
isResetting.value = true;
// 清空表格与总数
tableData.value = [];
total.value = 0;
// 重置各个查询参数(保持对象引用不变,避免整体替换带来的副作用)
queryRequest.value.current = 1;
queryRequest.value.size = 100;
queryRequest.value.performanceType = null;
queryRequest.value.methodName = "";
queryRequest.value.executionTimeDuration = null;
queryRequest.value.createDateRange = { beginDate: "", endDate: "" };
// 重要:使用 setTimeout(0) 将 isResetting 恢复延迟至宏任务,
// 确保 Element Plus 分页组件可能异步触发的 change 事件在 isResetting 仍为 true 时执行完毕,
// 从而被 handlePageChange 中的判断拦截。
setTimeout(() => {
isResetting.value = false;
}, 0);
};
/**
* 更多筛选
*/
const handleMoreFilter = async () => {
moreFilterVisible.value = !moreFilterVisible.value;
};
/**
* 详情
*/
const handleDetail = async (id: number) => {
// 获取性能日志详情
const result = await performanceLogApi.queryDetailInfoById(id);
detailData.value = result.data;
drawerVisible.value = true;
// 等待 DOM 更新,再执行
await nextTick().then(() => {
// 抽屉滚动条回到顶部
document.querySelector(".el-drawer__body")?.scrollTo(0, 0);
});
};
// 组件卸载时清理未完成的请求,防止内存泄漏
onBeforeUnmount(() => {
abortController?.abort();
});
</script>
<template>
<LayoutBasePage>
<template #header>
<!-- 页面标题,不显示设置列宽按钮 -->
<CommonPageTitle :title="moduleName ?? `性能监控`" :set-column-width="false" />
<!-- 操作栏 -->
<el-form class="header-form" :model="queryRequest" :inline="true" :label-width="90">
<el-form-item label="查询日期:">
<el-date-picker
v-model="dateRangeValue"
style="width: 240px"
type="daterange"
start-placeholder="开始日期"
range-separator="至"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD">
</el-date-picker>
</el-form-item>
<el-form-item>
<BasePreventReClickButton class="btn-same-width" type="primary" plain :on-click="handleQuery" :delay="500">
查询
</BasePreventReClickButton>
<el-button class="btn-same-width" type="primary" plain @click="handleReset">重置</el-button>
<el-button class="btn-same-width" type="primary" plain @click="handleMoreFilter">更多筛选</el-button>
</el-form-item>
</el-form>
<el-form class="header-form" :model="queryRequest" :inline="true" :label-width="90" v-show="moreFilterVisible">
<el-form-item class="header-e" label="类型:">
<el-select
popper-class="custom-select-dropdown"
v-model="queryRequest.performanceType"
placeholder="请选择"
clearable
filterable
allow-create
style="width: 240px">
<el-option label="sql" value="1"></el-option>
<el-option label="impl" value="2"></el-option>
<el-option label="other" value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item class="header-e" label="执行耗时:">
<el-input v-model="queryRequest.executionTimeDuration" type="number" clearable>
<template #prepend>超过</template>
<template #append>ms</template>
</el-input>
</el-form-item>
<el-form-item class="header-e" label="方法名:">
<el-input v-model="queryRequest.methodName" clearable />
</el-form-item>
</el-form>
</template>
<template #main>
<!-- 数据列表 -->
<el-table
:data="tableData"
v-loading="tableLoading"
:border="true"
highlight-current-row
stripe
style="width: 100%; height: 100%">
<el-table-column type="expand" label="展开" width="60" header-align="center" :align="`center`">
<template #default="props">
<div>
<table class="table-expand">
<tbody>
<tr>
<td class="title-col" style="width: 100px">方法返回类型:</td>
<td style="width: 100px">{{ props.row.methodReturnType }}</td>
<td class="title-col" style="width: 100px">方法参数类型:</td>
<td style="width: 250px">{{ props.row.methodParamTypes }}</td>
<td class="title-col" style="width: 100px">方法参数名:</td>
<td style="width: 150px">{{ props.row.methodParamNames }}</td>
</tr>
</tbody>
</table>
</div>
</template>
</el-table-column>
<el-table-column
prop="performanceType"
label="类型"
width="80"
header-align="center"
:align="`center`"
sortable
show-overflow-tooltip>
<template #default="scope">
<el-tag v-if="scope.row.performanceType === 1" type="primary">sql</el-tag>
<el-tag v-else-if="scope.row.performanceType === 2" type="success">impl</el-tag>
<el-tag v-else type="warning">other</el-tag>
</template>
</el-table-column>
<el-table-column
prop="operationTime"
label="时间"
width="165"
header-align="center"
:align="`center`"
sortable
show-overflow-tooltip>
<template #default="scope">
{{ formatDate(scope.row.operationTime) }}
</template>
</el-table-column>
<el-table-column
prop="className"
label="类名"
width="250"
header-align="center"
:align="`left`"
sortable
show-overflow-tooltip />
<el-table-column
prop="methodName"
label="方法名"
header-align="center"
:align="`left`"
sortable
show-overflow-tooltip />
<el-table-column
prop="executionTimeDuration"
label="执行耗时(ms)"
width="150"
fixed="right"
header-align="center"
:align="'right'"
sortable
show-overflow-tooltip />
<el-table-column label="操作" header-align="center" :align="`center`" fixed="right" width="80">
<template #default="scope">
<el-button type="primary" size="small" link @click="handleDetail(scope.row.id)">详情</el-button>
</template>
</el-table-column>
</el-table>
</template>
<template #footer>
<!-- 分页 -->
<el-pagination
:total="total"
:page-sizes="[100, 200, 300, 500, 1000]"
v-model:page-size="queryRequest.size"
v-model:current-page="queryRequest.current"
background
layout="total, sizes, prev, pager, next, jumper"
@change="handlePageChange" />
</template>
</LayoutBasePage>
<div>
<!-- 抽屉 -->
<!-- 抽屉样式的设置说明:
要想有效设置el-drawer的样式,需确保el-drawer的上层不是template,须被其他元素包裹
如:<template><div><el-drawer></el-drawer></div></template> 或者 <template><el-main><el-drawer></el-drawer></el-main></template>
这样设置el-drawer的样式不起效果:<template><el-drawer></el-drawer></template> -->
<el-drawer v-model="drawerVisible" :with-header="false" size="60%">
<div>
<table class="table-detail">
<tbody>
<tr>
<td style="width: 15%; height: 0px"></td>
<td style="width: 15%; height: 0px"></td>
<td style="width: 10%; height: 0px"></td>
<td style="width: 10%; height: 0px"></td>
<td style="width: 25%; height: 0px"></td>
<td style="width: 25%; height: 0px"></td>
</tr>
<tr>
<td colspan="4">包名</td>
<td>方法返回类型</td>
<td>执行耗时(ms)</td>
</tr>
<tr>
<td colspan="4">{{ detailData?.packageName }}</td>
<td>{{ detailData?.methodReturnType }}</td>
<td>{{ detailData?.executionTimeDuration }}</td>
</tr>
<tr>
<td colspan="4">类名</td>
<td colspan="2">方法名</td>
</tr>
<tr>
<td colspan="4">{{ detailData?.className }}</td>
<td colspan="2">{{ detailData?.methodName }}</td>
</tr>
<tr>
<td colspan="4">方法参数类型</td>
<td colspan="2">方法参数名</td>
</tr>
<tr>
<td colspan="4">{{ detailData?.methodParamTypes }}</td>
<td colspan="2">{{ detailData?.methodParamNames }}</td>
</tr>
<tr>
<td>方法参数值:</td>
<td colspan="5">{{ detailData?.methodParamValues }}</td>
</tr>
<tr>
<td>方法返回值:</td>
<td colspan="5">{{ detailData?.methodReturnValue }}</td>
</tr>
</tbody>
</table>
</div>
</el-drawer>
</div>
</template>
<style scoped lang="scss">
.header-form {
margin: 0;
padding: 0;
.el-form-item {
width: 330px;
height: 24px;
}
.btn-same-width {
width: 102px;
}
}
.el-tag {
padding: 0 10px;
}
.table-expand {
table-layout: fixed;
// 使用 calc 精准计算宽度,避免超出容器宽度
width: calc(100% - 20px);
margin: 0 10px;
border-collapse: collapse;
}
.table-detail {
table-layout: fixed;
width: 100%;
margin: 0;
border-collapse: collapse;
font-size: 14px;
}
td {
height: 30px;
line-height: 30px;
// 不换行
// white-space: nowrap;
// 溢出部分不显示
// overflow: hidden;
// 允许在长单词或URL中间换行
// word-wrap: break-word;
// 允许把单词截断,在单词内换行
word-break: break-all;
border: 1px solid #ccc;
padding: 0 10px;
}
.title-col {
// 左右两端对齐,text-align-last 可以改变段落的最后一行的对齐方式,但是只有在 text-align 属性设置为 "justify" 时才起作用
text-align: justify;
// 左右两端对齐,如果文本内容只有一行,必须要设置text-align-last: justify; 才有效果
text-align-last: justify;
}
</style>
API
src\api\common\performanceLog.ts
import type {
PerformanceLogBriefInfoResponse,
PerformanceLogDetailInfoResponse,
PerformanceLogQueryRequest
} from "@/openapi/types.gen";
import type { PageResult } from "@/types";
import { request } from "@/utils";
/**
* 分页查询(简略信息)
* @param queryRequest 查询参数对象
* @param signal 可选的 AbortSignal 用于取消请求
* @returns 返回分页结果
*/
export const queryPerformanceLogBriefPage = (queryRequest: PerformanceLogQueryRequest, signal?: AbortSignal) => {
return request.post<PageResult<PerformanceLogBriefInfoResponse>>("/performance-logs/query", queryRequest, {
signal
});
};
/**
* 查询详情
* @param id 主键 ID
* @returns 性能日志对象
*/
export const queryPerformanceLogDetailInfoById = (id: number) => {
return request.get<PerformanceLogDetailInfoResponse>(`/performance-logs/${id}`);
};
/**
* 性能日志 API 集合,便于统一管理
*/
export const performanceLogApi = {
/** 分页查询(简略信息) */
queryBriefPage: queryPerformanceLogBriefPage,
/** 查询详情 */
queryDetailInfoById: queryPerformanceLogDetailInfoById
};
应用效果


1972

被折叠的 条评论
为什么被折叠?



