api-response.java.ftl 统一响应结果封装类模板标准规范版

以下是完整添加详细中文注释ApiResponse.java 示例,每个元素都包含深入的说明,涵盖设计原理、使用场景、安全考量和最佳实践:

package ${package.Common};

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import lombok.RequiredArgsConstructor;

import java.time.Instant;

/**
 * <p>
 * 统一响应结果封装类,设计可参考 Spring 框架的 ResponseEntity + HttpStatus
 * </p>
 *
 * <p>
 * 【核心设计目标】
 * 1. 统一所有 API 接口的返回格式,消除前后端协作歧义
 * 2. 提供结构化的状态码、消息、数据三元组
 * 3. 通过内部枚举集中管理业务错误码,避免硬编码
 * 4. 优化 JSON 序列化性能,确保生产环境安全
 * </p>
 *
 * <p>
 * 【为什么需要统一响应?】
 * - 前端无需处理多种响应格式(成功返回对象,失败返回字符串等)
 * - 所有业务结果通过 code 字段区分,HTTP 状态码统一为 200
 * - 错误码标准化,便于监控、告警和国际化
 * - 避免敏感信息通过异常堆栈泄露到客户端
 * </p>
 *
 * <p>
 * 【关键设计决策】
 * - 使用 @Getter + @Setter 而非 @Data:避免生成无意义的 equals()/toString()
 * - code 字段使用 int 基础类型:状态码永不为 null,性能更优
 * - data 字段使用 @JsonInclude(NON_NULL):null 值不序列化,减少网络传输
 * - 内部定义 ResultCode 枚举:集中管理所有业务错误码
 * </p>
 *
 * @param <T> 响应数据的泛型类型,确保编译期类型安全和 Swagger 自动推导
 * @author ${author}
 * @since ${date}
 */
@Getter
@Setter
@Schema(description = "统一响应结果")
public class ApiResponse<T> {

    /**
     * 响应状态码(使用 int 基础类型)
     *
     * <p>
     * 【设计原理】
     * - 状态码是确定的数值,业务上永不为 null
     * - 使用基础类型 int 而非包装类型 Integer:
     *   ✓ 避免自动装箱/拆箱的性能开销
     *   ✓ 消除 NullPointerException 风险
     *   ✓ 与 ResultCode 枚举的 code 类型保持一致
     *   ✓ 符合 HTTP 状态码的传统设计(3位整数)
     * </p>
     *
     * <p>
     * 【状态码规范】
     * - 200-299:通用成功(如 200 = 操作成功)
     * - 400-499:客户端错误(如 400 = 参数校验失败)
     * - 500-599:服务端错误(如 500 = 系统内部错误)
     * - 1000+:业务自定义错误(如 1001 = 用户不存在)
     * </p>
     *
     * <p>
     * 【OpenAPI 集成】
     * - @Schema(example = "200"):在 Swagger UI 中显示示例值
     * - 前端可根据 code 做精准的业务处理(如 code=1001 跳转登录页)
     * </p>
     */
    @Schema(description = "响应状态码", example = "200")
    private int code;

    /**
     * 响应消息(用户可读的提示信息)
     *
     * <p>
     * 【设计原则】
     * - 用户友好:避免技术术语(如 "NullPointerException")
     * - 可操作:提示用户如何修正(如 "邮箱格式不正确,请检查后重试")
     * - 安全第一:生产环境绝不返回堆栈信息或敏感数据
     * - 可覆盖:工厂方法支持自定义消息,覆盖枚举默认消息
     * </p>
     *
     * <p>
     * 【国际化考虑】
     * - 实际项目中,message 应返回错误码(如 "USER_NOT_FOUND")
     * - 前端根据错误码映射多语言文本
     * - 当前设计为简化开发,直接返回中文消息
     * </p>
     *
     * <p>
     * 【安全红线】
     * - 系统错误必须返回通用提示(如 "系统繁忙,请稍后再试")
     * - 详细错误日志应记录到服务端(ELK/Sentry),而非返回给前端
     * </p>
     */
    @Schema(description = "响应消息", example = "操作成功")
    private String message;

    /**
     * 响应数据(业务返回的具体内容)
     *
     * <p>
     * 【泛型优势】
     * - 编译期类型安全:ApiResponse<User> 明确告知数据类型
     * - Swagger 自动推导:API 文档中正确显示数据结构
     * - 前端 TypeScript 类型生成:确保前后端类型一致
     * </p>
     *
     * <p>
     * 【序列化优化】
     * - @JsonInclude(JsonInclude.Include.NON_NULL):
     *   ✓ 当 data 为 null 时,JSON 中不包含该字段
     *   ✓ 减少网络传输体积(失败响应节省 15-20%)
     *   ✓ 前端无需判断 data 是否存在(如 if (res.data))
     * </p>
     *
     * <p>
     * 【典型场景】
     * - 成功:{ "code": 200, "message": "操作成功", "data": { "id": 1, "name": "张三" } }
     * - 失败:{ "code": 400, "message": "用户名不能为空" }
     *   (注意:失败时无 data 字段,因被 NON_NULL 过滤)
     * </p>
     */
    @Schema(description = "响应数据")
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private T data;
    
    /**
     * 响应时间戳(UTC 时间)
     *
     * <p>
     * 【设计原理】
     * - 使用 {@link Instant} 类型而非 String 或 Long:
     *   ✓ 语义明确:表示时间点而非字符串
     *   ✓ 时区无关:UTC 时间,避免客户端时区解析问题
     *   ✓ JSON 序列化友好:Jackson 默认序列化为 ISO 8601 格式
     *   ✓ 前端兼容性好:JavaScript Date 构造函数可直接解析
     * </p>
     *
     * <p>
     * 【时间格式】
     * - JSON 输出格式:ISO 8601(如 "2023-01-01T12:00:00Z")
     * - 优点:人类可读、机器可解析、国际标准
     * - 前端使用示例:new Date("2023-01-01T12:00:00Z")
     * </p>
     *
     * <p>
     * 【使用场景】
     * 1. 性能分析:前端计算网络延迟 = 当前时间 - timestamp
     * 2. 缓存策略:If-Modified-Since 请求头对比
     * 3. 调试定位:快速确认请求处理完成时间
     * 4. 分布式追踪:与 traceId 配合分析全链路耗时
     * </p>
     *
     * <p>
     * 【为什么不使用 LocalDateTime?】
     * - LocalDateTime 无时区信息,客户端解析可能出错
     * - Instant 表示 UTC 时间点,全球统一
     * - 符合 RESTful API 时间字段最佳实践
     * </p>
     *
     * <p>
     * 【性能考量】
     * - 每个响应都包含 timestamp,增加约 20-30 字节
     * - 在高并发场景下需评估网络带宽影响
     * - 可通过配置开关在生产环境关闭(本模板默认开启)
     * </p>
     */
    @Schema(description = "响应时间戳(UTC)", example = "2023-01-01T12:00:00Z")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Instant timestamp;

    /**
     * 私有无参构造函数
     *
     * <p>
     * 【设计意图】
     * - 强制通过静态工厂方法创建实例(如 success()/fail())
     * - 避免外部直接 new ApiResponse() 导致状态不一致
     * - 确保对象创建的语义清晰(成功/失败场景分离)
     * - 便于未来扩展(如自动注入 traceId、timestamp 等)
     * </p>
     *
     * <p>
     * 【为什么需要?】
     * - 如果允许 public 构造函数,开发者可能创建 code=0 的无效响应
     * - 工厂方法封装了业务规则(如成功必须 code=200)
     * </p>
     */
    private ApiResponse() {}

    /**
     * 私有全参构造函数(供静态工厂方法内部使用)
     *
     * <p>
     * 【参数说明】
     * @param code 状态码(由 ResultCode 枚举提供或验证)
     * @param message 响应消息(可覆盖枚举默认消息)
     * @param data 业务数据(成功时有值,失败时为 null)
     * @param timestamp 响应时间戳(通常为 Instant.now())
     * </p>
     *
     * <p>
     * 【访问控制】
     * - private 修饰:仅限本类静态工厂方法调用
     * - 确保所有 ApiResponse 实例都经过业务规则验证
     * </p>
     */
    private ApiResponse(int code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
        this.timestamp = Instant.now();
    }

    // ==================== 成功响应工厂方法 ====================

    /**
     * 创建无数据的成功响应
     *
     * <p>
     * 【使用场景】
     * - 删除操作成功(无需返回数据)
     * - 状态变更成功(如启用/禁用用户)
     * - 批量操作成功(仅需确认结果,无需详情)
     * </p>
     *
     * <p>
     * 【返回值】
     * - code: 200 (ResultCode.SUCCESS.getCode())
     * - message: "操作成功" (ResultCode.SUCCESS.getMessage())
     * - data: null (因无数据,JSON 中不显示 data 字段)
     * </p>
     *
     * @param <T> 泛型类型(类型推导由调用方决定)
     * @return 标准化的成功响应实例
     */
    public static <T> ApiResponse<T> success() {
        return new ApiResponse<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
    }

    /**
     * 创建带数据的成功响应(使用默认成功消息)
     *
     * <p>
     * 【最常用方法】
     * - 查询详情(返回单个实体)
     * - 创建/更新后返回实体(含自增ID等)
     * - 分页查询返回 PageRes<T>
     * </p>
     *
     * <p>
     * 【设计优势】
     * - 泛型自动推导:ApiResponse.success(user) → ApiResponse<User>
     * - Swagger 自动识别数据结构
     * - 前端 TypeScript 类型安全
     * </p>
     *
     * @param data 业务数据(不能为空,否则应使用 success())
     * @param <T> 数据的实际类型
     * @return 包含数据的成功响应
     */
    public static <T> ApiResponse<T> success(T data) {
        return new ApiResponse<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
    }

    /**
     * 创建带数据和自定义消息的成功响应
     *
     * <p>
     * 【使用场景】
     * - 需要更具体的成功提示(如 "用户创建成功,ID: 123")
     * - 国际化场景(根据语言返回不同消息)
     * - 业务特殊成功状态(如 "订单已提交,等待支付")
     * </p>
     *
     * @param data 业务数据
     * @param message 自定义成功消息
     * @param <T> 数据类型
     * @return 自定义消息的成功响应
     */
    public static <T> ApiResponse<T> success(T data, String message) {
        return new ApiResponse<>(ResultCode.SUCCESS.getCode(), message, data);
    }

    // ==================== 失败响应工厂方法 ====================

    /**
     * 创建通用失败响应(系统内部错误)
     *
     * <p>
     * 【使用场景】
     * - 未捕获的 RuntimeException
     * - 数据库连接失败等基础设施错误
     * - 第三方服务调用异常
     * </p>
     *
     * <p>
     * 【安全处理】
     * - 返回通用友好提示,避免暴露系统细节
     * - 详细错误应记录到服务端日志
     * - 前端统一处理 SYSTEM_ERROR code
     * </p>
     *
     * @param <T> 泛型类型
     * @return 系统错误响应
     */
    public static <T> ApiResponse<T> fail() {
        return new ApiResponse<>(ResultCode.SYSTEM_ERROR.getCode(), ResultCode.SYSTEM_ERROR.getMessage(), null);
    }

    /**
     * 使用预定义结果码创建失败响应
     *
     * <p>
     * 【核心方法】
     * - 全局异常处理器调用
     * - Service 层抛出 BusinessException 后转换
     * - 参数校验失败时使用 VALIDATION_ERROR
     * </p>
     *
     * <p>
     * 【优势】
     * - 强类型安全:编译期检查错误码有效性
     * - 代码即文档:ResultCode.USER_NOT_FOUND 自解释
     * - 团队规范统一:强制使用预定义错误码
     * </p>
     *
     * @param resultCode 预定义的业务错误码
     * @param <T> 泛型类型
     * @return 对应错误码的失败响应
     */
    public static <T> ApiResponse<T> fail(ResultCode resultCode) {
        Assert.notNull(resultCode, "resultCode must not be null");
        return new ApiResponse<>(resultCode.getCode(), resultCode.getMessage(), null);
    }

    /**
     * 使用结果码和自定义消息创建失败响应
     *
     * <p>
     * 【典型场景】
     * - 参数校验失败时,用通用码 + 具体字段错误:
     *   fail(ResultCode.VALIDATION_ERROR, "用户名不能为空")
     * - 业务规则校验失败:
     *   fail(ResultCode.USER_DISABLED, "用户已被禁用,无法登录")
     * </p>
     *
     * <p>
     * 【灵活性】
     * - 保留枚举的 code 语义
     * - 覆盖默认消息提供具体上下文
     * - 前端可同时使用 code(做逻辑判断)和 message(显示给用户)
     * </p>
     *
     * @param resultCode 预定义错误码
     * @param customMessage 自定义错误消息
     * @param <T> 泛型类型
     * @return 自定义消息的失败响应
     */
    public static <T> ApiResponse<T> fail(ResultCode resultCode, String customMessage) {
        return new ApiResponse<>(resultCode.getCode(), customMessage, null);
    }

    /**
     * 使用自定义状态码和消息创建失败响应(兼容遗留系统)
     *
     * <p>
     * 【使用场景】
     * - 集成第三方系统返回的错误码
     * - 无法预定义的动态错误码
     * - 迁移旧系统时的临时兼容
     * </p>
     *
     * <p>
     * 【注意事项】
     * - 应优先使用 ResultCode 枚举
     * - 此方法仅作为兜底方案
     * - 生产环境应尽量避免使用
     * </p>
     *
     * @param code 自定义状态码(建议符合规范:4xx/5xx/1000+)
     * @param message 错误消息
     * @param <T> 泛型类型
     * @return 自定义错误响应
     */
    public static <T> ApiResponse<T> fail(Integer code, String message) {
        // 处理 null 情况(防御性编程)
        int actualCode = (code != null) ? code : ResultCode.SYSTEM_ERROR.getCode();
        return new ApiResponse<>(actualCode, message, null);
    }

    // ==================== 响应状态码枚举 ====================

    /**
     * <p>
     * 响应状态码枚举(集中管理所有业务错误码)
     * </p>
     *
     * <p>
     * 【设计原则】
     * 1. 覆盖通用场景(成功、系统错误、校验错误等)
     * 2. 预留业务扩展空间(按模块划分错误码范围)
     * 3. 每个枚举包含:
     *    - code:整数状态码(与 HTTP 状态码风格一致)
     *    - message:默认用户友好消息
     * 4. 状态码范围规范:
     *    - 200-299:通用成功
     *    - 400-499:客户端错误
     *    - 500-599:服务端错误
     *    - 1000+:业务自定义错误(按模块分配)
     * </p>
     *
     * <p>
     * 【模块化错误码分配建议】
     * - 用户模块:1000-1999(如 USER_NOT_FOUND=1001)
     * - 订单模块:2000-2999(如 ORDER_NOT_FOUND=2001)
     * - 商品模块:3000-3999
     * - 权限模块:4000-4999
     * - 以此类推,避免冲突
     * </p>
     */
    @Getter
    @RequiredArgsConstructor
    public enum ResultCode {
        // =============== 通用成功 ===============
        /**
         * 操作成功
         * - code: 200
         * - message: "操作成功"
         * - 使用场景:所有成功的业务操作
         */
        SUCCESS(200, "操作成功"),

        // =============== 客户端错误 (4xx) ===============
        /**
         * 请求参数校验失败
         * - code: 400
         * - message: "请求参数校验失败"
         * - 使用场景:@Valid 校验失败、DTO 字段不合法
         */
        VALIDATION_ERROR(400, "请求参数校验失败"),
        
        /**
         * 错误的请求
         * - code: 400
         * - message: "错误的请求"
         * - 使用场景:请求格式错误、缺少必要参数
         */
        BAD_REQUEST(400, "错误的请求"),
        
        /**
         * 未认证
         * - code: 401
         * - message: "未认证,请登录"
         * - 使用场景:JWT 令牌缺失或过期
         */
        UNAUTHORIZED(401, "未认证,请登录"),
        
        /**
         * 无权限访问
         * - code: 403
         * - message: "无权限访问"
         * - 使用场景:用户权限不足
         */
        FORBIDDEN(403, "无权限访问"),
        
        /**
         * 请求资源不存在
         * - code: 404
         * - message: "请求资源不存在"
         * - 使用场景:ID 查询不到记录
         */
        NOT_FOUND(404, "请求资源不存在"),

        // =============== 服务端错误 (5xx) ===============
        /**
         * 系统内部错误
         * - code: 500
         * - message: "系统内部错误"
         * - 使用场景:未捕获异常、数据库错误等
         * - 安全提示:生产环境应返回通用消息
         */
        SYSTEM_ERROR(500, "系统内部错误"),
        
        /**
         * 服务暂时不可用
         * - code: 503
         * - message: "服务暂时不可用"
         * - 使用场景:依赖服务宕机、熔断触发
         */
        SERVICE_UNAVAILABLE(503, "服务暂时不可用"),

        // =============== 业务自定义错误 (1000+) ===============
        // 【用户模块 - 1000-1999】
        /**
         * 用户不存在
         * - code: 1001
         * - message: "用户不存在"
         * - 使用场景:登录、查询用户详情时用户不存在
         */
        USER_NOT_FOUND(1001, "用户不存在"),
        
        /**
         * 用户名已存在
         * - code: 1002
         * - message: "用户名已存在"
         * - 使用场景:注册时用户名重复
         */
        USER_EXISTS(1002, "用户名已存在"),
        
        /**
         * 用户已被禁用
         * - code: 1003
         * - message: "用户已被禁用"
         * - 使用场景:登录时用户状态为禁用
         */
        USER_DISABLED(1003, "用户已被禁用");

        /**
         * 状态码(使用 int 基础类型)
         *
         * <p>
         * 【为什么用 int?】
         * - 枚举是单例对象,int 比 Integer 更节省内存
         * - 状态码是确定值,永不为 null
         * - 与 ApiResponse.code 类型保持一致
         * - 避免自动装箱开销
         * </p>
         */
        private final int code;

        /**
         * 默认消息(用户友好提示)
         *
         * <p>
         * 【设计考虑】
         * - 直接返回中文,简化开发
         * - 实际项目中可改为错误码,由前端映射多语言
         * - 消息应简洁明确,避免技术术语
         * </p>
         */
        private final String message;

        /**
         * 枚举构造函数(私有,仅枚举常量可调用)
         *
         * @param code 状态码
         * @param message 默认消息
         */
        ResultCode(int code, String message) {
            this.code = code;
            this.message = message;
        }
    }
}

ApiResponse 中的 code 使用 int 基础类型,而不是 Integer 包装类型

这是一个非常专业且关键的问题!您观察到了类型设计的不一致性,这确实值得深入探讨。下面我将从设计原理、实际影响、最佳实践三个维度详细分析,并给出明确的改进建议。


🔍 一、当前设计的原因分析

1. ResultCode.code 使用 int 的原因

private final int code; // 基础类型
  • 性能考虑:枚举是单例对象,intInteger 更节省内存(无装箱开销)
  • 语义明确:状态码是确定的数值,不可能为 null
  • 历史习惯:HTTP 状态码、系统错误码等传统上都是 int

2. ApiResponse.code 使用 Integer 的原因

private Integer code; // 包装类型
  • JSON 序列化友好:Jackson 等序列化框架对包装类型处理更灵活
  • OpenAPI 兼容性:Swagger 规范中状态码字段通常允许为 null(虽然实际不会)
  • ⚠️ 历史遗留:早期模板可能未深入考虑类型一致性

⚠️ 二、当前设计的问题

1. 类型不一致导致的认知负担

// ApiResponse.code 是 Integer
Integer responseCode = apiResponse.getCode();

// ResultCode.code 是 int  
int enumCode = ResultCode.SUCCESS.getCode();

// 赋值时需要隐式装箱/拆箱
apiResponse.setCode(ResultCode.SUCCESS.getCode()); // int → Integer (自动装箱)

2. 潜在的空指针风险(虽然概率低)

// 如果 ApiResponse.code 被设为 null(不应该发生)
if (apiResponse.getCode() == 200) { // NullPointerException!
    // ...
}

3. 违反“最小惊讶原则”

  • 开发者期望:状态码是确定的数值,不应为 null
  • 实际设计:ApiResponse.code 允许 null,造成困惑

✅ 三、最佳实践建议:统一使用 int 基础类型

结论:应该改进!ApiResponse.code 也应使用 int 基础类型。

📊 类型选择对比表

维度int 基础类型Integer 包装类型
语义正确性✅ 状态码永不为 null❌ 允许 null(不符合业务语义)
性能✅ 无装箱开销❌ 自动装箱/拆箱开销
空指针风险✅ 完全避免⚠️ 存在风险
JSON 序列化✅ Jackson 完美支持✅ 支持(但无优势)
OpenAPI 兼容性✅ Swagger 正确显示✅ 支持(但无优势)
代码一致性✅ 与枚举类型统一❌ 与枚举类型不一致

🔧 四、改进带来的实际好处

1. 性能提升

  • 消除每次访问 code 字段时的自动拆箱开销
  • 在高并发场景下,累积性能提升显著

2. 代码更安全

// 改进前(有风险)
if (response.getCode() == 200) { } // 可能 NPE

// 改进后(绝对安全)
if (response.getCode() == 200) { } // 永远不会 NPE

3. 类型系统更严谨

// 编译期就能发现错误
ApiResponse<?> response = ...;
int code = response.getCode(); // 直接赋值,无需判空

4. 与主流框架保持一致

  • Spring 的 ResponseEntity 状态码:HttpStatus(内部是 int
  • HTTP/1.1 规范:状态码是 3 位整数
  • 数据库错误码:通常是 int

📝 五、迁移注意事项

1. 向后兼容性

  • 完全兼容int 可以自动装箱为 Integer(调用方无需修改)
  • 序列化无影响:JSON 中都是数字,无区别

2. 需要检查的场景

// 如果有代码显式检查 null(虽然不应该有)
if (response.getCode() == null) { ... } // 编译错误,需删除

// 如果有代码使用 Integer 特有方法
response.getCode().intValue(); // 改为直接使用 response.getCode()

3. OpenAPI 文档

  • Swagger UI 显示效果完全不变
  • 生成的 TypeScript 类型仍为 number

✅ 最终结论

强烈建议将 ApiResponse.code 改为 int 基础类型,理由如下:

  1. 语义正确性:状态码是确定值,不应为 null
  2. 性能优化:消除不必要的装箱/拆箱
  3. 安全性提升:彻底避免 NullPointerException
  4. 设计一致性:与 ResultCode 枚举类型统一
  5. 行业标准:符合主流框架和协议的设计惯例

这种改进属于无损优化——既提升了代码质量,又保持了完全的向后兼容性,是典型的“小改动,大收益”重构。

推荐将上面的 @Data 注解改为 @Getter 和 @Setter 吗?

这是一个非常重要且专业的设计问题!是否将 @Data 替换为 @Getter + @Setter 需要根据具体使用场景来判断。下面我将从多个维度进行深度分析,并给出明确的推荐方案。


🔍 一、@Data@Getter + @Setter 的功能对比

1. @Data 注解包含的功能

@Data
public class ApiResponse<T> {
    // 自动生成以下方法:
    // 1. 所有字段的 getter
    // 2. 所有非 final 字段的 setter  
    // 3. toString()
    // 4. equals() 和 hashCode()
    // 5. requiredArgsConstructor (如果存在 final 字段)
}

2. @Getter + @Setter 注解包含的功能

@Getter
@Setter
public class ApiResponse<T> {
    // 仅生成:
    // 1. 所有字段的 getter
    // 2. 所有非 final 字段的 setter
}

⚠️ 二、在 ApiResponse 类中使用 @Data 的潜在问题

1. equals()hashCode() 的语义问题

// ApiResponse 包含泛型字段 T data
ApiResponse<User> response1 = ApiResponse.success(user1);
ApiResponse<User> response2 = ApiResponse.success(user2);

// 由于 user1.equals(user2) 可能为 false
// 导致 response1.equals(response2) 也为 false
// 但这在业务上可能没有意义!

问题分析

  • ApiResponse数据传输载体,不是业务实体
  • 通常不会对两个 ApiResponse 对象进行相等性比较
  • 自动生成的 equals() 可能导致意外行为(如放入 HashSet 时)

2. toString() 的安全风险

// 如果 data 包含敏感信息(如用户密码)
ApiResponse<User> response = ApiResponse.success(userWithPassword);
log.info("Response: {}", response); // 可能泄露密码!

问题分析

  • 自动生成的 toString() 会打印所有字段
  • 在日志中可能意外泄露敏感数据
  • 生产环境应避免自动 toString()

3. 继承兼容性问题

  • 虽然 ApiResponse 通常不被继承,但如果未来需要继承
  • @Data 生成的 equals() 在继承场景下有经典问题(对称性破坏)

✅ 三、推荐方案:使用 @Getter + @Setter

结论:强烈推荐将 @Data 替换为 @Getter + @Setter

📊 对比分析表

维度@Data@Getter + @Setter推荐度
功能必要性生成了不需要的方法仅生成必要方法
安全性toString() 可能泄露敏感信息无自动 toString()
语义正确性equals() 无业务意义无 equals(),避免误用
性能额外方法增加类大小方法最少,性能最优
可维护性隐藏了不必要复杂性显式声明所需功能

🔧 四、如果确实需要 toString() 怎么办?

方案 1:手动实现安全的 toString()

@Override
public String toString() {
    return "ApiResponse{" +
            "code=" + code +
            ", message='" + message + '\'' +
            ", data=[REDACTED]" + // 敏感数据脱敏
            '}';
}

方案 2:使用 @ToString 注解(选择性包含字段)

@Getter
@Setter
@ToString(exclude = "data") // 排除敏感的 data 字段
public class ApiResponse<T> {
    // ...
}

方案 3:完全不提供 toString()

  • 推荐方案:让调用方自行决定如何打印

  • 日志记录时显式选择要打印的字段:

    log.info("Response code: {}, message: {}", response.getCode(), response.getMessage());
    

📝 六、Lombok 注解使用最佳实践总结

类型推荐注解原因
DTO/VO/传输对象@Getter + @Setter避免无意义的 equals/toString
Entity/ORM 实体@Data@Getter + @Setter + @EqualsAndHashCode需要 equals 用于集合操作
配置类@Data通常需要 toString() 用于调试
枚举/常量类无注解或 @Getter通常字段为 final

💡 核心原则只生成真正需要的方法


✅ 最终结论

强烈推荐将 ApiResponse 中的 @Data 替换为 @Getter + @Setter,理由如下:

  1. 安全第一:避免 toString() 意外泄露敏感数据
  2. 语义清晰:传输对象不需要相等性比较
  3. 最小化原则:只生成必要的 getter/setter 方法
  4. 性能优化:减少不必要的方法生成
  5. 专业体现:体现了对代码质量和安全性的深度思考

这种改进属于防御性编程的最佳实践,能够有效避免潜在的安全风险和维护问题,是企业级代码质量的重要体现。

上面的是我的项目结构,现在我想要加入开源的通义大模型,实现项目具有以下功能 自行准备本地知识库资料(如本专业相关资料内容)作为大模型本地知识库的输入 教师侧:备课与设计: 根据所提供的本地课程大纲、课程知识库文档等自动设计教学内容,包括知识讲解、实训练习与指导、时间分布等。 · 考核内容生成: 根据教学内容自动生成考核题目及参考答案,考核题目种类可多样化,根据学科设计,如计算机类可设计相关编程题和答案 · 学情数据分析: 对学生提交的答案进行自动化检测,提供错误定位与修正建议。对学生整体数据进行分析,总结知识掌握情况与教学建议。 学生侧: · 在线学习助手: 对学生的提出的问题,结合教学内容进行解答; · 实时练习评测助手: 根据学生历史练习情况,以及学生的练习要求,生成随练题目,并对练习纠错。 管理侧: 用户管理:管理员/教师/学生等用户的基本管理 课件资源管理:按学科列表教师备课产生的课件、练习等资源,可以导出。 大屏概览: · 教师使用次数统计/活跃板块(当日/本周) · 学生使用次数统计/活跃板块(当日/本周) · 教学效率指数(备课与修正耗时、课后练习设计与修正耗时、课程优化方向(如:某学科通过率持续偏低) · 学生学习效果(平均正确率趋势、知识点掌握情况,高频错误知识点等) 非功能性需求 需明确使用至少1个开源大模型作为核心技术组件; 需采用本地知识库作为输入,知识库资料总量不大于100M; 生成的内容、练习与答案与本地知识库的关联性和准确性;(对抽取知识点设计的题目验证关联性和与答案的准确性有知识点说明) 我该在哪里添加 下面我再对功能进行进一步的说明:1、首先本地知识库可以让用户自己上传(即用户自己可以控制本地知识库,但一开始会有一个默认的知识库,知识库是在项目中的某个目录中可以吗), 2、在备课与设计的功能上ai可以帮助教师设计教学内容,并且教师可以下载该教学内容 3、在考核内容生成中,考核题目同样可以进行下载,而且可以有带答案不带答案两种方式; 完整的项目结构又是怎样的,哪些内容需要更改 下面是我目前的项目结构卷 Windows-SSD 的文件夹 PATH 列表 卷序列号为 20C7-404E C:. | project_structure.txt | +---main | +---java | | \---com | | | Aiapp1Application.java | | | | | +---annotation | | | APPLoginUser.java | | | IgnoreAuth.java | | | LoginUser.java | | | | | +---config | | | InterceptorConfig.java | | | MybatisPlusConfig.java | | | MyMetaObjectHandler.java | | | | | +---controller | | | CommonController.java | | | ConfigController.java | | | DictionaryController.java | | | ExampaperController.java | | | ExamquestionController.java | | | ExamrecordController.java | | | ExamredetailsController.java | | | ExamrewrongquestionController.java | | | FileController.java | | | ForumController.java | | | JiaoshiController.java | | | KechengController.java | | | KechengLiuyanController.java | | | NewsController.java | | | UsersController.java | | | YonghuController.java | | | | | +---dao | | | CommonDao.java | | | ConfigDao.java | | | DictionaryDao.java | | | ExampaperDao.java | | | ExamquestionDao.java | | | ExamrecordDao.java | | | ExamredetailsDao.java | | | ExamrewrongquestionDao.java | | | ForumDao.java | | | JiaoshiDao.java | | | KechengDao.java | | | KechengLiuyanDao.java | | | NewsDao.java | | | TokenDao.java | | | UsersDao.java | | | YonghuDao.java | | | | | +---entity | | | | ConfigEntity.java | | | | DictionaryEntity.java | | | | EIException.java | | | | ExampaperEntity.java | | | | ExamquestionEntity.java | | | | ExamrecordEntity.java | | | | ExamredetailsEntity.java | | | | ExamrewrongquestionEntity.java | | | | ForumEntity.java | | | | JiaoshiEntity.java | | | | KechengEntity.java | | | | KechengLiuyanEntity.java | | | | NewsEntity.java | | | | TokenEntity.java | | | | UsersEntity.java | | | | YonghuEntity.java | | | | | | | +---model | | | | DictionaryModel.java | | | | ExampaperModel.java | | | | ExamquestionModel.java | | | | ExamrecordModel.java | | | | ExamredetailsModel.java | | | | ExamrewrongquestionModel.java | | | | ForumModel.java | | | | JiaoshiModel.java | | | | KechengLiuyanModel.java | | | | KechengModel.java | | | | NewsModel.java | | | | YonghuModel.java | | | | | | | +---view | | | | DictionaryView.java | | | | ExampaperView.java | | | | ExamquestionView.java | | | | ExamrecordView.java | | | | ExamredetailsView.java | | | | ExamrewrongquestionView.java | | | | ForumView.java | | | | JiaoshiView.java | | | | KechengLiuyanView.java | | | | KechengView.java | | | | NewsView.java | | | | YonghuView.java | | | | | | | \---vo | | | DictionaryVO.java | | | ExampaperVO.java | | | ExamquestionVO.java | | | ExamrecordVO.java | | | ExamredetailsVO.java | | | ExamrewrongquestionVO.java | | | ForumVO.java | | | JiaoshiVO.java | | | KechengLiuyanVO.java | | | KechengVO.java | | | NewsVO.java | | | YonghuVO.java | | | | | +---interceptor | | | AuthorizationInterceptor.java | | | | | +---model | | | \---enums | | | TypeEnum.java | | | | | +---service | | | | CommonService.java | | | | ConfigService.java | | | | DictionaryService.java | | | | ExampaperService.java | | | | ExamquestionService.java | | | | ExamrecordService.java | | | | ExamredetailsService.java | | | | ExamrewrongquestionService.java | | | | ForumService.java | | | | JiaoshiService.java | | | | KechengLiuyanService.java | | | | KechengService.java | | | | NewsService.java | | | | TokenService.java | | | | UsersService.java | | | | YonghuService.java | | | | | | | \---impl | | | CommonServiceImpl.java | | | ConfigServiceImpl.java | | | DictionaryServiceImpl.java | | | ExampaperServiceImpl.java | | | ExamquestionServiceImpl.java | | | ExamrecordServiceImpl.java | | | ExamredetailsServiceImpl.java | | | ExamrewrongquestionServiceImpl.java | | | ForumServiceImpl.java | | | JiaoshiServiceImpl.java | | | KechengLiuyanServiceImpl.java | | | KechengServiceImpl.java | | | NewsServiceImpl.java | | | TokenServiceImpl.java | | | UsersServiceImpl.java | | | YonghuServiceImpl.java | | | | | +---ServletContextListener | | | DictionaryServletContextListener.java | | | | | +---thread | | | MyThreadMethod.java | | | | | \---utils | | BaiduUtil.java | | CommonUtil.java | | FileUtil.java | | HttpClientUtils.java | | JQPageInfo.java | | MPUtil.java | | PageUtils.java | | PoiUtil.java | | Query.java | | R.java | | SpringContextUtils.java | | SQLFilter.java | | StringUtil.java | | ValidatorUtils.java | | | \---resources | | application.yml | | | +---front | | | index.html | | | | | +---css | | | bootstrap.min.css | | | common.css | | | front-kaoshi-style.css | | | homeworkPC.min.css | | | style.css | | | theme.css | | | | | +---elementui | | | | elementui.css | | | | elementui.js | | | | | | | \---fonts | | | element-icons.ttf | | | element-icons.woff | | | | | +---img | | | avator.png | | | banner.jpg | | | jianshe.png | | | jiaotong.png | | | line.jpg | | | nongye.png | | | seckilling.jpg | | | select.png | | | selectActive.png | | | unselect.png | | | weixin.png | | | yuan.png | | | zhifubao.png | | | zhongguo.png | | | | | +---js | | | bootstrap.AMapPositionPicker.js | | | bootstrap.min.js | | | config.js | | | jquery.js | | | utils.js | | | validate.js | | | vue.js | | | | | +---layui | | | | layui.all.js | | | | layui.js | | | | | | | +---css | | | | | layui.css | | | | | layui.mobile.css | | | | | | | | | \---modules | | | | | code.css | | | | | | | | | +---laydate | | | | | \---default | | | | | laydate.css | | | | | | | | | \---layer | | | | \---default | | | | icon-ext.png | | | | icon.png | | | | layer.css | | | | loading-0.gif | | | | loading-1.gif | | | | loading-2.gif | | | | | | | +---font | | | | iconfont.eot | | | | iconfont.svg | | | | iconfont.ttf | | | | iconfont.woff | | | | iconfont.woff2 | | | | | | | +---images | | | | \---face | | | | 0.gif | | | | 1.gif | | | | 10.gif | | | | 11.gif | | | | 12.gif | | | | 13.gif | | | | 14.gif | | | | 15.gif | | | | 16.gif | | | | 17.gif | | | | 18.gif | | | | 19.gif | | | | 2.gif | | | | 20.gif | | | | 21.gif | | | | 22.gif | | | | 23.gif | | | | 24.gif | | | | 25.gif | | | | 26.gif | | | | 27.gif | | | | 28.gif | | | | 29.gif | | | | 3.gif | | | | 30.gif | | | | 31.gif | | | | 32.gif | | | | 33.gif | | | | 34.gif | | | | 35.gif | | | | 36.gif | | | | 37.gif | | | | 38.gif | | | | 39.gif | | | | 4.gif | | | | 40.gif | | | | 41.gif | | | | 42.gif | | | | 43.gif | | | | 44.gif | | | | 45.gif | | | | 46.gif | | | | 47.gif | | | | 48.gif | | | | 49.gif | | | | 5.gif | | | | 50.gif | | | | 51.gif | | | | 52.gif | | | | 53.gif | | | | 54.gif | | | | 55.gif | | | | 56.gif | | | | 57.gif | | | | 58.gif | | | | 59.gif | | | | 6.gif | | | | 60.gif | | | | 61.gif | | | | 62.gif | | | | 63.gif | | | | 64.gif | | | | 65.gif | | | | 66.gif | | | | 67.gif | | | | 68.gif | | | | 69.gif | | | | 7.gif | | | | 70.gif | | | | 71.gif | | | | 8.gif | | | | 9.gif | | | | | | | \---lay | | | \---modules | | | carousel.js | | | code.js | | | colorpicker.js | | | element.js | | | flow.js | | | form.js | | | jquery.js | | | laydate.js | | | layedit.js | | | layer.js | | | laypage.js | | | laytpl.js | | | mobile.js | | | rate.js | | | slider.js | | | table.js | | | transfer.js | | | tree.js | | | upload.js | | | util.js | | | | | +---modules | | | | config.js | | | | | | | +---http | | | | http.js | | | | | | | +---layarea | | | | layarea.js | | | | | | | \---tinymce | | | | index.html | | | | tinymce.js | | | | | | | \---tinymce | | | | jquery.tinymce.min.js | | | | license.txt | | | | readme.md | | | | tinymce.js | | | | tinymce.min.js | | | | | | | +---langs | | | | readme.md | | | | zh_CN.js | | | | | | | +---plugins | | | | +---advlist | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---anchor | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---autolink | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---autoresize | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---autosave | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---bbcode | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---charmap | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---code | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---codesample | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---colorpicker | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---contextmenu | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---directionality | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---emoticons | | | | | | plugin.js | | | | | | plugin.min.js | | | | | | | | | | | \---js | | | | | emojis.js | | | | | emojis.min.js | | | | | | | | | +---fullpage | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---fullscreen | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---help | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---hr | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---image | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---imagetools | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---importcss | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---indent2em | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---insertdatetime | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---legacyoutput | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---link | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---lists | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---media | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---nonbreaking | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---noneditable | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---pagebreak | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---paste | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---preview | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---print | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---quickbars | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---save | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---searchreplace | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---spellchecker | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---tabfocus | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---table | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---template | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---textcolor | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---textpattern | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---toc | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---visualblocks | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | +---visualchars | | | | | plugin.js | | | | | plugin.min.js | | | | | | | | | \---wordcount | | | | plugin.js | | | | plugin.min.js | | | | | | | +---skins | | | | +---content | | | | | +---dark | | | | | | content.css | | | | | | content.min.css | | | | | | content.min.css.map | | | | | | | | | | | +---default | | | | | | content.css | | | | | | content.min.css | | | | | | content.min.css.map | | | | | | | | | | | +---document | | | | | | content.css | | | | | | content.min.css | | | | | | content.min.css.map | | | | | | | | | | | \---writer | | | | | content.css | | | | | content.min.css | | | | | content.min.css.map | | | | | | | | | \---ui | | | | +---oxide | | | | | | content.css | | | | | | content.inline.css | | | | | | content.inline.min.css | | | | | | content.inline.min.css.map | | | | | | content.min.css | | | | | | content.min.css.map | | | | | | content.mobile.css | | | | | | content.mobile.min.css | | | | | | content.mobile.min.css.map | | | | | | skin.css | | | | | | skin.min.css | | | | | | skin.min.css.map | | | | | | skin.mobile.css | | | | | | skin.mobile.min.css | | | | | | skin.mobile.min.css.map | | | | | | | | | | | \---fonts | | | | | tinymce-mobile.woff | | | | | | | | | \---oxide-dark | | | | | content.css | | | | | content.inline.css | | | | | content.inline.min.css | | | | | content.inline.min.css.map | | | | | content.min.css | | | | | content.min.css.map | | | | | content.mobile.css | | | | | content.mobile.min.css | | | | | content.mobile.min.css.map | | | | | skin.css | | | | | skin.min.css | | | | | skin.min.css.map | | | | | skin.mobile.css | | | | | skin.mobile.min.css | | | | | skin.mobile.min.css.map | | | | | | | | | \---fonts | | | | tinymce-mobile.woff | | | | | | | \---themes | | | +---mobile | | | | theme.js | | | | theme.min.js | | | | | | | \---silver | | | theme.js | | | theme.min.js | | | | | +---pages | | | +---chat | | | | chat.html | | | | | | | +---dictionary | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---exampaper | | | | add.html | | | | detail.html | | | | exam.html | | | | list.html | | | | | | | +---examquestion | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---examrecord | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---examredetails | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---examrewrongquestion | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---forum | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---home | | | | home.html | | | | | | | +---jiaoshi | | | | add.html | | | | center.html | | | | detail.html | | | | list.html | | | | register.html | | | | | | | +---kecheng | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---kechengLiuyan | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---login | | | | login.html | | | | | | | +---news | | | | add.html | | | | detail.html | | | | list.html | | | | | | | +---recharge | | | | recharge.html | | | | | | | \---yonghu | | | add.html | | | center.html | | | detail.html | | | list.html | | | register.html | | | | | \---xznstatic | | +---css | | | | bootstrap.min.css | | | | common.css | | | | element.min.css | | | | login.css | | | | public.css | | | | style.css | | | | swiper.min.css | | | | | | | \---fonts | | | element-icons.ttf | | | element-icons.woff | | | | | +---img | | | 162237296.jpg | | | 162240878.jpg | | | 19.jpg | | | 1_092ZZ2503138.jpg | | | 20.jpg | | | index_24.gif | | | index_35.gif | | | index_41.gif | | | index_44.gif | | | logo.png | | | news_list_time.jpg | | | service_btn.png | | | service_img.png | | | service_title.png | | | | | \---js | | bootstrap.min.js | | element.min.js | | index.js | | jquery-1.11.3.min.js | | jquery.SuperSlide.2.1.1.js | | swiper.min.js | | | +---img | | \---img | | back-img-bg.jpg | | front-img-bg.jpg | | logo.jpg | | | +---mapper | | CommonDao.xml | | ConfigDao.xml | | DictionaryDao.xml | | ExampaperDao.xml | | ExamquestionDao.xml | | ExamrecordDao.xml | | ExamredetailsDao.xml | | ExamrewrongquestionDao.xml | | ForumDao.xml | | JiaoshiDao.xml | | KechengDao.xml | | KechengLiuyanDao.xml | | NewsDao.xml | | TokenDao.xml | | UsersDao.xml | | YonghuDao.xml | | | +---static | | +---admin | | | | 1-install.bat | | | | 2-run.bat | | | | 3-build.bat | | | | babel.config.js | | | | package-lock.json | | | | package.json | | | | vue.config.js | | | | | | | +---dist | | | | | favicon.ico | | | | | index.html | | | | | | | | | +---css | | | | | app.381d2044.css | | | | | chunk-vendors.a72b0961.css | | | | | | | | | +---fonts | | | | | element-icons.535877f5.woff | | | | | element-icons.732389de.ttf | | | | | | | | | +---img | | | | | 404.3648f234.png | | | | | zhongguo.20798bfa.png | | | | | | | | | \---js | | | | app.b100d28f.js | | | | app.b100d28f.js.map | | | | chunk-vendors.213b68ba.js | | | | chunk-vendors.213b68ba.js.map | | | | | | | +---public | | | | favicon.ico | | | | index.html | | | | | | | \---src | | | | App.vue | | | | main.js | | | | | | | +---assets | | | | +---css | | | | | element-variables.scss | | | | | style.scss | | | | | | | | | \---img | | | | | 404.png | | | | | avator.png | | | | | bg.jpg | | | | | captcha.jpg | | | | | login.png | | | | | logo.png | | | | | password.png | | | | | role.png | | | | | username.png | | | | | | | | | \---test | | | | jianshe.png | | | | jiaotong.png | | | | nongye.png | | | | weixin.png | | | | zhifubao.png | | | | zhongguo.png | | | | | | | +---components | | | | +---common | | | | | BreadCrumbs.vue | | | | | Editor.vue | | | | | FileUpload.vue | | | | | | | | | +---home | | | | | HomeCard.vue | | | | | HomeChart.vue | | | | | HomeComment.vue | | | | | HomeProgress.vue | | | | | | | | | +---index | | | | | IndexAside.vue | | | | | IndexAsideStatic.vue | | | | | IndexAsideSub.vue | | | | | IndexHeader.vue | | | | | IndexMain.vue | | | | | | | | | \---SvgIcon | | | | index.vue | | | | | | | +---icons | | | | | index.js | | | | | svgo.yml | | | | | | | | | \---svg | | | | | 404.svg | | | | | articleEdit.svg | | | | | banner.svg | | | | | bug.svg | | | | | build.svg | | | | | cfg.svg | | | | | channel.svg | | | | | chart.svg | | | | | clipboard.svg | | | | | code.svg | | | | | component.svg | | | | | contacts.svg | | | | | dashboard.svg | | | | | date.svg | | | | | dept.svg | | | | | dict.svg | | | | | documentation.svg | | | | | download.svg | | | | | drag.svg | | | | | druid.svg | | | | | edit.svg | | | | | education.svg | | | | | email.svg | | | | | excel.svg | | | | | exit-fullscreen.svg | | | | | eye-open.svg | | | | | file.svg | | | | | form.svg | | | | | fullscreen.svg | | | | | icon.svg | | | | | international.svg | | | | | job.svg | | | | | language.svg | | | | | link.svg | | | | | list.svg | | | | | lock.svg | | | | | log.svg | | | | | logininfor.svg | | | | | menu.svg | | | | | message.svg | | | | | money.svg | | | | | monitor.svg | | | | | nested.svg | | | | | nested0.svg | | | | | online.svg | | | | | operation.svg | | | | | password.svg | | | | | password0.svg | | | | | pdf.svg | | | | | people.svg | | | | | peoples.svg | | | | | phone.svg | | | | | post.svg | | | | | qq.svg | | | | | search.svg | | | | | sender.svg | | | | | server.svg | | | | | shopping.svg | | | | | shoppingCard.svg | | | | | size.svg | | | | | skill.svg | | | | | star.svg | | | | | swagger.svg | | | | | system.svg | | | | | tab.svg | | | | | table.svg | | | | | table0.svg | | | | | task.svg | | | | | template.svg | | | | | theme.svg | | | | | tool.svg | | | | | tree-table.svg | | | | | tree.svg | | | | | user.svg | | | | | user0.svg | | | | | validCode.svg | | | | | wechat.svg | | | | | zip.svg | | | | | | | | | \---svg | | | | agricultureRegister.svg | | | | AI.svg | | | | AIDeviceLayout.svg | | | | area.svg | | | | base.svg | | | | batch.svg | | | | board.svg | | | | board1.svg | | | | boardConfig.svg | | | | cfg.svg | | | | code.svg | | | | company.svg | | | | crop.svg | | | | crops.svg | | | | dashboard.svg | | | | dataAbnormal.svg | | | | dataLack.svg | | | | dept.svg | | | | device.svg | | | | deviceMonitorData.svg | | | | dict.svg | | | | diseasesinsect.svg | | | | diseasesinsects.svg | | | | documentation.svg | | | | EIM.svg | | | | email.svg | | | | environmental.svg | | | | eye-open.svg | | | | farmingProject.svg | | | | finance.svg | | | | financeBudget.svg | | | | financeReality.svg | | | | firm.svg | | | | firms.svg | | | | harvestBatch.svg | | | | harvestDetection.svg | | | | harvestManage.svg | | | | harvestWorks.svg | | | | heavyMetalDetection.svg | | | | home.svg | | | | inspection.svg | | | | internet.svg | | | | internetActive.svg | | | | log.svg | | | | mainSystem.svg | | | | mainSystemActive.svg | | | | menu.svg | | | | monitorEquipment.svg | | | | news.svg | | | | order.svg | | | | password.svg | | | | peoples.svg | | | | pest.svg | | | | pestActive.svg | | | | pesticideResidue.svg | | | | pests.svg | | | | phone.svg | | | | plant.svg | | | | plants.svg | | | | plantsActive.svg | | | | residual.svg | | | | retroactiveCoding.svg | | | | scheme.svg | | | | source.svg | | | | sourceActive.svg | | | | system.svg | | | | task.svg | | | | tempFarm.svg | | | | traceability.svg | | | | traceabilityList.svg | | | | traceabilityStyle.svg | | | | user.svg | | | | user0.svg | | | | validCode.svg | | | | video.svg | | | | videoEquipment.svg | | | | videoKey.svg | | | | VIP.svg | | | | vipCustomized.svg | | | | warnings.svg | | | | workOrder.svg | | | | ┐┤?х╣▄└э.svg | | | | ╓╪╜Ё╩?╝ь▓т╣▄└э.svg | | | | | | | +---router | | | | router-static.js | | | | | | | +---store | | | | store.js | | | | | | | +---utils | | | | api.js | | | | base.js | | | | http.js | | | | i18n.js | | | | menu.js | | | | storage.js | | | | style.css | | | | style.js | | | | utils.js | | | | validate.js | | | | | | | \---views | | | | 404.vue | | | | center.vue | | | | home.vue | | | | index.vue | | | | login.vue | | | | pay.vue | | | | register.vue | | | | update-password.vue | | | | | | | \---modules | | | +---config | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionary | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionaryExampaper | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionaryExamquestion | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionaryForumState | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionaryKecheng | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionaryNews | | | | add-or-update.vue | | | | list.vue | | | | | | | +---dictionarySex | | | | add-or-update.vue | | | | list.vue | | | | | | | +---exampaper | | | | add-or-update.vue | | | | exam.vue | | | | list.vue | | | | | | | +---examquestion | | | | add-or-update.vue | | | | list.vue | | | | | | | +---examrecord | | | | add-or-update.vue | | | | list.vue | | | | | | | +---examredetails | | | | add-or-update.vue | | | | list.vue | | | | | | | +---examrewrongquestion | | | | add-or-update.vue | | | | list.vue | | | | | | | +---forum | | | | add-or-update.vue | | | | list.vue | | | | | | | +---jiaoshi | | | | add-or-update.vue | | | | list.vue | | | | | | | +---kecheng | | | | add-or-update.vue | | | | list.vue | | | | | | | +---kechengLiuyan | | | | add-or-update.vue | | | | list.vue | | | | | | | +---news | | | | add-or-update.vue | | | | list.vue | | | | | | | +---users | | | | add-or-update.vue | | | | list.vue | | | | | | | \---yonghu | | | add-or-update.vue | | | list.vue | | | | | \---upload | | a.txt | | config1.jpg | | config2.jpg | | config3.jpg | | file.rar | | jiaoshi.xls | | jiaoshi1.jpg | | jiaoshi2.jpg | | jiaoshi3.jpg | | jiaoshi4.jpg | | jiaoshi5.jpg | | jiaoshi6.jpg | | kecheng1.jpg | | kecheng2.jpg | | kecheng3.jpg | | kecheng4.jpg | | kecheng5.jpg | | kecheng6.jpg | | music.mp3 | | news1.jpg | | news2.jpg | | news3.jpg | | news4.jpg | | news5.jpg | | news6.jpg | | video.mp4 | | yonghu1.jpg | | yonghu2.jpg | | yonghu3.jpg | | | \---templates \---test \---java \---com Aiapp1ApplicationTests.java
07-15
2025-10-24 10:45:48.150 ERROR 25365 --- [[ACTIVE] ExecuteThread: '43' for queue: 'weblogic.kernel.Default (self-tuning)'] c.o.g.a.c.c.e.Error500Controller : 捕获到错误: NullPointerException: null 2025-10-24 10:45:48.158 ERROR 25365 --- [[ACTIVE] ExecuteThread: '76' for queue: 'weblogic.kernel.Default (self-tuning)'] c.o.group.apps.core.aop.ConsolePrintAop : 抛出异常 : null 2025-10-24 10:45:48.184 ERROR 25365 --- [[ACTIVE] ExecuteThread: '104' for queue: 'weblogic.kernel.Default (self-tuning)'] c.o.g.a.c.c.e.Error500Controller : 捕获到错误: NullPointerException: null 2025-10-24 10:45:48.319 ERROR 25365 --- [[ACTIVE] ExecuteThread: '29' for queue: 'weblogic.kernel.Default (self-tuning)'] c.o.g.a.c.c.e.Error500Controller : 捕获到错误: NullPointerException: null 2025-10-24 10:45:48.469 ERROR 25365 --- [[ACTIVE] ExecuteThread: '76' for queue: 'weblogic.kernel.Default (self-tuning)'] c.o.g.a.c.c.e.Error500Controller : 捕获到错误: NullPointerException: null 2025-10-24 10:45:48.520 ERROR 25365 --- [[ACTIVE] ExecuteThread: '20' for queue: 'weblogic.kernel.Default (self-tuning)'] o.s.b.w.servlet.support.ErrorPageFilter : Cannot forward to error page for request [/coa/coaSignOffPage] as the response has already been committed. As a result, the response may have the wrong status code. If your application is running on WebSphere Application Server you may be able to resolve this problem by setting com.ibm.ws.webcontainer.invokeFlushAfterService to false freemarker.core.InvalidReferenceException: The following has evaluated to null or missing: ==> Session["_USER_COMPANY_"] [in template "layout/jsPlugins.ftl" at line 66, column 45] ---- Tip: It's the final [] step that caused this error, not those before it. ---- Tip: If the failing expression is known to legally refer to something that's sometimes null or missing, either specify a default value like myOptionalVar!myDefault, or use <#if myOptionalVar??>when-present<#else>when-missing</#if>. (These only cover the last step of the expression; to cover the whole expression, use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)?? ---- ---- FTL stack trace ("~" means nesting-related): - Failed at: ${Session["_USER_COMPANY_"].companyCo... [in template "layout/jsPlugins.ftl" at line 66, column 43] - Reached through: #include "jsPlugins.ftl" [in template "layout/layout.ftl" in macro "sectionBody" at line 39, column 1] - Reached through: @sectionBody [in template "error/500.ftl" at line 7, column 1] ---- at freemarker.core.InvalidReferenceException.getInstance(InvalidReferenceException.java:134) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.UnexpectedTypeException.newDescriptionBuilder(UnexpectedTypeException.java:85) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.UnexpectedTypeException.<init>(UnexpectedTypeException.java:48) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.NonHashException.<init>(NonHashException.java:49) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Dot._eval(Dot.java:48) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Expression.eval(Expression.java:101) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.BuiltInsForExistenceHandling$ExistenceBuiltIn.evalMaybeNonexistentTarget(BuiltInsForExistenceHandling.java:52) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.BuiltInsForExistenceHandling$if_existsBI._eval(BuiltInsForExistenceHandling.java:129) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Expression.eval(Expression.java:101) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.DollarVariable.calculateInterpolatedStringOrMarkup(DollarVariable.java:100) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.DollarVariable.accept(DollarVariable.java:63) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.visit(Environment.java:334) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.visit(Environment.java:340) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.include(Environment.java:2925) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Include.accept(Include.java:171) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.visit(Environment.java:370) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.invokeMacroOrFunctionCommonPart(Environment.java:876) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.invokeMacro(Environment.java:812) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.UnifiedCall.accept(UnifiedCall.java:84) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.visit(Environment.java:334) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.visit(Environment.java:340) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.core.Environment.process(Environment.java:313) ~[freemarker-2.3.30.jar:2.3.30] at freemarker.template.Template.process(Template.java:383) ~[freemarker-2.3.30.jar:2.3.30] at org.springframework.web.servlet.view.freemarker.FreeMarkerView.processTemplate(FreeMarkerView.java:391) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.view.freemarker.FreeMarkerView.doRender(FreeMarkerView.java:304) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.view.freemarker.FreeMarkerView.renderMergedTemplateModel(FreeMarkerView.java:255) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.view.AbstractTemplateView.renderMergedOutputModel(AbstractTemplateView.java:179) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.view.AbstractView.render(AbstractView.java:316) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.DispatcherServlet.render(DispatcherServlet.java:1373) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1118) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:687) ~[javax.servlet.javax.servlet-api.jar:3.1.0] at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.6.RELEASE.jar:5.2.6.RELEASE] at javax.servlet.http.HttpServlet.service(HttpServlet.java:790) ~[javax.servlet.javax.servlet-api.jar:3.1.0] at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:295) ~[com.oracle.weblogic.servlet.jar:12.2.1.4] at weblogic.servlet.internal.StubSecurityHelper$ServletServiceAction.run(StubSecurityHelper.java:260) ~[com.oracle.weblogic.servlet.jar:12.2.1.4] at weblogic.servlet.internal.StubSecurityHelper.invokeServlet(StubSecurityHelper.java:137) ~[com.oracle.weblogic.servlet.jar:12.2.1.4] at weblogic.servlet.internal.ServletStubImpl.execute(ServletStubImpl.java:353) ~[com.oracle.weblogic.servlet.jar:12.2.1.4] at weblogic.servlet.internal.TailFilter.doFilter(TailFilter.java:25) ~[com.oracle.weblogic.servlet.jar:12.2.1.4] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at com.oda.group.apps.weaver.sso.filter.LoginFilter.doFilter(LoginFilter.java:122) ~[framework-weaver-3.0.0-SNAPSHOT.jar:3.0.0-SNAPSHOT] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:123) ~[druid-1.1.10.jar:1.1.10] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at org.jasig.cas.client.util.AssertionThreadLocalFilter.doFilter(AssertionThreadLocalFilter.java:50) ~[cas-client-core-3.4.1.jar:3.4.1] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at org.jasig.cas.client.util.HttpServletRequestWrapperFilter.doFilter(HttpServletRequestWrapperFilter.java:71) ~[cas-client-core-3.4.1.jar:3.4.1] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at org.jasig.cas.client.authentication.AuthenticationFilter.doFilter(AuthenticationFilter.java:155) ~[cas-client-core-3.4.1.jar:3.4.1] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] at org.jasig.cas.client.validation.AbstractTicketValidationFilter.doFilter(AbstractTicketValidationFilter.java:236) ~[cas-client-core-3.4.1.jar:3.4.1] at weblogic.servlet.internal.FilterChainImpl.doFilter(FilterChainImpl.java:78) [com.oracle.weblogic.servlet.jar:12.2.1.4] 会导致 调用analyseXml为null吗
10-25
你提供的日志非常关键,现在已经 **明确找到了问题的根源**。 --- ## ✅ 结论先行 > ❓ **“会导致调用 `/analyseXml` 返回 null 吗?”** > > **不会直接导致 `analyseXml` 接口返回 null**, > 但会 **间接引发整个页面崩溃、响应中断、前端拿不到数据** —— 表现上看起来就像 “analyseXml 没返回” 或 “为 null”。 --- ## 🔍 核心问题分析 ### 🧨 真实错误(已暴露): ```ftl freemarker.core.InvalidReferenceException: The following has evaluated to null or missing: ==> Session["_USER_COMPANY_"] [in template "layout/jsPlugins.ftl" at line 66, column 45] ``` 这是典型的: > 用户未登录或 Session 已过期 → `Session["_USER_COMPANY_"]` 为 `null` > 但在 FreeMarker 模板中直接访问 `.companyCode` → 抛出异常! --- ### ⚠️ 错误发生的位置 1. 文件:`jsPlugins.ftl` 2. 行号:66 3. 代码类似: ```ftl ${Session["_USER_COMPANY_"].companyCode} ``` 4. 调用链: - `error/500.ftl` → 包含 `layout.ftl` → `#include "jsPlugins.ftl"` - 所以即使你想展示一个“500错误页”,它也要渲染这个模板 → 再次触发 NPE! 👉 **恶性循环**: > 发生了一个异常 → 想跳转到 500 页面 → 渲染 500 页面时又因 Session 为空抛异常 → 响应已提交 → 无法转发 → 日志报错:“Cannot forward to error page” --- ### 📢 关键警告信息解释: ```text Cannot forward to error page for request [/coa/coaSignOffPage] as the response has already been committed. ``` 这意味着: - 应用试图处理某个异常并跳转到 500 错误页; - 但在跳转前,部分响应已经写回浏览器(committed); - WebLogic 不允许再转发(forward),所以错误页没显示; - 最终用户可能看到空白页、乱码或超时 —— **看似接口无返回(如 analyseXml 失败)** --- ## ✅ 是否会影响 `/analyseXml` 接口? | 影响类型 | 是否受影响 | 说明 | |--------|------------|------| | ✅ 直接影响 | ❌ 否 | `analyseXml` 本身逻辑不受此模板错误影响 | | ⚠️ 间接影响 | ✅ 是 | 如果请求 `/analyseXml` 的上下文也依赖该模板(如返回 HTML 页面而非 JSON),则可能中途崩溃 | | 🌐 全局影响 | ✅ 是 | 整个系统一旦进入“未登录+错误页不安全”的状态,所有页面都可能连锁崩溃 | --- ## 🎯 场景还原(发生了什么?) 1. 用户长时间未操作,Session 过期; 2. 用户刷新页面 `/coa/coaSignOffPage`; 3. 后端 Controller 可能抛了另一个异常(比如权限不足); 4. 系统尝试跳转到 `error/500.ftl`; 5. 在渲染 `500.ftl` 时,包含了 `jsPlugins.ftl`; 6. `jsPlugins.ftl` 访问了 `${Session["_USER_COMPANY_"].companyCode}` → 此时 Session 为空; 7. FreeMarker 抛出 `InvalidReferenceException` → 新的 NPE; 8. 此时响应头已输出 → WebLogic 报 “response already committed”; 9. 浏览器收不到完整响应 → 显示失败 → 前端以为 `analyseXml` 等接口没通。 --- ## ✅ 如何修复? ### ✅ 1. 修改 FreeMarker 模板:使用安全取值语法 #### 修改文件:`layout/jsPlugins.ftl` 将: ```ftl ${Session["_USER_COMPANY_"].companyCode} ``` 改为: ```ftl ${(Session["_USER_COMPANY_"]?default({})).companyCode!'UNKNOWN'} ``` 或者更严谨地判断: ```ftl <#assign company = Session["_USER_COMPANY_"]?default({}) /> var companyCode = "${company.companyCode!'UNKNOWN'}"; ``` 📌 这样即使 Session 不存在,也不会抛异常。 --- ### ✅ 2. 单独设计“无 Session”可用的错误页面 创建一个极简错误页,**完全不依赖任何 Session 变量** #### 示例:`error/500-simple.ftl` ```ftl <!DOCTYPE html> <html> <head> <title>系统错误</title> </head> <body> <h1>抱歉,系统出现错误</h1> <p>请稍后重试或重新登录。</p> <a href="/coa/login">点击重新登录</a> </body> </html> ``` 并在配置中指定 Spring Boot 使用这个页面: ```java @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/error").setViewName("error/500-simple"); } } ``` --- ### ✅ 3. 登录拦截器提前终止未认证请求 确保在进入 Controller 前就拦截非法访问: ```java @Component public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; HttpSession session = request.getSession(false); if (session == null || session.getAttribute("_USER_COMPANY_") == null) { String ajaxHeader = request.getHeader("X-Requested-With"); boolean isAjax = "XMLHttpRequest".equals(ajaxHeader); if (isAjax) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"Unauthorized\"}"); } else { response.sendRedirect("/login"); // 或 CAS 登录地址 } return; } chain.doFilter(request, response); } } ``` 这样就不会走到模板渲染阶段。 --- ### ✅ 4. AOP 和 ErrorController 必须打印完整堆栈 确保你的: - `ConsolePrintAop` - `Error500Controller` 都用的是: ```java logger.error("发生异常", throwable); // ✅ 正确方式 ``` 而不是: ```java logger.error("抛出异常 : " + throwable.getMessage()); // ❌ 导致显示 null ``` --- ## ✅ 总结 | 问题 | 回答 | |------|------| | 🤔 为什么报错? | FreeMarker 模板访问了空的 `Session["_USER_COMPANY_"]` | | 💡 为什么看到多个 NPE? | 多个线程并发请求,均因 Session 失效触发相同错误 | | ⚠️ 会影响 `analyseXml` 吗? | 不直接影响其逻辑,但若其页面依赖出错模板,则整体渲染失败,表现像“没返回” | | ✅ 如何解决? | 1. 模板加默认值<br>2. 错误页去 Session 依赖<br>3. 拦截器提前拦截<br>4. 日志补全堆栈 | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值