谷粒商城八商品服务品牌管理之前后端数据校验

数据校验不仅需要前端,也需要后端,为了防止恶意攻击。

前端数据校验是vue自带的,后端校验我们用JSR303(java规范第303号相关标准),springboot进行了实现

异常处理我们使用springmvc提供的统一异常处理

自定义异常

异常枚举

package com.atlinxi.common.exception;

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

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

    private int code;
    private String message;

    BizCodeEnume(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

自定义异常

package com.atlinxi.gulimall.product.exception;

import com.atlinxi.common.exception.BizCodeEnume;
import com.atlinxi.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

/**
 * 集中处理所有异常
 * <p>
 * 处理basePackages下的异常,该包下抛出的所有异常都会进入到这儿
 *
 * @RestControllerAdvice 是 @ControllerAdvice @ResponseBody 的集合
 * @Slf4j lombok提供的日志功能
 */
@Slf4j
@RestControllerAdvice(basePackages = "com.atlinxi.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {

    // MethodArgumentNotValidException 异常的信息可以从这里获取
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e) {

        BindingResult bindingResult = e.getBindingResult();

        Map<String, String> errorMap = new HashMap<>();

        bindingResult.getFieldErrors().forEach((fieldError) -> {


            String field = fieldError.getField();
            String defaultMessage = fieldError.getDefaultMessage();

            errorMap.put(field, defaultMessage);

        });


        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMessage()).put("data", errorMap);

    }


    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable) {

        log.error("错误:",throwable);

        return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMessage());
    }
}

数据校验

前端

brand-add-or-update.vue

<template>
  <el-dialog
    :title="!dataForm.brandId ? '新增' : '修改'"
    :close-on-click-modal="false"
    :visible.sync="visible"
  >
    <el-form
      :model="dataForm"
      :rules="dataRule"
      ref="dataForm"
      @keyup.enter.native="dataFormSubmit()"
      label-width="140px"
    >
      <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">
        <!-- <singleUpload></singleUpload> -->
        <!-- 也可以使用短横线 -->
        <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>
      <el-form-item label="显示状态" prop="showStatus">
        <el-switch
          v-model="dataForm.showStatus"
          active-color="#13ce66"
          inactive-color="#ff4949"
          :active-value="1"
          :inactive-value="0"
        >
        </el-switch>
      </el-form-item>
      <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>
    </el-form>
    <span slot="footer" class="dialog-footer">
      <el-button @click="visible = false">取消</el-button>
      <el-button type="primary" @click="dataFormSubmit()">确定</el-button>
    </span>
  </el-dialog>
</template>

<script>
// 导入单图片上传
// @ 代表src路径,说是自定义的,但我不知道在哪儿定义的,不关心
// import SingleUpload from "@/components/upload/singleUpload"
// 这个是敲上面的短横线写法,自动导入的
import singleUpload from "../../../components/upload/singleUpload.vue";
export default {
  components: { singleUpload },
  data() {
    return {
      visible: false,
      dataForm: {
        brandId: 0,
        name: "",
        logo: "",
        descript: "",
        showStatus: 1,
        firstLetter: "",
        sort: 0,
      },
      dataRule: {
        name: [{ required: true, message: "品牌名不能为空", trigger: "blur" }],
        logo: [
          { required: true, message: "品牌logo地址不能为空", trigger: "blur" },
        ],
        descript: [
          { required: true, message: "介绍不能为空", trigger: "blur" },
        ],
        showStatus: [
          {
            required: true,
            message: "显示状态[0-不显示;1-显示]不能为空",
            trigger: "blur",
          },
        ],
        firstLetter: [
          {
            // 自定义校验器
            validator: (rule, value, callback) => {
              if (value == "") {
                callback(new Error("首字母必须填写"));
              } else if (!/^[a-zA-Z]+$/.test()) {
                callback(new Error("首字母必须是a-z或者A-Z之间"));
              } else {
                callback();
              }
            },
            trigger: "blur",
          },
        ],
        sort: [
          {
            validator: (rule, value, callback) => {
              
              if (value === "") {
                console.log("排序字段:" + value + (value == ""));
                callback(new Error("排序字段必须填写"));
                // Number.isInteger() 只能验证整数
              } else if (!(Number.isInteger(value) || value == 0)) {
                callback(new Error("排序必须是0或正整数"));
              } else {
                callback();
              }
            },
            trigger: "blur",
          },
        ],
      },
    };
  },
  methods: {
    init(id) {
      this.dataForm.brandId = id || 0;
      this.visible = true;
      this.$nextTick(() => {
        this.$refs["dataForm"].resetFields();
        if (this.dataForm.brandId) {
          this.$http({
            url: this.$http.adornUrl(
              `/product/brand/info/${this.dataForm.brandId}`
            ),
            method: "get",
            params: this.$http.adornParams(),
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.dataForm.name = data.brand.name;
              this.dataForm.logo = data.brand.logo;
              this.dataForm.descript = data.brand.descript;
              this.dataForm.showStatus = data.brand.showStatus;
              this.dataForm.firstLetter = data.brand.firstLetter;
              this.dataForm.sort = data.brand.sort;
            }
          });
        }
      });
    },
    // 表单提交
    dataFormSubmit() {
      this.$refs["dataForm"].validate((valid) => {
        if (valid) {
          this.$http({
            url: this.$http.adornUrl(
              `/product/brand/${!this.dataForm.brandId ? "save" : "update"}`
            ),
            method: "post",
            data: this.$http.adornData({
              brandId: this.dataForm.brandId || undefined,
              name: this.dataForm.name,
              logo: this.dataForm.logo,
              descript: this.dataForm.descript,
              showStatus: this.dataForm.showStatus,
              firstLetter: this.dataForm.firstLetter,
              sort: this.dataForm.sort,
            }),
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.$message({
                message: "操作成功",
                type: "success",
                duration: 1500,
                onClose: () => {
                  this.visible = false;
                  this.$emit("refreshDataList");
                },
              });
            } else {
              this.$message.error(data.msg);
            }
          });
        }
      });
    },
  },
};
</script>

后端

笔记

package com.atlinxi.gulimall.product;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;

/**
 * 1、整合mybatis-plus
 *      1) 导入依赖(实际上我们在common工程中导入了该依赖)
 *      <dependency>
 *             <groupId>com.baomidou</groupId>
 *             <artifactId>mybatis-plus-boot-starter</artifactId>
 *             <version>3.3.1</version>
 *         </dependency>
 *      2)配置
 *          1、配置数据源(导入数据库驱动和在application.yml配置数据源相关信息)
 *          2、使用@MapperScan,告诉mybatis-plus,sql映射文件的位置
 *
 *
 *
 * 2、逻辑删除
 *      1)在配置文件中配置全局的逻辑删除规则
 *
 *
 * 3. JSR303(java规范第303号相关标准)
 *
 *      SpringBoot2.3.0以后版本没有引入javax.validation,需要手动引入对应版本
 *
 *      这些注解都在javax.validation.constraints下
 *
 *      1) springboot提供的,给bean添加校验注解,定义自己的message提示
 *
 *          @Future 字段必须是未来的时间
 *
 *          @Max,@Min,@NotEmpty,@Size,@Email
 *
 *
 *      2)开启校验功能
 *
 *          BindingResult 获取校验的结果
 *
 *          public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
 *
 *          效果,校验错误以后会有默认的响应
 *          (我这儿没有响应,就是postman请求返回的时候不会响应message,但是不影响后续的操作)
 *
 *
 *      3)分组校验(多场景的复杂校验)
 *          例如新增和修改字段的校验要求可能是不一样的,
 *              例如id,新增是不需要的,因为id是自增的,但是修改就必须携带了
 *
 *          1)给校验注解标注什么情况需要进行校验
 *
 *              如果在函数参数中标注了分组,则字段必须标注分组,否则不生效
 *              如果在函数参数中没有标注分组,则字段不能标注分组,标注分组的反而不生效
 *
 *              @NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
 *
 *              public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand ) {


		@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
 	   @Null(message = "新增不能指定id",groups = {AddGroup.class})
  	   @TableId
   	   private Long brandId;

 *
 *       4)自定义校验
 *
 *          1)编写一个自定义的校验注解(定义校验的基本规范)
 *
 *          2)编写一个自定义校验器
 *
 *              判断传入的参数是否符合规范   ConstraintValidator
 *          3)关联自定义的校验器和自定义的校验注解
 *
 *          自定义注解中有这么一个注解
 *
 *          校验注解可以使用多个校验器,
 *          @Constraint(
 *  *         validatedBy = {ListValueConstraintValidator.class,}}
 *  *           )
 *
 *
 *
 * 4. 统一异常处理
 *
 * @ControllerAdvice
 *
 *
 *
 *
 */

@MapperScan("com.atlinxi.gulimall.product.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallProductApplication {

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

}

添加依赖

product模块
       <dependency>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-starter-validation</artifactId>
          </dependency>



common模块
<dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>

BrandEntity

package com.atlinxi.gulimall.product.entity;

import com.atlinxi.common.valid.AddGroup;
import com.atlinxi.common.valid.ListValue;
import com.atlinxi.common.valid.UpdateGroup;
import com.atlinxi.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

/**
 * 品牌
 *
 * @author linxi
 * @email qazokmzjl@gmail.com
 * @date 2022-10-12 23:06:12
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 品牌id
     */
    @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
    @Null(message = "新增不能指定id",groups = {AddGroup.class})
    @TableId
    private Long brandId;
    /**
     * 品牌名
     */
    // @NotEmpty 不为null 或者 ""
    // @NotNull  不为null
    // @NotBlank 必须包含一个非空字符,不能是一个空格
    // message 默认的值在ValidationMessages.properties
    @NotBlank(message = "品牌名必须提交",groups = {UpdateGroup.class,AddGroup.class})
    private String name;
    /**
     * 品牌logo地址
     */
    @URL(message = "logo必须是一个合法的url地址",groups = {AddGroup.class,UpdateGroup.class})
    @NotBlank(groups = {AddGroup.class})
    private String logo;
    /**
     * 介绍
     */
    private String descript;
    /**
     * 显示状态[0-不显示;1-显示]
     *
     * @ListValue   自定义注解,该值只能是0/1
     */
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals = {0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
    /**
     * 检索首字母
     * @Pattern 自定义注解,可以传正则表达式
     *
     * java中指定正则的时候不需要前后两个 / /^[a-zA-Z]$/
     */

    @Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups = {AddGroup.class,UpdateGroup.class})
    @NotEmpty(groups = {AddGroup.class})
    private String firstLetter;
    /**
     * 排序
     */
    @Min(value = 0,message = "排序必须大于等于0")
    @NotNull(groups = {AddGroup.class})
    private Integer sort;

}

BrandController

package com.atlinxi.gulimall.product.controller;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import com.atlinxi.common.valid.AddGroup;
import com.atlinxi.common.valid.UpdateGroup;
import com.atlinxi.common.valid.UpdateStatusGroup;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.atlinxi.gulimall.product.entity.BrandEntity;
import com.atlinxi.gulimall.product.service.BrandService;
import com.atlinxi.common.utils.PageUtils;
import com.atlinxi.common.utils.R;



/**
 * 品牌
 *
 * @author linxi
 * @email qazokmzjl@gmail.com
 * @date 2022-10-12 23:06:12
 */
@RestController
@RequestMapping("product/brand")
public class BrandController {
    @Autowired
    private BrandService brandService;

    /**
     * 列表
     */
    @RequestMapping("/list")
    public R list(@RequestParam Map<String, Object> params) {
        PageUtils page = brandService.queryPage(params);

        return R.ok().put("page", page);
    }


    /**
     * 信息
     */
    @RequestMapping("/info/{brandId}")
    public R info(@PathVariable("brandId") Long brandId) {
        BrandEntity brand = brandService.getById(brandId);

        return R.ok().put("brand", brand);
    }

    /**
     * 保存
     *
     * @Valid 开启校验,不支持分组
     * @Validated 开启校验,支持分组
     */
    @RequestMapping("/save")
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand /*,BindingResult result*/) {
//        if (result.hasErrors()) {
//            Map<String, String> map = new HashMap<>();
//            // 1. 获取校验的错误结果
//            result.getFieldErrors().forEach((item) -> {
//                // FieldError
//                // 校验错误之后的提示消息
//                String defaultMessage = item.getDefaultMessage();
//                // 获取错误的属性的名字
//                String field = item.getField();
//
//                map.put(field, defaultMessage);
//
//
//            });
//            return R.error(400, "提交的数据不合法").put("data", map);
//        } else {
//            brandService.save(brand);
//        }

        brandService.save(brand);

        return R.ok();
    }

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

        return R.ok();
    }


    /**
     * 修改状态
     * @param brand
     * @return
     */
    @RequestMapping("/update/status")
        public R updateStatus(@Validated({UpdateStatusGroup.class})@RequestBody BrandEntity brand) {
        brandService.updateById(brand);

        return R.ok();
    }

    /**
     * 删除
     */
    @RequestMapping("/delete")
    public R delete(@RequestBody Long[] brandIds) {
        brandService.removeByIds(Arrays.asList(brandIds));

        return R.ok();
    }

}

自定义校验

自定义注解

package com.atlinxi.common.valid;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * JSR303 规定校验注解必须有三个属性 message、groups、Payload
 * <p>
 * 校验注解还必须包含以下元注解
 * <p>
 *
 *  指定校验器
 *  @Constraint( validatedBy = {}
 * )
 */
@Documented
@Constraint(
        validatedBy = {ListValueConstraintValidator.class}
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {

    // valid自带的注解是从 ValidationMessages.properties 获取到对应的message
    // 我们也建一个配置文件,配置 com.atlinxi.common.valid.ListValue.message
    String message() default "{com.atlinxi.common.valid.ListValue.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    int[] vals() default {};
}

自定义校验器

package com.atlinxi.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

/**
 * 自定义校验器
 *
 * 第一个泛型指定注解,第二个泛型指定校验数据的类型
 * public interface ConstraintValidator<A extends Annotation, T> {
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();

    /**
     * 初始化
     *
     * 会将字段的详细信息给到这儿
     * @ListValue(vals = {0,1})
     *
     *
     * @param constraintAnnotation
     */
    @Override
    public void initialize(ListValue constraintAnnotation) {

        int[] vals = constraintAnnotation.vals();

        for (int val : vals) {
            set.add(val);
        }

    }


    /**
     *
     * 判断是否校验成功
     *
     * @param value   前端提交的值,也就是需要校验的值
     * @param constraintValidatorContext    上下文环境信息
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {

        return set.contains(value);
    }
}

最终让李国华决心走这一步的是房思琪的自尊心。一个如此精致的小孩是不会说出去的,因为这太脏了。自尊心往往是一根伤人伤己的针,但是在这里,自尊心会缝起她的嘴。

房思琪的初恋乐园
林奕含

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值