@Valid 注解 + 全局处理器优雅处理参数验证

@Valid 注解 + 全局处理器优雅处理参数验证

相关地址:

参考 文档:公众号文章


系统环境:
Jdk 版本:jdk 8
SpringBoot 版本:2.3.12.RELEASE

一、为什么使用 @Valid 来验证参数

在平常通过 Spring 框架写代码时候,会经常写接口类,相信大家对该类的写法非常熟悉。在写接口时经常要写效验请求参数逻辑,这时候我们会常用做法是写大量的 if 与 if else 类似这样的代码来做判断,如下:

@RestController
public class TestController {

    @PostMapping("/user")
    public String addUserInfo(@RequestBody User user) {
        if (user.getName() == null || "".equals(user.getName()) {
            ......
        } else if(user.getSex() == null || "".equals(user.getSex())) {
            ......
        } else if(user.getUsername() == null || "".equals(user.getUsername())) {
            ......
        } else {
            ......
        }
        ......
    }

}

这样的代码如果按正常代码逻辑来说,是没有什么问题的,不过按优雅来说,简直糟糕透了。不仅不优雅,而且如果存在大量的验证逻辑,这会使代码看起来乱糟糟,大大降低代码可读性,那么有没有更好的方法能够简化这个过程呢?

答案当然是有,推荐的是使用 @Valid 注解来帮助我们简化验证逻辑。

二、@Valid 注解的作用

注解 @Valid 的主要作用是用于数据效验,可以在定义的实体中的属性上,添加不同的注解来完成不同的校验规则,而在接口类中的接收数据参数中添加 @valid 注解,这时你的实体将会开启一个校验的功能。

三、@Valid 的相关注解

下面是 @Valid 相关的注解,在实体类中不同的属性上添加不同的注解,就能实现不同数据的效验功能。

@Valid注解大全及用法规范

注解描述
@AssertFalse带注解的元素必须为false,支持boolean/Boolean
@AssertTrue带注解的元素必须为true,支持boolean/Boolean
@DecimalMax带注解的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin带注解的元素必须是一个数字,其值必须大于等于指定的最小值
@Digits带注解的元素必须是一个可接受范围内的数字
@Future带注解的元素必须是将来的某个时刻、日期或时间
@Max带注解的元素必须是一个数字,其值必须小于等于指定的最大值
@Min带注解的元素必须是一个数字,其值必须大于等于指定的最小值
@NotNull带注解的元素不能是Null
@Null带注解的元素必须是Null
@Past带注解的元素必须是过去的某个时刻、日期或时间
@Pattern带注解的元素必须符合指定的正则表达式
@Size带注解的元素必须大于等于指定的最小值,小于等于指定的最大值
@Email带注解的元素必须是格式良好的电子邮箱地址
@NotEmpty带注解的元素不能是空,String类型不能为null,Array、Map不能为空,切size/length大于0
@NotBlank字符串不能为空、空字符串、全空格
@URL字符串必须是一个URL

四、使用 @Valid 进行参数效验步骤

整个过程如下图所示,用户访问接口,然后进行参数效验,因为 @Valid 不支持平面的参数效验(直接写在参数中字段的效验)所以基于 GET 请求的参数还是按照原先方式进行效验,而 POST 则可以以实体对象为参数,可以使用 @Valid 方式进行效验。如果效验通过,则进入业务逻辑,否则抛出异常,交由全局异常处理器进行处理。

图片

1、实体类中添加 @Valid 相关注解

使用 @Valid 相关注解非常简单,只需要在参数的实体类中属性上面添加如 @NotBlank、@Max、@Min 等注解来对该字段进限制,如下:

User:

@Data
public class User {
    @NotBlank(message = "姓名不为空")
    private String username;
    @NotBlank(message = "密码不为空")
    private String password;
}

如果是嵌套的实体对象,则需要在最外层属性上添加 @Valid 注解:

User:

@Data
public class User {
    @NotBlank(message = "姓名不为空")
    private String username;
    @NotBlank(message = "密码不为空")
    private String password;
    //嵌套必须加 @Valid,否则嵌套中的验证不生效
    @Valid
    @NotNull(message = "用户信息不能为空")
    private UserInfo userInfo;
}

UserInfo:

@Data
public class UserInfo {
    @NotBlank(message = "年龄不为空")
    @Max(value = 18, message = "不能超过18岁")
    private String age;
    @NotBlank(message = "性别不能为空")
    private String gender;
}

2、接口类中添加 @Valid 注解

在 Controller 类中添加接口,POST 方法中接收设置了 @Valid 相关注解的实体对象,然后在参数中添加 @Valid 注解来开启效验功能,需要注意的是, @Valid 对 Get 请求中接收的平面参数请求无效,稍微略显遗憾。

@RestController
public class TestController {

    @PostMapping("/save")
    public String addUserInfo(@Valid @RequestBody User user) {
        return "调用成功!";
    }

}

3、全局异常处理类中处理 @Valid 抛出的异常

最后,我们写一个全局异常处理类,然后对接口中抛出的异常进行处理,而 @Valid 配合 Spring 会抛出 MethodArgumentNotValidException 异常,这里我们需要对该异常进行处理即可。

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.BAD_REQUEST) //设置状态码为 400
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public String paramExceptionHandler(MethodArgumentNotValidException e) {
        BindingResult exceptions = e.getBindingResult();
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (exceptions.hasErrors()) {
            List<ObjectError> errors = exceptions.getAllErrors();
            if (!errors.isEmpty()) {
                // 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
                FieldError fieldError = (FieldError) errors.get(0);
                return fieldError.getDefaultMessage();
            }
        }
        return "请求参数错误";
    }

}

五、SpringBoot 中使用 @Valid 示例

1、Maven 引入相关依赖

Maven 引入 SpringBoot 相关依赖,这里引入了 Lombok 包来简化开发过程。

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
    </dependencies>

2、自定义个异常类

自定义个异常类,方便我们处理 GET 请求(GET 请求参数中一般是没有实体对象的,所以不能使用 @Valid),当请求验证失败时,手动抛出自定义异常,交由全局异常处理。

public class ParamErrorException extends RuntimeException {

    public ParamErrorException() {
    }

    public ParamErrorException(String message) {
        super(message);
    }

}

3、自定义响应类

定义一个返回信息的枚举类,方便我们快速响应信息,不必每次都写返回消息和响应码。

public enum RespCode {
    SUCCESS(200, "OK", "请求成功"),
    FAIL(9999, "FAIL", "请求失败"),
    BAD_REQUEST(400, "Bad Request", "参数无效"),
    UNAUTHORIZED(401, "Unauthorized", "未经授权的访问,由于凭据无效被拒绝"),
    FORBIDDEN(403, "Forbidden", "请求资源的访问被服务器拒绝"),
    NOT_FOUND(404, "Not Found","请求路径不存在"),
    METHOD_NOT_ALLOWED(405, "Method Not Allowed", "请求的HTTP方法不允许"),
    REQUEST_TIMEOUT(408, "Request Timeout", "请求超时"),
    INTERNAL_SERVER_ERROR(500, "Internal Server Error", "服务器内部系统未知异常"),
    BAD_GATEWAY(502, "Bad Gateway", "网关异常"),
    GATEWAY_TIMEOUT(504, "Gateway Timeout", "网关超时"),
    CONNECT_EXCEPTION(530, "Service Connect Exception", "服务连接异常"),
    NULL_POINTER_EXCEPTION(550, "Null Pointer Exception", "服务器内部空指针异常"),;
    
    private Integer code;
    private String desc;
    private String message;
    
    RespCode(Integer code, String desc,String message) {
        this.code = code;
        this.desc = desc;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}

4、自定义响应对象类

创建用于返回调用方的响应信息的实体类。

import lombok.Data;

@Data
public class ResponseResult {
    private Integer code;
    private String msg;

    public ResponseResult(){
    }

    public ResponseResult(RespCode resultEnum){
        this.code = resultEnum.getCode();
        this.msg = resultEnum.getMessage();
    }

    public ResponseResult(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

5、自定义实体类中添加 @Valid 相关注解

下面将创建用于 POST 方法接收参数的实体对象,里面添加 @Valid 相关验证注解,并在注解中添加出错时的响应消息。

User

import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

/**
 * user实体类
 */
@Data
public class User {
    private Long userId;
    @NotBlank(message = "姓名不为空")
    private String username;
    @NotBlank(message = "密码不为空")
    private String password;
    // 嵌套必须加 @Valid,否则嵌套中的验证不生效
    @Valid
    @NotNull(message = "userinfo不能为空")
    private UserInfo userInfo;
}

UserInfo

import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;

@Data
public class UserInfo {
    @NotBlank(message = "年龄不为空")
    @Max(value = 18, message = "不能超过18岁")
    private String age;
    @NotBlank(message = "性别不能为空")
    private String gender;
}

6、Controller 中添加 @Valid 注解

接口类中添加 GET 和 POST 方法的两个接口用于测试,其中 POST 方法以上面创建的 Uer 实体对象接收参数,并使用 @Valid,而 GET 请求一般接收参数较少,所以使用正常判断逻辑进行参数效验。

package com.it.controller;

import com.it.entity.User;
import com.it.enums.ResponseResult;
import org.springframework.web.bind.annotation.*;

import javax.validation.Valid;

@RestController
public class TestController {

    /**
     * 获取用户信息
     *
     * @param userId 用户id
     * @return ResponseResult
     */
    @GetMapping("/info")
    public ResponseResult findUserInfo(@RequestParam  String userId) {
         if (StringUtils.isEmpty(userId)) {
            throw new ParamErrorException("userId 不能为空");
        }
        return new ResponseResult(ResultEnum.SUCCESS);
    }
 	/**
     * 新增用户
     *
     * @param user 用户信息
     * @return ResponseResult
     */
    @PostMapping("/save")
    public String addUserInfo(@Valid @RequestBody User user) {
        return "调用成功!";
    }

}

7、全局异常处理

这里创建一个全局异常处理类,方便统一处理异常错误信息。里面添加了不同异常处理的方法,专门用于处理接口中抛出的异常信。

package com.it.exception;

import com.it.response.ResponseResult;
import com.it.response.RespCode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;

import java.util.List;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 请求参数缺失异常
     *
     * @param e 请求参数缺失异常
     * @return ResponseResult
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ResponseResult parameterMissingExceptionHandler(MissingServletRequestParameterException e) {
        log.error("", e);
        return new ResponseResult(RespCode.BAD_REQUEST.getCode(), "请求参数 " + e.getParameterName() + " 不能为空");
    }
    /**
     * 404找不到资源
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseResult notFound(NoHandlerFoundException e) {
        return new ResponseResult(RespCode.NOT_FOUND.getCode(),RespCode.NOT_FOUND.getMessage());
    }

    /**
     * 缺少请求体异常处理器
     *
     * @param e 缺少请求体异常
     * @return ResponseResult
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) {
        log.error("", e);
        if (!StringUtils.isEmpty(e.getMessage())) {
            return new ResponseResult(RespCode.BAD_REQUEST.getCode(), e.getMessage());
        }
        return new ResponseResult(RespCode.BAD_REQUEST.getCode(), "缺少请求体!");
    }

    @ExceptionHandler(value = NullPointerException.class)
    public ResponseResult nullPointerExceptionHandler(NullPointerException e) {
        log.error("", e);
        return new ResponseResult(RespCode.NULL_POINTER_EXCEPTION);
    }

    /**
     * 参数效验异常处理器 拦截 @Valid 校验失败的情况
     *
     * @param e 参数验证异常
     * @return ResponseInfo
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseResult parameterExceptionHandler(MethodArgumentNotValidException e) {
        log.error("", e);
        // 获取异常信息
        return getResponseResult(e.getBindingResult());
    }
    /**
     * 请求参数校验失败,拦截 @Validated 校验失败的情况
     * 两个注解 @Valid 和 @Validated 区别是后者可以加分组校验,前者没有分组校验
     */
    @ResponseStatus(value = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(BindException.class)
    public ResponseResult bindException(BindException e) {
        log.error("", e);
        return getResponseResult(e.getBindingResult());
    }

    /**
     * 自定义参数错误异常处理器
     *
     * @param e 自定义参数
     * @return ResponseInfo
     */
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler({ParamErrorException.class})
    public ResponseResult paramExceptionHandler(ParamErrorException e) {
        log.error("", e);
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (!StringUtils.isEmpty(e.getMessage())) {
            return new ResponseResult(RespCode.BAD_REQUEST.getCode(), e.getMessage());
        }
        return new ResponseResult(RespCode.BAD_REQUEST);
    }

    private static ResponseResult getResponseResult(BindingResult e) {
        // 获取异常信息
        BindingResult exceptions = e;
        // 判断异常中是否有错误信息,如果存在就使用异常中的消息,否则使用默认消息
        if (exceptions.hasErrors()) {
            List<ObjectError> errors = exceptions.getAllErrors();
            if (!errors.isEmpty()) {
                // 这里列出了全部错误参数,按正常逻辑,只需要第一条错误即可
                FieldError fieldError = (FieldError) errors.get(0);
                return new ResponseResult(RespCode.BAD_REQUEST.getCode(), fieldError.getDefaultMessage());
            }
        }
        return new ResponseResult(RespCode.BAD_REQUEST);
    }

}

8、启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

9、示例测试

下面将针对上面示例中设置的两种接口进行测试,分别来验证参数效验功能。

|| - 测试接口 /info

使用 GET 方法请求地址 http://localhost:8080/info?userId=1 时,返回信息:

{
    "code": 200,
    "msg": "请求成功"
}

当不输入参数,输入地址 http://localhost:8080/info 时,返回信息:

{
    "code": 400,
    "msg": "请求参数 userId 不能为空"
}

可以看到在执行 GET 请求,能够正常按我们全局异常处理器中的设置处理异常信息。

|| - 测试接口 /save

(1)、使用 POST 方法发起请求,首先进行不加 JSON 请求体来对 http://localhost:8080/save 地址进行请求,返回信息:

{
    "code": 400,
    "msg": "参数体不能为空"
}

(2)、输入部分参数进行测试。

请求内容:

{
 "username":"test",
 "pa***rd":"***"
}

返回信息:

{
    "code": 400,
    "msg": "用户信息不能为空"
}

(3)、输入完整参数,且设置 age > 18 时,进行测试。

{
    "username":"zs",
    "pa***ord":"111",
    "userInfo": {
        "age":"19",
        "gender":"男"
    }
}

返回信息:

2022-09-14 15:53:01.483 ERROR 33592 --- [nio-8000-exec-3] com.it.exception.GlobalExceptionHandler  : 

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.it.response.ResponseResult com.it.controller.TestController1.addUserInfo(com.it.entity.User): [Field error in object 'user' on field 'userInfo.age': rejected value [19]; codes [Max.user.userInfo.age,Max.userInfo.age,Max.age,Max.java.lang.String,Max]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.userInfo.age,userInfo.age]; arguments []; default message [userInfo.age],18]; default message [不能超过18岁]] 

{
    "code": 400,
    "msg": "不能超过18岁"
}

可以看到在执行 POST 请求,也能正常按我们全局异常处理器中的设置处理异常信息,且提示信息为我们设置在实体类中的 Message。

10 @Validated 注解开启注解校验功能

上述例子咱们用的是@Valid进行校验的,用@Validated注解也可以,那么他两的区别是什么呢?

所属不同:
该注解所属包为:javax.validation.Valid,而 @Validated是Spring基于@Valid进一步封装,并提供了一些高级功能,如分组,分组顺序等

11.@Valid和@Validated高级使用

1.Valid级联校验
级联校验也叫嵌套检测,嵌套即一个实体类包含另一个实体类

@Data
public class User {
    @NotBlank(message = "姓名不为空")
    private String username;
    @NotBlank(message = "密码不为空")
    private String password;
    //嵌套必须加 @Valid,否则嵌套中的验证不生效
    @Valid
    @NotNull(message = "用户信息不能为空")
    private UserInfo userInfo;
}

2.@Validated分组校验

分组校验是由@Validated的value方法提供的,用于开启指定的组校验,分别作用不同的业务场景中

BaseRequest:

@Data
public class BaseRequest implements Serializable {
    public BaseRequest() {
    }
    public interface Detail {
    }
    public interface Delete {
    }
    public interface Add {
    }
    public interface Edit {
    }
}

User:

@Data
public class User extends BaseRequest {

    @NotNull(message = "id不能为空",groups = {Detail.class,Delete.class, Edit.class})
    @Min(value = 1, message = "id必须为正整数",groups = {Detail.class,Delete.class, Edit.class})
    private Long userId;

    @NotBlank(message = "姓名不为空",groups = {Add.class})
    private String username;

    @NotBlank(message = "密码不为空",groups = {Add.class})
    private String password;
    
	@NotBlank(message = "地址不为空")
    private String add;

    private String mobile;

    //嵌套必须加 @Valid,否则嵌套中的验证不生效
    @Valid
    @NotNull(message = "用户信息不能为空",groups = {Add.class})
    private UserInfo userInfo;
}

Controller

@Validated
@RestController
public class TestController {

    /**
     * 获取用户信息
     *
     * @param userId 用户id
     * @return ResponseResult
     */
    @GetMapping("/info")
    public ResponseResult info(@RequestParam String userId) {
        if (StringUtils.isEmpty(userId)) {
            throw new ParamErrorException("userId 不能为空");
        }
        return new ResponseResult(RespCode.SUCCESS);
    }

    //@Validated注解对于非实体类的校验,在类上注解才会起效果
    @GetMapping("/info1")
    public ResponseResult info1(@NotBlank(message = "参数不能为空") @RequestParam String userId) {

        return new ResponseResult(RespCode.SUCCESS);
    }

    /**
     * 新增用户
     *
     * @param user 用户信息
     * @return ResponseResult
     */
    @RequestMapping(value = "/save", method = RequestMethod.POST)
    public ResponseResult save(@Validated({BaseRequest.Add.class}) @RequestBody User user) {

        return new ResponseResult(RespCode.SUCCESS);
    }

    /**
     * 修改用户信息
     *
     * @param user 用户信息
     * @return ResponseResult
     */
    @RequestMapping(value = "/edit", method = RequestMethod.PUT)
    public ResponseResult update(@Validated({BaseRequest.Edit.class}) @RequestBody User user) {

        return new ResponseResult(RespCode.SUCCESS);
    }

}

12. 测试接口 /save

(1)、使用 POST 方法发起请求,对 http://localhost:8080/save 地址进行请求,

请求信息:

{
    "username":"zs",
    "userInfo": {
        "age":"18",
        "gender":"男"
    }
}

返回信息:

{
    "code": 400,
    "msg": "密码不为空"
}

结论:

如果校验注解添加上groups方法并指定分组,只有@Validated注解value方法指定该分组,才会开启校验注解的校验数据功能

13. 解决@ControllerAdvice不能捕获NoHandlerFoundException

对不存在的接口进行请求 http://localhost:8080/save1

{
    "timestamp": "2022-09-14T08:32:26.863+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "",
    "path": "/save2"
}

确保异常类里面已经添加了捕获Handler

    /**
     * 404找不到资源
     */
    @ExceptionHandler(NoHandlerFoundException.class)
    @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
    public ResponseResult notFound(NoHandlerFoundException e) {
        return new ResponseResult(RespCode.NOT_FOUND.getCode(),RespCode.NOT_FOUND.getMessage());
    }

研究DispatcherServlet源码发现,NoHandlerFoundException异常能否被抛出,关键在如下代码:

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
	noHandlerFound(processedRequest, response);
	return;
}
protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
		if (pageNotFoundLogger.isWarnEnabled()) {
			pageNotFoundLogger.warn("No mapping for " + request.getMethod() + " " + getRequestUri(request));
		}
		//private boolean throwExceptionIfNoHandlerFound = false;
		if (this.throwExceptionIfNoHandlerFound) {
			throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
					new ServletServerHttpRequest(request).getHeaders());
		}
		else {
			response.sendError(HttpServletResponse.SC_NOT_FOUND);
		}
	}

只有找不到对应该请求的处理器时,才会进入下面的noHandler方法去抛出NoHandlerFoundException异常。
所以配置如下:

spring:
  mvc:
   throw-exception-if-no-handler-found: true

通过测试发现,光配置这个还不行,springboot的WebMvcAutoConfiguration会默认配置如下资源映射:

/映射到/static(或/public、/resources、/META-INF/resources) /webjars/ 映射到classpath:/META-INF/resources/webjars/ /**/favicon.ico映射favicon.ico文件.

这下就明白了,即使你的地址错误,仍然会匹配到/**这个静态资源映射地址,就不会进入noHandlerFound方法,自然不会抛出NoHandlerFoundException了。
所以,我们需要的就是改掉默认的静态资源映射访问路径就可以了。
配置如下属性,NoHandlerFoundException异常就能被@ControllerAdvice捕获了

application.yaml

server:
  port: 8000
spring:
  mvc:
   throw-exception-if-no-handler-found: true
   static-path-pattern: /statics/**

{
    "code": 404,
    "msg": "请求路径不存在"
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

橘右今

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

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

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

打赏作者

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

抵扣说明:

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

余额充值