【14】SpringBoot的参数校验器-Validator

SpringBoot的参数校验器-Validator

01、分析

因为在日常的开发中,服务端对象的校验是非常重要的一个环节,

比如:注册的时候:校验用户名,密码,身份证,邮箱等信息是否为空,以及格式是否正确,

但是这种在日常的开发中进行校验太繁琐了,代码繁琐而且很多。

Validator框架应运而生,它的出现就是为了解决开发人员在开发的时候减少代码的,提升开发效率。它专门用来做接口的参数校验,比如:密码长度、是否为空等等。

Spring的validator校验框架遵守的是JSR-303的验证规范(参数校验规范),JSP全称:Java Specification Requests缩写。

在默认情况下:SpringBoot会引入hibernate validation机制来支持JSR-303验证规范。

SpringBoot的validator校验框架支持如下特征:

  • JSR303特征:JSR303是一项标准,只提供规范不提供实现。规定一些校验规范即校验注解。比如:@Null@NotNull@Pattern。这些类都位于:javax.validation.constraints包下。
  • hibernate validation特征:hibernate validation是对JSR303规范的实现并且进行了增强和扩展。并增加了注解:@Email@Length@Range等等。
  • Spring Validation:Spring Validation是对Hibernate Validation的二次封装。在SpringMvc模块中添加了自动校验器。并将校验信息封装到特定的类中。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UHImZE7k-1658331936478)(assets/1624709706407.png)]

小结

你明白验证数据的合法性是开发中非常重要的一个环节

它能保存数据的合法性的一种手段

常见的注解

JSR提供的校验注解:         
@Null   被注释的元素必须为 null    
@NotNull    被注释的元素必须不为 null    
@AssertTrue     被注释的元素必须为 true    
@AssertFalse    被注释的元素必须为 false    
@Min(value)     被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@Max(value)     被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@DecimalMin(value)  被注释的元素必须是一个数字,其值必须大于等于指定的最小值    
@DecimalMax(value)  被注释的元素必须是一个数字,其值必须小于等于指定的最大值    
@Size(max=, min=)   被注释的元素的大小必须在指定的范围内    
@Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内    
@Past   被注释的元素必须是一个过去的日期    
@Future     被注释的元素必须是一个将来的日期    
@Pattern(regex=,flag=)  被注释的元素必须符合指定的正则表达式    
Hibernate Validator提供的校验注解:  
@NotBlank(message =)   验证字符串非null,且trim后长度必须大于0    
@Email  被注释的元素必须是电子邮箱地址    
@Length(min=,max=)  被注释的字符串的大小必须在指定的范围内    
@NotEmpty   被注释的字符串的必须非空    
@Range(min=,max=,message=)  被注释的元素必须在合适的范围内

02、Spring整合validator

核心步骤概述

在spring框架框架的开发中,只需要两个步骤:

1:在需要校验的bean(entity)中的属性上增加对应注解

2:在springmvc的方法参数中的 entity中加==@Validated==的注解即可。

3:使用全局统一异常处理捕获的验证失败的提示信心

具体实现步骤
02-01、在pom.xml添加依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
02-02、新建用户实体并结合验证注解
@Data
public class UserVo {
    @NotNull(message = "用户id不能为空")
    private Long userId;
    @NotBlank(message = "用户名不能为空")
    @Length(max = 20, message = "用户名不能超过20个字符")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")
    private String username;
    @NotBlank(message = "手机号不能为空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")
    private String mobile;
    @NotBlank(message = "联系邮箱不能为空")
    @Email(message = "邮箱格式不对")
    private String email;
    @Future(message = "时间必须是将来时间")
    private Date createTime;
}
02-02、添加UserValidationController进行校验
@RestController
@Api(description = "用户校验")
@RequestMapping("/user")
public class UserValiatorController {
    @PostMapping("/valiator/reg")
    public UserVo createUser(@RequestBody @Validated UserVo userVo) {
        return userVo;
    }
}
02-03、通过接口测试和访问

http://localhost:8080/doc.html

{
  "code": 500,
  "data": null,
  "message": "服务器忙,请稍后在试"
}

通过前面的全局异常统一处理 + 统一返回 + 校验会得到如下的信息:

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.kuangstudy.kuangstudybootsvalid.vo.UserVo com.kuangstudy.kuangstudybootsvalid.controller.UserValiatorController.createUser(com.kuangstudy.kuangstudybootsvalid.vo.UserVo) with 4 errors: [Field error in object 'userVo' on field 'mobile': rejected value []; codes [NotBlank.userVo.mobile,NotBlank.mobile,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.mobile,mobile]; arguments []; default message [mobile]]; default message [手机号不能为空]] [Field error in object 'userVo' on field 'username': rejected value []; codes [NotBlank.userVo.username,NotBlank.username,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.username,username]; arguments []; default message [username]]; default message [用户名不能为空]] [Field error in object 'userVo' on field 'email': rejected value []; codes [NotBlank.userVo.email,NotBlank.email,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.email,email]; arguments []; default message [email]]; default message [联系邮箱不能为空]] [Field error in object 'userVo' on field 'mobile': rejected value []; codes [Pattern.userVo.mobile,Pattern.mobile,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [userVo.mobile,mobile]; arguments []; default message [mobile],[Ljavax.validation.constraints.Pattern$Flag;@766ada33,^[1][3,4,5,6,7,8,9][0-9]{9}$]; default message [手机号格式有误]] 

得知信息:

如果你校验失败,springmvc的validator内部会以异常的方式进行返回。报错异常:MethodArgumentNotValidException 而这个异常里面,包含所有的校验的提示信息。

我们现在要做的事情就是:

1、怎么把验证的异常进行捕获?

2、怎么把验证信息进行提取?

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 对服务器端出现500异常进行统一处理
     * 缺点:不明确
     * 场景:
     */
    @ExceptionHandler(Throwable.class)
    public ErrorHandler makeExcepton(Throwable e, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.SERVER_ERROR, e);
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), e);
        return errorHandler;
    }

    /**
     * 对服务器端出现500异常进行统一处理
     * 缺点:明确异常信息
     */
    @ExceptionHandler(BusinessException.class)
    public ErrorHandler makeOrderException(BusinessException businessException, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .message(businessException.getMessage())
                .status(businessException.getCode())
                .build();
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), businessException);
        return errorHandler;
    }


    /**
     * 对服务器端出现500异常进行统一处理
     * 缺点:明确异常信息
     */
    @ExceptionHandler(OrderException.class)
    public ErrorHandler makeOrderException(OrderException orderException, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .message(orderException.getMessage())
                .status(orderException.getCode())
                .build();
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), orderException);
        return errorHandler;
    }


    /**
     * 对验证的统一异常进行统一处理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ErrorHandler handlerValiator(MethodArgumentNotValidException e, HttpServletRequest request) throws JsonProcessingException {
        // 1: 从MethodArgumentNotValidException提取验证失败的所有的信息。返回一个List<FieldError>
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        // 2: 把fieldErrors中,需要的部分提出出来进行返回
        List<Map<String, String>> mapList = toValidatorMsg(fieldErrors);
        // 3: 把需要的异常信息转换成json进行返回
        ObjectMapper objectMapper = new ObjectMapper();
        String mapJson = objectMapper.writeValueAsString(mapList);
        ErrorHandler errorHandler = ErrorHandler.fail(ResultCodeEnum.PARAM_ERROR, e, mapJson);
        return errorHandler;
    }


    /**
     * 对验证异常进行统一处理提取需要的部分
     *
     * @param fieldErrorList
     * @return
     */
    private List<Map<String, String>> toValidatorMsg(List<FieldError> fieldErrorList) {
        List<Map<String, String>> mapList = new ArrayList<>();
        // 循环提取
        for (FieldError fieldError : fieldErrorList) {
            Map<String, String> map = new HashMap<>();
            // 获取验证失败的属性
            map.put("field", fieldError.getField());
            // 获取验证失败的的提示信息
            map.put("msg", fieldError.getDefaultMessage());
            mapList.add(map);
        }
        return mapList;
    }
}

03、自定义校验器

03-01、步骤1:首先定义验证异常注解
package com.kuangstudy.common.ano;
import com.kuangstudy.config.handler.PhoneValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "手机格式不正确!";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Phone[] value();
    }
}
03-02、步骤2:定义校验器
package com.kuangstudy.kuangstudybootsvalid.common;

import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description:
 * @author: xuke
 * @time: 2021/6/26 20:57
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {

    @Override
    public boolean isValid(String phonevalue, ConstraintValidatorContext constraintValidatorContext) {
        // 1: 如果用户没输入直接返回不校验,因为空的判断应该交给@NotNull去做就行了
        if (StringUtils.isEmpty(phonevalue)) {
            return true;
        }
        // 2: 如果填写手机号码就进行正则校验
        Pattern p = Pattern.compile("^(13[0-9]|14[5|7|9]|15[0|1|2|3|5|6|7|8|9]|17[0|1|6|7|8]|18[0-9])\\d{8}$");
        // 2:如果校验通过就返回true,否则返回false;
        Matcher matcher = p.matcher(phonevalue);
        return matcher.matches();
    }


    @Override
    public void initialize(Phone constraintAnnotation) {
    }
}

03-03、步骤3:在对应实体的属性上进行校验即可
package com.kuangstudy.kuangstudybootsvalid.vo;
import com.kuangstudy.kuangstudybootsvalid.common.Phone;
import lombok.Data;
import org.hibernate.validator.constraints.Length;

import javax.validation.constraints.*;
import java.util.Date;
/**
 * @author 飞哥
 * @Title: 学相伴出品
 * @Description: 我们有一个学习网站:https://www.kuangstudy.com
 * @date 2021/6/2 14:29
 */
@Data
public class UserVo {

    @NotBlank(message = "请输入phone")
    @Phone
    private String phone;

}

建议:一定要注意保存校验器的独立性(可输入和不输入都满足).

03-04、其他的校验器的验证可以参考如下:
/**
 * 常用的一些验证,如手机、移动号码、联通号码、电信号码、密码、座机、 邮政编码、邮箱、年龄、身份证、URL、QQ、汉字、字母、数字等
 */
public class ValidateUtil {
    /**
     * 手机号规则
     */
    public static final String MOBILE_PATTERN = "^((13[0-9])|(14[0-9])|(15[0-9])|(17[0-9])|(18[0-9]))(\\d{8})$";
    /**
     * 中国电信号码格式验证 手机段: 133,153,180,181,189,177,1700,173
     **/
    private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|53|7[37]|8[019])\\d{8}$)|(?:^(?:\\+86)?1700\\d{7}$)";
    /**
     * 中国联通号码格式验证 手机段:130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     **/
    private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[5]|5[56]|7[56]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[7-9]\\d{7}$)";
    /**
     * 中国移动号码格式验证 手机段:134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     **/
    private static final String CHINA_MOVE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[7]|5[0-27-9]|7[8]|8[2-478])\\d{8}$)|(?:^(?:\\+86)?1705\\d{7}$)";
    /**
     * 密码规则(6-16位字母、数字)
     */
    public static final String PASSWORD_PATTERN = "^[0-9A-Za-z]{6,16}$";
    /**
     * 固号(座机)规则
     */
    public static final String LANDLINE_PATTERN = "^(?:\\(\\d{3,4}\\)|\\d{3,4}-)?\\d{7,8}(?:-\\d{1,4})?$";
    /**
     * 邮政编码规则
     */
    public static final String POSTCODE_PATTERN = "[1-9]\\d{5}";
    /**
     * 邮箱规则
     */
    public static final String EMAIL_PATTERN = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
    /**
     * 年龄规则 1-120之间
     */
    public static final String AGE_PATTERN = "^(?:[1-9][0-9]?|1[01][0-9]|120)$";
    /**
     * 身份证规则
     */
    public static final String IDCARD_PATTERN = "^\\d{15}|\\d{18}$";
    /**
     * URL规则,http、www、ftp
     */
    public static final String URL_PATTERN = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
    /**
     * QQ规则
     */
    public static final String QQ_PATTERN = "^[1-9][0-9]{4,13}$";
    /**
     * 全汉字规则
     */
    public static final String CHINESE_PATTERN = "^[\u4E00-\u9FA5]+$";
    /**
     * 全字母规则
     */
    public static final String STR_ENG_PATTERN = "^[A-Za-z]+$";
    /**
     * 整数规则
     */
    public static final String INTEGER_PATTERN = "^-?[0-9]+$";
    /**
     * 正整数规则
     */
    public static final String POSITIVE_INTEGER_PATTERN = "^\\+?[1-9][0-9]*$";
    /**
     * @param mobile 手机号码
     * @return boolean
     * @Description: 验证手机号码格式
     */
    public static boolean validateMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(MOBILE_PATTERN);
    }
    /**
     * 验证是否是电信手机号,133、153、180、189、177
     *
     * @param mobile 手机号
     * @return boolean
     */
    public static boolean validateTelecom(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_TELECOM_PATTERN);
    }
    /**
     * 验证是否是联通手机号 130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     *
     * @param mobile 电话号码
     * @return boolean
     */
    public static boolean validateUnionMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_UNICOM_PATTERN);
    }
    /**
     * 验证是否是移动手机号
     *
     * @param mobile 手机号 134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     * @return boolean
     */
    public static boolean validateMoveMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_MOVE_PATTERN);
    }
    /**
     * @param pwd 密码
     * @return boolean
     * @Description: 验证密码格式  6-16 位字母、数字
     */
    public static boolean validatePwd(String pwd) {
        if (StringUtils.isEmpty(pwd)) {
            return Boolean.FALSE;
        }
        return Pattern.matches(PASSWORD_PATTERN, pwd);
    }
    /**
     * 验证座机号码,格式如:58654567,023-58654567
     *
     * @param landline 固话、座机
     * @return boolean
     */
    public static boolean validateLandLine(final String landline) {
        if (StringUtils.isEmpty(landline)) {
            return Boolean.FALSE;
        }
        return landline.matches(LANDLINE_PATTERN);
    }
    /**
     * 验证邮政编码
     *
     * @param postCode 邮政编码
     * @return boolean
     */
    public static boolean validatePostCode(final String postCode) {
        if (StringUtils.isEmpty(postCode)) {
            return Boolean.FALSE;
        }
        return postCode.matches(POSTCODE_PATTERN);
    }
    /**
     * 验证邮箱(电子邮件)
     *
     * @param email 邮箱(电子邮件)
     * @return boolean
     */
    public static boolean validateEamil(final String email) {
        if (StringUtils.isEmpty(email)) {
            return Boolean.FALSE;
        }
        return email.matches(EMAIL_PATTERN);
    }
    /**
     * 判断年龄,1-120之间
     *
     * @param age 年龄
     * @return boolean
     */
    public static boolean validateAge(final String age) {
        if (StringUtils.isEmpty(age)) {
            return Boolean.FALSE;
        }
        return age.matches(AGE_PATTERN);
    }
    /**
     * 身份证验证
     *
     * @param idCard 身份证
     * @return boolean
     */
    public static boolean validateIDCard(final String idCard) {
        if (StringUtils.isEmpty(idCard)) {
            return Boolean.FALSE;
        }
        return idCard.matches(IDCARD_PATTERN);
    }
    /**
     * URL地址验证
     *
     * @param url URL地址
     * @return boolean
     */
    public static boolean validateUrl(final String url) {
        if (StringUtils.isEmpty(url)) {
            return Boolean.FALSE;
        }
        return url.matches(URL_PATTERN);
    }
    /**
     * 验证QQ号
     *
     * @param qq QQ号
     * @return boolean
     */
    public static boolean validateQq(final String qq) {
        if (StringUtils.isEmpty(qq)) {
            return Boolean.FALSE;
        }
        return qq.matches(QQ_PATTERN);
    }
    /**
     * 验证字符串是否全是汉字
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateChinese(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(CHINESE_PATTERN);
    }
    /**
     * 判断字符串是否全字母
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateStrEnglish(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(STR_ENG_PATTERN);
    }
    /**
     * 判断是否是整数,包括负数
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validateInteger(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(INTEGER_PATTERN);
    }
    /**
     * 判断是否是大于0的正整数
     *
     * @param str 字符串
     * @return boolean
     */
    public static boolean validatePositiveInt(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(POSITIVE_INTEGER_PATTERN);
    }
}
03-05、小结

对于上述的问题,只能验证实体相关的校验。但是对于下面的可能就不能进行验证处理。如下:

/*
     * @Author xuke
     * @Description 对uservo进行校验
     * @Date 20:29 2021/6/26
     * @Param [userVo]
     * @return com.kuangstudy.kuangstudybootsvalid.vo.UserVo
     **/
    @PostMapping("/valiator/reg2")
    public UserVo createUser2(String name, Integer flag) {
       
        return null;
    }

怎么做呢?使用 Assert即可。

04、SpringBoot-Assert和自定义KAssert

04-01、Assert参数校验

Assert是断言的意思,它是Spring提供的一个工具类,org.springframework.util.Assert
Web 应用在接受表单提交的数据后都需要对其进行合法性检查,如果表单数据不合法,请求将被驳回。类似的,当我们在编写类的方法时,也常常需要对方法入参进行合法性检查,如果入参不符合要求,方法将通过抛出异常的方式拒绝后续处理。

 /*
     * @Author xuke
     * @Description 对uservo进行校验 @Valid String name;
     * @Date 20:29 2021/6/26
     * @Param [userVo]
     * @return com.kuangstudy.kuangstudybootsvalid.vo.UserVo
 **/
@PostMapping("/valiator/reg2")
public UserVo createUser2(String name,Integer flag) {
    Assert.isNull(name,"用户名不允许为空!");// 但是这个不明确不利于扩展
    return null;
}

上面默认情况下会出现:IllegalArgumentException 异常,结果如下:

{
  "code": 500,
  "data": null,
  "message": "服务器忙,请稍后在试"
}

不方便管理和控制。

04-02、自定义KAssert参数校验
步骤1:自定义KAssert
public class KAssert {

    public static void isEmpty(Object object,Integer code,String message) {
        if (object == null || "".equals(object)) {
            throw new ValidationException(code,message);
        }
    }

    public static void isEmpty(Object object, ResultCodeEnum resultCodeEnum) {
        if (object == null || "".equals(object)) {
            throw new ValidationException(resultCodeEnum.getCode(),resultCodeEnum.getMessage());
        }
    }
}

步骤2:自定义KAssert对应的异常
@Data
public class ValidationException extends RuntimeException {
    private Integer code;
    private String message;
    public ValidationException(ResultCodeEnum resultCodeEnum) {
        this.code = resultCodeEnum.getCode();
        this.message = resultCodeEnum.getMessage();
    }
    public ValidationException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
步骤3:全局异常统一处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 对自定义异常的统一处理
     * 缺点:明确异常信息
     */
    @ExceptionHandler(ValidationException.class)
    public ErrorHandler makevalidationException(ValidationException validationException, HttpServletRequest request) {
        ErrorHandler errorHandler = ErrorHandler.builder()
                .message(validationException.getMessage())
                .status(validationException.getCode())
                .build();
        log.error("请求的地址是:{},出现的异常是:{}", request.getRequestURL(), validationException);
        return errorHandler;
    }

}
步骤4:测试结果

http://localhost:8080/doc.html

{
  "code": 401,
  "data": null,
  "message": "请输入用户名!"
}
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值