谷粒商城笔记+踩坑(4)——商品服务-品牌管理,阿里云云存储+JSR303数字校验+统一异常处理

本文涉及的产品
对象存储 OSS,20GB 3个月
对象存储 OSS,恶意文件检测 1000次 1年
对象存储 OSS,内容安全 1000次 1年
简介: 商品服务-品牌管理、添加“品牌管理”到人人后台管理系统、前端显示状态开关、阿里云云存储实现文件上传、异常处理类、JSR303数字校验、分组校验、自定义校验

 导航:

谷粒商城笔记+踩坑汇总篇

Java笔记汇总:

【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析-CSDN博客

目录

7、商品服务-品牌管理

7.1、添加“品牌管理”到人人后台管理系统

7.1.1、在人人后台管理系统中新增“品牌管理”菜单

7.1.2、人人生成的前端vue文件复制到前端工程

7.1.3、修改权限

7.1.4、测试增删改查基础功能

7.2、新增时显示状态开关(仅前端)

7.3、文件上传功能,阿里云云存储

7.3.1、分布式系统上传文件

7.3.2、开通阿里云OSS对象存储服务,创建新的Bucket

7.3.3、子账户创建和授权,获取Endpoint、AccessKey ID、AccessKey Secret

7.3.4、测试普通上传方式(不建议)

7.3.5、建立第三方服务模块,实现服务端签名后直传

7.3.6、测试上传文件功能

7.3.7、实现服务端签名后直传文件,新建OssController

7.3.8、配置网关路由

7.3.9、启动网关、测试获取oss签名请求

7.3.10、前端联调,实现文件上传功能

7.3.11、在阿里云配置跨域规则

7.3.12、启动测试

7.4、效果优化-显示图片(仅前端)

7.5、前端表单校验

7.6、JSR303数字校验

7.6.1、基本校验实现,@Valid

7.6.2、统一封装错误状态码

7.6.3、商品模块统一封装异常处理类,品牌参数校验异常

7.6.4、分组校验,增改校验分开,@Validated

7.6.5、编写自定义校验,必须提交指定数值

7.6.6、使用自定义校验,showStatus只能是0或1

7.6.8、最终校验的实体类和controller代码

7.6.9、postman测试校验


7、商品服务-品牌管理

7.1、添加“品牌管理”到人人后台管理系统

7.1.1、在人人后台管理系统中新增“品牌管理”菜单

菜单管理->新增菜单

image.gif

7.1.2、人人生成的前端vue文件复制到前端工程

前端代码路径\product\main\resources\src\views\modules\product

image.gif

7.1.3、修改权限

没有新增删除按钮: 修改权限,Ctrl+Shift+F查找isAuth后,

在src/utils/index.js修改isAuth,注释并全部返回为true

image.gif

image.gif

7.1.4、测试增删改查基础功能

运行项目, 可以发现增删改查都是成功的.

注意:现在没有添加表单校验,新增时表单有非法输入会失败,例如状态设为汉字。

http://localhost:8001/#/product-brand

image.gif

7.2、新增时显示状态开关(仅前端)

1、在列表中添加自定义列:中间加<template></template>标签。可以通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据

2、修改开关状态,发送修改请求

3、数据库中showStatus是01,开关默认值是true/false。 所以在开关中设置:active-value="1" 、:inactive-value="0"属性,与数据库同步

代码+注释:

<!--brand.vue中显示状态那一列-->
      <el-table-column
        prop="showStatus"
        header-align="center"
        align="center"
        label="显示状态"
      >
        <template slot-scope="scope">
          <el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1" 
            :inactive-value="0"
            @change="updateBrandStatus(scope.row)" 
          >
          </el-switch>
        </template>
      </el-table-column>
<!--brand-add-or-update.vue中显示状态那一列-->
      <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>

image.gif

//brand.vue中新增方法,用来修改状态
updateBrandStatus(data) {
      let { brandId, showStatus } = data;
      this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        data: this.$http.adornData({ brandId, showStatus }, false),
      }).then(({ data }) => {
        this.$message({
          message: "状态修改成功",
          type: "success",
        });
      });
    },

image.gif

7.3、文件上传功能,阿里云云存储

7.3.1、分布式系统上传文件

单体应用上传:上传文件到服务器,想获取文件时再向服务器发请求获取文件。

分布式系统上传: 因为有多台服务器,为防止负载均衡导致获取文件时没找到对应的服务器,所以使用专门的存读文件服务器,或者云存储

和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上

这里我们选择将图片放置到阿里云上,使用对象存储

帮助文档:

上传策略:服务端签名后直传

image.gif

7.3.2、开通阿里云OSS对象存储服务,创建新的Bucket

尽管开通即可,oss是按量计费,开发阶段不会超过一块钱。

注册、登录、实名认证、开通oss对象存储:

对象存储OSS_云存储服务_企业数据管理_存储-阿里云

点击立即开通

image.gif

点击“管理控制台” :

image.gif

oss基本概念:基本概念-Object-对象-存储-对象存储 OSS-阿里云

存储空间(Bucket)

存储空间是用户用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。存储空间具有各种配置属性,包括地域、访问权限、存储类型等。用户可以根据实际需求,创建不同类型的存储空间来存储不同的数据。

建议一个项目创建一个存储空间。

对象(Object)

对象是OSS存储数据的基本单元,也被称为OSS的文件。和传统的文件系统不同,对象没有文件目录层级结构的关系。对象由元信息(Object Meta),用户数据(Data)和文件名(Key)组成,并且由存储空间内部唯一的Key来标识。对象元信息是一组键值对,表示了对象的一些属性,比如最后修改时间、大小等信息,同时用户也可以在元信息中存储一些自定义的信息。

创建bucket:

image.gif

手动上传任意文件测试:

image.gif

image.gif

image.gif

点击文件详情、 复制url就能直接下载文件:

image.gif

7.3.3、子账户创建和授权,获取EndpointAccessKey IDAccessKey Secret

创建子账户

image.gif

image.gif

image.gif

点击创建用户

image.gif

新建成功后得到AccessKey IDAccessKey Secret

子账户分配权限,管理OSS对象存储服务 image.gif

给oss完全权限:

image.gif

Endpoint。 (gulimall-xmh -> 概览 -> Endpoint(地域节点))

image.gif

7.3.4、测试普通上传方式(不建议)

image.gif

经过服务器不建议

安装-gt-lt-Java-对象存储 OSS-阿里云

product模块导入依赖

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.15.0</version>
</dependency>

image.gif

上传文件流,将下面代码中String都改成自己的信息:

@Test
    public void testOss(){
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "yourAccessKeyId";
        String accessKeySecret = "yourAccessKeySecret";
        // 填写Bucket存储空间名称,例如gulimall-hello。
        String bucketName = "examplebucket";
        // 填写存储对象Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
        String objectName = "exampledir/exampleobject.txt";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "D:\\localpath\\examplefile.txt";
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        try {
            InputStream inputStream = new FileInputStream(filePath);
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, inputStream);
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    }

image.gif

运行,上传成功:

image.gif

复制文件url在浏览器也是可以打开的。

7.3.5、建立第三方服务模块,实现服务端签名后直传

删除前面测试时引入的依赖和测试类,以后导入starter依赖。

image.gif

官方演示示例 :

aliyun-spring-boot/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample at master · alibaba/aliyun-spring-boot · GitHub

  • 新建springboot模块,gulimall-third-party

image.gif

image.gif

  • 第三方模块引入common、oss依赖和alibaba管理依赖
<properties>
        <java.version>11</java.version>
        <spring-cloud.version>2021.0.4</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    <dependency>
      <groupId>com.vince.gulimall</groupId>
      <artifactId>gulimall-common</artifactId>
      <version>0.0.1-SNAPSHOT</version>
    </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2021.0.1.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

image.gif

  • nacos新建命名空间third-party

image.gif

  • 命名空间中新建配置文件oss.yml

image.gif

spring:
  cloud:
    alicloud:
      oss:
        endpoint: xxx
        bucket: xxx
      access-key: xxx
      secret-key: xxx

image.gif

  • 项目新建bootstrap.yml
spring:
  application:
    name: gulimall-third-party
  cloud:
    nacos:
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
        namespace: de0e12ff-8fc4-45a0-bdee-5b5618f4054f
        extension-configs:
          - data-id: oss.yml
            group: DEFAULT_GROUP
            refresh: true

image.gif

坑点:报错没有配置endpoint:

SpringBoot 2.4.x的版本之后,对于bootstrap.properties/bootstrap.yaml配置文件(我们合起来成为Bootstrap配置文件)的支持,需要导入如下的依赖

这里在common中导入:

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
            <version>3.1.4</version>
        </dependency>
image.gif
  • 项目新建application.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-third-party
server:
  port: 30000

image.gif

  • 主启动类注解@EnableDiscoveryClient开启nacos注册
@SpringBootApplication
@EnableDiscoveryClient
public class GulimallThirdPartyApplication {
    public static void main(String[] args) {
        SpringApplication.run(GulimallThirdPartyApplication.class, args);
    }
}

image.gif

7.3.6、测试上传文件功能

在第三方模块的pom里排除mybatisplus依赖:

<dependency>
      <groupId>com.vince.gulimall</groupId>
      <artifactId>gulimall-common</artifactId>
      <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
    </dependency>

image.gif

编写测试类:

注意修改bucket

@SpringBootTest
class GulimallThirdPartyApplicationTest {
    @Autowired
    OSSClient ossClient;
    @Test
    public void testUpload() throws FileNotFoundException {
        //上传文件流。
        InputStream inputStream = new FileInputStream("E:\\SystemDefault\\桌面\\1.jpg");
        ossClient.putObject("改成自己bucketName例如gulimall-xmh", "hahaha1.jpg", inputStream);
        // 关闭OSSClient。
        ossClient.shutdown();
        System.out.println("上传成功.");
    }
}

image.gif

7.3.7、实现服务端签名后直传文件,新建OssController

在第三方模块controller.OssController

代码来源:简单上传-上传-文件-OSS-对象存储 OSS-阿里云

这里主要是从yml里注入oss需要的关键属性,删去了跨域相关的代码,因为跨域我们之前在网关模块的配置类里已经统一配置了跨域规则。

@RestController
public class OssController {
    @Autowired
    OSS ossClient;  //注意不是OssClient
    //从nacos配置的yml里注入阿里云云存储的关键属性
    @Value("${spring.cloud.alicloud.oss.endpoint}")
    String endpoint;
    @Value("${spring.cloud.alicloud.oss.bucket}")
    String bucket;
    @Value("${spring.cloud.alicloud.access-key}")
    String accessId;
    @Value("${spring.cloud.alicloud.secret-key}")
    String accessKey;
    @RequestMapping("/oss/policy")
    public R policy(){
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        // 用户上传文件时指定的前缀
        String dir = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
        Map<String, String> respMap=null;
        try {
            long expireTime = 30;
            long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
            Date expiration = new Date(expireEndTime);
            PolicyConditions policyConds = new PolicyConditions();
            policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
            policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
            String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
            byte[] binaryData = postPolicy.getBytes("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);
            respMap= new LinkedHashMap<String, String>();
            respMap.put("accessid", accessId);
            respMap.put("policy", encodedPolicy);
            respMap.put("signature", postSignature);
            respMap.put("dir", dir);
            respMap.put("host", host);
            respMap.put("expire", String.valueOf(expireEndTime / 1000));
        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally {
            ossClient.shutdown();
        }
        return R.ok().put("data",respMap);
    }
}

image.gif

文件真正访问地址:https://bucket名.endpoint名/文件名

例如:

image.gif

启动访问获取签名:

image.gif

7.3.8、配置网关路由

#        oss等第三方模块路由
        - id: third_party_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
#            http://localhost:88/api/thirdparty/oss/policy--->http://localhost:30000/oss/policy
            - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

image.gif

注意:位置要放到/api的上面

image.gif

7.3.9、启动网关、测试获取oss签名请求

访问http://localhost:88/api/thirdparty/oss/policy测试

image.gif

7.3.10、前端联调,实现文件上传功能

  • 将资料里的upload文件夹cv在/renren-fast-vue/src/components中
  • 修改multiUpload.vue和singeUplad.vue组件中el-upload中的action属性,替换成自己的Bucket域名

image.gif

  • 修改\src\views\modules\productbrand-add-or-update.vue,把单个文件上传组件应用到brand-add-or-update.vue
//在<script>标签中导入组件
import singleUpload from "@/components/upload/singleUpload"
//在export default中声明要用到的组件
  components:{
    singleUpload
  },

image.gif

<!--用新的组件替换原来的输入框-->
    <el-form-item label="品牌logo地址" prop="logo">
        <singleUpload v-model="dataForm.logo"></singleUpload>
      </el-form-item>

image.gif

7.3.11、在阿里云配置跨域规则

因为是直接从前端8001端口转到阿里云的bucket域名,所以会有跨域问题:

image.gif

跨域都是目标地址需要配置规则,所以在阿里云网页配置规则。

进入oss:阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

点击概述,下拉基础设置,配置跨域访问: image.gif

创建新规则

image.gif

image.gif

7.3.12、启动测试

启动人人后台管理模块、网关、product、第三方模块

http://localhost:8001/#/product-brand

点击添加品牌-图片上传,进行测试。

image.gif

测试成功!

image.gif

原图片名c02d81be9ae5405ea13e31e9f5b22375!400x400 (1) .jpeg

7.4、效果优化-显示图片(仅前端)

原效果:

新增品牌后,发现在品牌logo下面显示的是地址。应该显示图片。

image.gif

目标效果:

需要组件:elementUI里的table-自定义模板(自定义显示某一列,用于获取url)和others-Image。

在品牌logo下添加图片标签

<el-table-column
        prop="logo"
        header-align="center"
        align="center"
        label="品牌logo"
      >
        <template slot-scope="scope">
          <img :src="scope.row.logo" style="width: 100px; height: 80px">
        </template>
      </el-table-column>

image.gif

7.5、前端表单校验

  • 首字母只能为a-z或者A-Z的一个字母
  • 排序必须是大于等于0的一个整数

el-form中rules表示校验规则

<!--排序加上.number表示要接受一个数字        -->
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>

image.gif

//首字母校验规则
    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",
          },
        ],

image.gif

7.6、JSR303数字校验

JSR303: Java数据校验规范提案。

jsr,是Java Specification Requests的缩写,意思是Java规范提案,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。

303号规定了数据校验的标准。

7.6.1、基本校验实现,@Valid

  • springboot2.3.0以上需要手动引入依赖,引入到common模块中
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>

image.gif

  • 给实体类添加校验注解,并自定义提示message

product模块

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
  private static final long serialVersionUID = 1L;
  /**
   * 品牌id
   */
  @TableId
//  @NotNull(message = "修改必须有id",groups = {UpdateGroup.class})  //分组校验
//  @Null(message = "新增id必须为null",groups = {AddGroup.class})
  private Long brandId;
  /**
   * 品牌名
   */
  @NotBlank(message = "品牌名必须提交")    //不能是null和""和"  "
  private String name;
  /**
   * 品牌logo地址。非空并且是url
   */
  @NotEmpty    //不能是null和""
  @URL(message = "logo必须是一个合法的url地址")
  private String logo;
  /**
   * 介绍
   */
  private String descript;
  /**
   * 显示状态[0-不显示;1-显示]
   */
  private Integer showStatus;
  /**
   * 检索首字母
   */
  @NotEmpty
  @Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母必须是一个字母")
  private String firstLetter;
  /**
   * 排序
   */
  @NotNull
  @Min(value = 0, message = "排序必须大于等于0")
  private Integer sort;
}

image.gif

@NotEmpty和@NotBlank区别:

@NotEmpty非空,不能是null和""

@NotBlank 非空白,不能是null和""和"  "

  • 需要校验的controller参数添加@Valid注解,并返回提示信息
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@RequestBody @Valid BrandEntity brand){
    brandService.save(brand);
        return R.ok();
    }

image.gif

  • 规范校验错误时返回结果,BindingResult 参数

了解即可,后面用异常类抛出异常,返回R.error()

@RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
        if (result.hasErrors()){
            Map<String, String> map = new HashMap<>();
            //1、获取校验的结果
            result.getFieldErrors().forEach((item)->{
                //获取到错误提示
                String message = item.getDefaultMessage();
                //获取到错误属性的名字
                String field = item.getField();
                map.put(field, message);
            });
            return R.error().put("data", map);
        }else{
            brandService.save(brand);
        }
        return R.ok();
    }

image.gif

启动product和网关,测试:

POST http://localhost:88/api/product/brand/save
Content-Type: application/json
{"name": "aa", "logo": "avc"}

image.gif

测试结果:

image.gif

7.6.2、统一封装错误状态码

  • 正规开发过程中,错误状态码有着严格的定义规则

错误码和错误信息定义类:

1. 错误码定义规则为五位数字

2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常

3. 维护错误码后需要维护错误描述,将他们定义为枚举形式

错误码列表(前两位业务场景):

10: 通用

    如10001:参数格式校验失败

11: 商品

12: 订单

13: 购物车

14: 物流

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码。

在common模块的exception包中新建BizCodeEnum枚举用来存储状态码

//错误码和错误信息定义类:
//        1. 错误码定义规则为五位数字
//        2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
//        3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
//
//
//        错误码列表(前两位业务场景):
//         10: 通用
//             如10001:参数格式校验失败
//         11: 商品
//         12: 订单
//         13: 购物车
//         14: 物流
public enum BizCodeEnum {
    UNKNOW_EXEPTION(10000,"系统未知异常"),
    VALID_EXCEPTION( 10001,"参数格式校验失败");
    private int code;
    private String msg;
    BizCodeEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    public int getCode() {
        return code;
    }
    public String getMsg() {
        return msg;
    }
}

image.gif

7.6.3、商品模块统一封装异常处理类,品牌参数校验异常

  • product模块exception包下新建类GulimallExceptionControllerAdvice,用来集中处理此模块所有异常

为什么不放进common模块?

因为品牌参数校验异常具有个性化,例如状态字段只能是1和0,所以只处理product模块异常,而不是放进common模块检测全局异常。

@RestControllerAdvice(basePackages = "com.xmh.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
    // 处理数据校验异常
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleVaildException(MethodArgumentNotValidException e){
        BindingResult bindingResult = e.getBindingResult();
        Map<String, String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError)->{
            errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data", errorMap);
    }
    //处理全局异常
    @ExceptionHandler(value = Throwable.class)    //Exception和Error继承自Throwable
    public R handleException(Throwable throwable){
        return R.error(BizCodeEnume.UNKNOW_EXEPTION.getCode(), BizCodeEnume.UNKNOW_EXEPTION.getMsg());
    }
}

image.gif

BrandController改回来,以后都用捕获异常

/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {
        brandService.save(brand);
        return R.ok();
    }

image.gif

  • 测试

image.gif

7.6.4、分组校验,增改校验分开,@Validated

场景:新增时id必须为空,修改时id必须非空,这样实体类注解就凌乱了,这时就必须用到分组校验。

注意:

  • 实体类变量注解分组,代表此变量支持这些分组的校验。
  • controller参数对象注解分组,代表这个对象只校验有配置这个分组的成员变量。
  • 在common模块中valid包里新建空接口AddGroup,UpdateGroup用来分组
  • 实体类的id进行分组校验
    Brand实体类:
@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;
  /**
   * 品牌名
   */
  @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
  private String name;
  /**
   * 品牌logo地址
   */
  @NotBlank(groups = {AddGroup.class})
  @URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
  private String logo;
  /**
   * 介绍
   */
  private String descript;
  /**
   * 显示状态[0-不显示;1-显示]。自定义校验
   */
//  @Pattern()
  @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
  private Integer showStatus;
  /**
   * 检索首字母
   */
  @NotEmpty(groups={AddGroup.class})
  @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
  private String firstLetter;
  /**
   * 排序
   */
  @NotNull(groups={AddGroup.class})
  @Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
  private Integer sort;
}
  • image.gif 在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

注意:没有指定分组的校验,默认是不起作用的,所以要给所有属性都指定分组。

  • @Valid改为@Validated注解,值为group接口数组,标记当前校验是哪个组
/**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand) {  // BindingResult result
        brandService.save(brand);
        return R.ok();
    }
    /**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@RequestBody @Validated({UpdateGroup.class}) BrandEntity brand) {
        brandService.updateById(brand);
        return R.ok();
    }
  • image.gif
  • 测试:

http://localhost:88/api/product/brand/save

post请求,{"name": "aaa", "logo": "m","brandId":32}

image.gif

7.6.5、编写自定义校验,必须提交指定数值

此步骤其实是画蛇添足的,因为前端状态字段是状态栏,值只能是0和1

步骤:

  • 编写一个自定义校验注解ListValue
  • 新建配置文件ValidationMessages.properties保存注解信息
  • 编写一个自定义校验器ListValueConstraintValidator
  • 关联自定义的校验器和自定义的校验注解(可以指定多个不同的校验器,适配不同类型的校验)

common模块引入依赖(已完成) :

<dependency>
            <groupId>jakarta.validation</groupId>
            <artifactId>jakarta.validation-api</artifactId>
            <version>2.0.2</version>
        </dependency>

image.gif

自定义校验注解@ListValue:

这里可以ctrl+b参考其他@NotNull等注解编写。

common模块的valid包下:

@Documented
//约束。同一个注解可以指定多个不同的校验器,适配不同类型的校验。这里ListValueConstraintValidator.class是数值校验器
@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 {
//    校验出错后,错误信息去哪取。前缀一般是当前全类名,在ValidationMessages.properties配置文件里设置com.atguigu.common.valid.ListValue.message=必须提交指定的值
    String message() default "{com.vince.common.valid.ListValue.message}";
//    支持分组校验的功能
    Class<?>[] groups() default {};
//    自定义负载信息
    Class<? extends Payload>[] payload() default {};
//    自定义注解里的属性
    int[] vals() default {};
}

image.gif

配置校验注解提示信息:

common模块创建配置文件ValidationMessages.properties:

com.atguigu.common.valid.ListValue.message=必须提交指定的值

image.gif

编写自定义校验器, 数值必须包含在vals范围内

校验器实现ConstraintValidator接口。common模块的valid包下:

//第一个泛型是校验注解,第二个泛型是校验数据类型
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
    private Set<Integer> set=new HashSet<>();
//    初始化方法,ListValue是自定义的注解.
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] value = constraintAnnotation.vals();
        for (int i : value) {
            set.add(i);
        }
    }
    /**
     * 判断是否校验成功
     * @param value 需要校验的值
     * @param context
     * @return
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return  set.contains(value);
    }
}

image.gif

关联校验器和校验注解:

在校验注解的@Constraint的validateBy属性中加入校验器

@Constraint(validatedBy = {ListValueConstraintValidator.class})

image.gif

7.6.6、使用自定义校验,showStatus只能是0或1

common模块的valid包下:

public interface UpdateStatus {
}

image.gif

实体类:

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

image.gif

BrandController

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

image.gif

修改前端路径:

image.gif

image.gif

7.6.8、最终校验的实体类和controller代码

实体类:

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
  private static final long serialVersionUID = 1L;
  /**
   * 品牌id
   */
  @NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class,UpdateStatusGroup.class})
  @Null(message = "新增不能指定id",groups = {AddGroup.class})
  @TableId
  private Long brandId;
  /**
   * 品牌名
   */
  @NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
  private String name;
  /**
   * 品牌logo地址
   */
  @NotBlank(groups = {AddGroup.class,UpdateGroup.class})
  @URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
  private String logo;
  /**
   * 介绍
   */
  private String descript;
  /**
   * 显示状态[0-不显示;1-显示]
   */
  @NotNull(groups = {AddGroup.class,UpdateGroup.class, UpdateStatusGroup.class})
  @ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
  private Integer showStatus;
  /**
   * 检索首字母
   */
  @NotEmpty(groups={AddGroup.class,UpdateGroup.class})
  @Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
  private String firstLetter;
  /**
   * 排序
   */
  @NotNull(groups={AddGroup.class,UpdateGroup.class})
  @Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
  private Integer sort;
}

image.gif

controller:

/**
     * 保存
     */
    @RequestMapping("/save")
    //校验有注解AddGroup的字段,基本每个字段都有,主要是必须id为null
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand) {  // BindingResult result
        brandService.save(brand);
        return R.ok();
    }
    /**
     * 修改
     */
    @RequestMapping("/update")
    //校验有注解UpdateGroup的字段,基本每个字段都有,主要是必须id不为null
    public R update(@RequestBody @Validated({UpdateGroup.class}) BrandEntity brand) {
        brandService.updateById(brand);
        return R.ok();
    }
    /**
     * 仅修改状态
     * @param brand
     * @return
     */
    @RequestMapping("/update/status")
    //校验传来对象的status属性,只能是0或者1。
    public R updateStatus(@RequestBody @Validated({UpdateStatusGroup.class}) BrandEntity brand) {   //只校验状态
        brandService.updateById(brand);
        return R.ok();
    }

image.gif

7.6.9、postman测试校验

增:

http://localhost:88/api/product/brand/save

post

{"name": "aaa", "logo": "m","brandId":32}

image.gif

改:

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

post

{"name": "aaa", "logo": "m"}

image.gif

只改状态 :

http://localhost:88/api/product/brand/update/status

post

{"brandId":1,"showStatus":2}

image.gif


相关实践学习
借助OSS搭建在线教育视频课程分享网站
本教程介绍如何基于云服务器ECS和对象存储OSS,搭建一个在线教育视频课程分享网站。
相关文章
|
6月前
|
安全 中间件 API
跨平台整合:如何在不同系统中使用淘宝商品详情API
使用淘宝商品详情API实现跨平台整合,涉及步骤包括理解平台要求、研究API文档、设计数据模型、开发中间件、确保安全认证、测试调试、遵循法规、UI适配及持续维护。此过程能共享数据,提升效率,增加销售机会,优化顾客体验。注意API调用限制、数据格式及各平台特定需求。
|
6月前
|
JSON 前端开发 JavaScript
从API到界面:如何将淘宝/天猫商品详情返回值转化为用户友好的展示
在当今数字化时代,API(应用程序编程接口)已经成为连接不同服务和数据的桥梁。对于电商平台来说,API更是不可或缺的一部分。本文将以淘宝/天猫为例,探讨如何将API返回的商品详情数据转化为用户友好的展示形式。我们将通过代码示例来揭示这一过程。
|
1天前
|
搜索推荐 数据挖掘 API
探讨拼多多商品 API 接口:运用及收益
拼多多以其独特的商业模式迅速崛起,成为电商领域的重要力量。拼多多商品API接口为开发者和企业提供了一套强大的工具,能够获取丰富的商品信息,包括基本资料、价格详情、库存数据、商品图片、销售属性、销量数据及用户评价等。该接口在电商平台拓展、数据分析、移动应用开发和营销推广等多个领域展现出卓越的应用价值,不仅促进了销售额和利润的增长,还优化了用户体验,积累了宝贵的数据资产,为企业战略决策提供了重要依据。
33 5
|
10天前
|
缓存 负载均衡 API
抖音抖店API请求获取宝贝详情数据、原价、销量、主图等参数可支持高并发调用接入演示
这是一个使用Python编写的示例代码,用于从抖音抖店API获取商品详情,包括原价、销量和主图等信息。示例展示了如何构建请求、处理响应及提取所需数据。针对高并发场景,建议采用缓存、限流、负载均衡、异步处理及代码优化等策略,以提升性能和稳定性。
|
12天前
|
XML JSON Java
淘宝详情接口是如何使用的❓
淘宝详情接口使用步骤简述:首先在淘宝开放平台注册账号并创建应用,待审核通过后获取App Key和App Secret;接着了解接口所需的必填和可选参数;然后通过HTTP请求调用接口,处理返回的JSON数据,提取商品信息;最后确保遵守平台规则,合理使用数据。
|
2月前
|
JSON Java API
谷粒商城笔记+踩坑(7)——新增商品,请求参数转vo类
效果展示、配置、启动会员模块、获取当前分类关联的品牌(不用分页)、获取当前分类下的分组及其关联的属性、新增商品、添加复合配置、限制内存、报错loadbalancer解决
谷粒商城笔记+踩坑(7)——新增商品,请求参数转vo类
|
2月前
|
搜索推荐 安全 API
京东商品详情的 API 探秘与应用
在数字化商业时代,准确详尽的商品信息至关重要。京东作为国内领先电商平台,提供了丰富的商品资源及强大的API接口,涵盖商品查询、订单管理等方面,助力开发者轻松获取商品详情。本文将详细介绍如何使用京东API获取商品信息,并探讨其在数据分析、比价工具及个性化推荐系统中的应用价值。使用时需注意API调用频率、数据时效性及安全性等问题。
|
6月前
|
XML JSON API
如何获取阿里巴巴中国站1688商品详情 API 返回值说明
`1688` 是阿里巴巴旗下的一个 B2B(Business-to-Business)电商平台,专注于为企业提供批发和采购服务。`1688` 商品详情 API 是指从阿里巴巴中国站获取商品详细信息的接口。这个 API 通常需要开发者在阿里巴巴开放平台上注册并获取相应的 API 密钥(AppKey 和 AppSecret)才能使用。
|
6月前
|
供应链 API 开发者
阿里系接口推荐:1688商品详情数据接口
阿里系接口推荐:1688商品详情数据接口
60 10
|
6月前
|
XML JSON API
淘系接口推荐:淘宝宝贝详情数据接口
淘系接口推荐:淘宝宝贝详情数据接口