项目编码规范示例:性能日志

后端

实体

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}&lt;{@link PageResult}&lt;{@link PerformanceLogBriefInfoResponse}&gt;&gt;
     */
    @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}&lt;{@link PerformanceLogDetailInfoResponse}&gt;
     */
    @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
};

应用效果

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值