商品服务-品牌管理表单校验

1. 前端校验

前端校验功能防君子,后端检验功能防小人(防止绕过前端界面直接发请求)。

Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。

  • 自定义校验规则dataRule
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="140px">
</el-form>

1.1 品牌名、logo地址、介绍

这三项都不为空即可。

  • 绑定校验规则的name、logo、descript属性,同时和data中的dataForm.name、dataForm.logo、dataForm.descript双向绑定。
<el-form-item label="品牌名" prop="name">
  <el-input v-model="dataForm.name" placeholder="品牌名"></el-input>
</el-form-item>
<el-form-item label="品牌logo地址" prop="logo">
  <single-upload v-model="dataForm.logo"></single-upload>
</el-form-item>
<el-form-item label="介绍" prop="descript">
  <el-input v-model="dataForm.descript" placeholder="介绍"></el-input>
</el-form-item>

校验规则

  • required: true表示该项必填,message的值表示如果不符合规则提示信息。trigger: "blur"失去焦点事件。
dataRule: {
name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
logo: [{ required: true, message: "品牌logo地址不能为空", trigger: "blur" },],
descript: [{ required: true, message: "介绍不能为空", trigger: "blur" },],
showStatus: [{ required: true, message: "显示状态不能为空", trigger: "blur" },],
},

1.2 检索首字母、排序

检索首字母必须是a-z或A-Z,使用正则表达式即可。排序必须是大于等于0的整数

<el-form-item label="检索首字母" prop="firstLetter">
  <el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input>
</el-form-item>
<el-form-item label="排序" prop="sort">
  <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>

校验规则

dataRule: {
firstLetter: [
  { validator:(rule, value, callback)=>{
    if(value ==""){
      callback(new Error('首字母必须填写'));
    }else if(!/^[a-zA-Z]$/.test(value)){
      callback(new Error('首字母必须a-z或者A-Z之间'));
    }else{
      callback();
    }
  }, trigger: "blur" },
], 
sort: [
  { validator:(rule, value, callback)=>{
    if(value==""){
      callback(new Error('排序字段必须填写'));
    }else if(!Number.isInteger(value) || value<0){
      callback(new Error('排序字段必须是一个大于等于0的整数'));
    }else{
      callback();
    }
  }, trigger: "blur" }, 
],
},

1.3 给表单默认值

dataForm: {
  brandId: 0,
  name: "",
  logo: "",
  descript: "",
  showStatus: 1,
  firstLetter: "",
  sort: 0,
},

2. 后端校验

只有前端检验还不行,有可能别人知道你的接口,直接通过postman向后台发送请求。
校验注解都在这个包:javax.validation.constraints

  • 后台使用JSR303规范
  1. 给需要提交给数据库的bean添加注解,并自定义提示信息。
  2. 在controller层标注提交的bean需要进行校验@Valid
  3. 给需要校验的bean加一个BindingResult,就可以获取到校验的结果。

以品牌名为例。

public class BrandEntity implements Serializable {
  //该属性至少有一个非空字符(只有一个空格不允许)
  @NotBlank(message = "品牌名不能为空")
  private String name;
}
@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand){
	brandService.save(brand);
        return R.ok();
    }
}

测试

传入name为空请求,返回400错误。

2.1 自定义接口返回内容

数据提交不符合规范后的返回内容不是我们想要的,可以自定义返回内容。

@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
    if (result.hasErrors()){
        //新建一个集合,用来存放错误数据
        HashMap<String, String> map = new HashMap<>();
        result.getFieldErrors().forEach((item)->{
            //错误属性字段
            String field= item.getField();
            //错误提示信息
            String message = item.getDefaultMessage();
            map.put(field,message);
        });
        return R.error(400,"提交数据不合法").put("data",map);
    }else{
        brandService.save(brand);
    }
    return R.ok();
}

测试

给其他字段添加注解校验

@NotBlank(message = "品牌名不能为空")
private String name;

@NotEmpty
@URL(message = "logo必须是一个合法的URL地址")
private String logo;

//自定义注解,传入一个正则表达式
@Pattern(regexp = "/^[a-zA-Z]$/",message = "检索首字母必须是一个字母")
@NotEmpty
private String firstLetter;
  
@Min(value =0,message = "排序必须是一个大于等于0的整数")
@NotNull
private Integer sort;

测试

3. 统一异常处理

  • 自定义一个异常处理类,用来处理controller层捕获的异常。
  • controller层遇到异常,进行抛出即可。
//处理哪些controller的异常,返回值都是json格式。
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class MyExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e){
        BindingResult result = e.getBindingResult();
        //新建一个集合,用来存放错误数据
        HashMap<String, String> map = new HashMap<>();
        result.getFieldErrors().forEach((item)-> {
            //错误属性字段
            String field = item.getField();
            //错误提示信息
            String message = item.getDefaultMessage();
            map.put(field, message);
        });
        return R.error(400,"提交数据不合法").put("data",map);
    }
}

controller

  • controller层就不用对异常进行处理了。
@RequestMapping("/save")
  //@RequiresPermissions("product:brand:save")
  public R save(@Valid @RequestBody BrandEntity brand){
      brandService.save(brand);
      return R.ok();
  }

系统错误码

错误码和错误信息定义类
1.错误码定义规则为5为数字
2.前两位表示业务场景,最后三位表示错误码。例如:10000。10:通用 000:系统未知异常
3.维护错误码后需要维护错误描述,将他们定义为枚举形式
错误码列表;
  10:通用
    000:系统未知异常
    001:参数格式校验
  11:商品
  12:订单
  13:购物车
  14:物流

3.1 自定义枚举异常类

枚举异常类定义在commom服务中,方便别的服务引用。

public enum BizCodeEnume {
    UNKNOW_EXCEPTION(10000,"系统未知异常"),
    VAILD_EXCEPTION(10001,"参数格式校验失败异常");

    private int code;
    private String msg;
    //枚举构造器
    BizCodeEnume(int code,String msg){
        this.code=code;
        this.msg=msg;
    }

    public int getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
}

3.2 在异常处理类中使用该枚举

@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class MyExceptionControllerAdvice {

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e){
        BindingResult result = e.getBindingResult();
        HashMap<String, String> map = new HashMap<>();
        result.getFieldErrors().forEach((item)-> {
            String field = item.getField();
            String message = item.getDefaultMessage();
            map.put(field, message);
        });
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",map);
    }

    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable e){
        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
    }
}

4. JSR303分组校验功能

需求:新增和修改功能,需要校验的规则不同,比如:

  • id是自增的,新增的时候不需要进行验证,但是修改的时候需要验证。
  • url字段新增的时候必填,修改的时候如果字段不变可以不用填。

1.定义分组接口

//定义一个空接口即可,只是一个标识。
public interface AddGroup {
}

public interface UpdateGroup {
}

2.在bean属性上添加分组

  • 给校验注解标注什么情况需要进行校验。
  • 没有标注分组的注解,在分组校验@Validated({AddGroup.class})情况下不生效。
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@NotNull(message = "修改必须指定id",groups = {UpdateGroup.class})
private Long brandId;

@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String name;

@NotEmpty(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的URL地址",groups ={AddGroup.class,UpdateGroup.class} )
private String logo;
  1. 在controller标注需要校验哪种规则@Validated
@RequestMapping("/save")
public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){
    brandService.save(brand);
    return R.ok();
}

@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
    brandService.updateById(brand);
    return R.ok();
}

5. 自定义检验注解

需求:有些情况正则不能满足我们的需求,需要自定义注解来实现数据校验功能。

  • 比如,showStatus属性 。显示状态[0-不显示;1-显示]
<!--数据校验依赖 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

5.1 编写自定义检验注解

  • 指定注解校验器
  • 自定义错误信息
  • 自定义属性
@Documented
@Constraint(validatedBy = { })   //指定注解校验器
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
//@Repeatable(List.class) 可重复注解
public @interface ListValue {
    //JSR303规定的自定义校验注解必须的三个属性
    String message() default "{com.atguigu.common.valid.ListValue.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    //自定义注解属性
    int[] values() default {};
}

ValidationMessages.properties

com.atguigu.common.valid.ListValue.message=\u5fc5\u987b\u63d0\u4ea4\u6307\u5b9a\u7684\u503c

5.2 编写自定义校验器

  • 判断提交的值是否在注解规定的范围内
public class ListValueConstraintValidator implements ConstraintValidator<ListValue , Integer> {

    private Set<Integer> set =new HashSet<>();
    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] values = constraintAnnotation.values();
        for (int value : values) {
            set.add(value);
        }
    }

    //判断是否校验成功value:需要检验的值
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}

5.3 关联自定义检验注解和自定义校验器

@Constraint(validatedBy = { ListValueConstraintValidator.class})   //指定注解校验器
public @interface ListValue {}

测试

@ListValue(values = {0,1},groups = {AddGroup.class})
private Integer showStatus;

6. 总体测试

新增没问题,修改状态时报错。

Request URL: http://localhost:88/api/product/brand/update

6.1 问题分析

  • 修改显示状态发送update请求,使用的UpdateGroup.class组的验证。
  • 品牌名验证规则是:新增和修改时都必须携带。
  • 如果改为只是新增时需要携带品牌名会出现:绕过前端直接发修改请求,不用携带品牌名也能进行修改。
@RequestMapping("/update")
public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){
    brandService.updateById(brand);
    return R.ok();
}
@NotBlank(message = "品牌名不能为空",groups = {AddGroup.class,UpdateGroup.class})
private String name;

6.2 解决方案

新增一个修改状态的组

public interface UpdateStateGroup {
}

修改showStatus校验规则

@NotNull(groups = {AddGroup.class,UpdateStateGroup.class})
@ListValue(values = {0,1},groups = {AddGroup.class,UpdateStateGroup.class})
private Integer showStatus;

新增controller单独处理修改状态请求

  • 只需要传入id和状态即可。
/**
 * 修改显示状态
 */
@RequestMapping("/update/state")
// @RequiresPermissions("product:brand:update")
public R updateState(@Validated({UpdateStateGroup.class}) @RequestBody BrandEntity brand){
    brandService.updateById(brand);
    return R.ok();
}

前端更改请求路径

updateBrandStatus(data) {
  //结构获取需要的两个值
  let {brandId,showStatus}=data;
  //后台发送修改信息。
  this.$http({
  url: this.$http.adornUrl('/product/brand/update/state'),
  method: 'post',
  data: this.$http.adornData({brandId,showStatus}, false)
  }).then(({ data }) => {
    this.$message({
      type:"success",
      message:"状态更新成功"
    })
   });
},

再次测试,更改状态成功,数据库数据更新成功。


posted @ 2022-01-25 18:01  初夏那片海  阅读(99)  评论(0)    收藏  举报