谷粒商城:09.商品服务 — 品牌管理

品牌数据表对应数据库gulimall-pms中的pms-brand

数据表结构

字段名称字段类型字段长度不是null主键注释
brand_idbigint20品牌id
namechar50品牌名
logovarchar2000品牌logo地址
descriptlongtext介绍
show_statustinyint4显示状态[0-不显示;1-显示]
first_letterchar1检索首字母
sortint11排序

之前的三级分类中,自己设计界面进行增删改查。其实在逆向工程renren-generator生成的代码中,会生成对应数据表的增删改查界面。

1. 新增品牌管理菜单

在这里插入图片描述

2. 搭建基础增删改查界面

使用逆向生成的代码

在这里插入图片描述

将两个vue界面复制到modules/product中

在这里插入图片描述

重启项目,打开品牌维护界面,基础增删改查已经就绪

在这里插入图片描述

由于系统默认会做权限控制,有一些按钮会做权限判断是否显示

在这里插入图片描述

将权限控制设置为永远返回true

在这里插入图片描述

重新查看页面效果

在这里插入图片描述

3. 优化细节—显示状态[0-不显示;1-显示]

  1. 删除[0-不显示;1-显示]

    • 列表brand.vue显示状态

      在这里插入图片描述

    • 新增更新brand-add-or-update.vue显示状态

      在这里插入图片描述

  2. 将显示状态改为用switch组件控制

    • 列表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"
          >
          </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"
        >
        </el-switch>
      </el-form-item>
      
    • 效果

      在这里插入图片描述在这里插入图片描述

  3. 监听Switch组件点击事件

    • 列表brand.vue显示状态

      <el-switch
        v-model="scope.row.showStatus"
        active-color="#13ce66"
        inactive-color="#ff4949"
        @change="updateBrandStatus(scope.row)"
        :active-value="1"
        :inactive-value="0"
      >
      

      直接使用逆向生成代码中的product/brand/update接口来更新数据

      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({
            type: "success",
            message: "状态更新成功",
          });
        });
      },
      

4. 文件上传

在这里插入图片描述

分布式文件上传将所有的文件存储服务在统一位置处理。

4.1 阿里云对象存储OSS

4.1.1. 简介

对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。

4.1.2. 使用步骤

  1. 开通阿里云对象存储服务https://www.aliyun.com/product/oss

    在这里插入图片描述

    在这里插入图片描述

    进入控制台,可以查看API文档

    在这里插入图片描述

    资源术语

    中文英文说明
    存储空间Bucket存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。
    对象/文件Object对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。
    地域Region地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请查看OSS已经开通的Region
    访问域名EndpointEndpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint
    访问密钥AccessKeyAccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。

    推荐:一个项目创建一个Bucket

  2. 创建Bucket

    在这里插入图片描述

    在这里插入图片描述

4.2. 图片上传方式

4.2.1. 方式一:普通上传方式

在这里插入图片描述

这种方式用户上传还要经过自己的应用服务器,额外操作。

4.2.2. 方式一:服务端签名后直传

在这里插入图片描述

4.2.3. 项目采用上传方式

  1. 阿里云存储对象账号密码存储在自己的应用服务器中
  2. 前端向阿里云发送数据的时候,首先向服务器请求Policy上传策略,服务器根据账号密码生成防伪签名(防伪策略,令牌,地址等)
  3. 前端携带防伪签名访问OSS,如果正确接收上传请求。

4.3. 文件上传实现

官方文档

4.3.1. 安装SDK,引入依赖

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

在这里插入图片描述

4.3.2. 测试文件上传

  1. 复制文件上传代码

    在这里插入图片描述

    // yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。
    String endpoint = "yourEndpoint";
    // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    String accessKeyId = "yourAccessKeyId";
    String accessKeySecret = "yourAccessKeySecret";
    
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
    // 创建PutObjectRequest对象。
    // 填写Bucket名称、Object完整路径和本地文件的完整路径。Object完整路径中不能包含Bucket名称。
    // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
    PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket", "exampleobject.txt", new File("D:\\localpath\\examplefile.txt"));
    
    // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
    // ObjectMetadata metadata = new ObjectMetadata();
    // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
    // metadata.setObjectAcl(CannedAccessControlList.Private);
    // putObjectRequest.setMetadata(metadata);
    
    // 上传文件。
    ossClient.putObject(putObjectRequest);
    
    // 关闭OSSClient。
    ossClient.shutdown();            
    
  2. 参数

    endpoint:地域节点

    在这里插入图片描述

    accessKeyId、accessKeySecret:

    • 管理AccessKey

      在这里插入图片描述

    • 使用子用户AccessKey

      在这里插入图片描述

    • 创建用户

      在这里插入图片描述

    • 设置账号

      在这里插入图片描述

    • 开通后生成accessKeyId、accessKeySecret

      在这里插入图片描述

    • 新建账户没有任何权限,添加权限

      在这里插入图片描述

      在这里插入图片描述

  3. 完整代码

    @Test
    public void testUpload() {
        String endpoint = "xxx";
        String accessKeyId = "xxx";
        String accessKeySecret = "xxx";
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
        PutObjectRequest putObjectRequest = new PutObjectRequest("gulimall-kaisarh", "login.png", new File("C:\\Users\\Administrator\\Pictures\\login.png"));
        ossClient.putObject(putObjectRequest);
        ossClient.shutdown();
        System.out.println("上传完成");
    }
    
  4. 测试

    在这里插入图片描述

    在这里插入图片描述

4.4. SpringCloud Alibaba-OSS实现对象存储

4.4.1. 引入SpringCloud Alibaba-OSS

由于很多服务都可能使用文件上传,因此直接在gulimall-common中引入

--引入spring-alibaba-oss-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>

在这里插入图片描述

4.4.2. 配置阿里云oss 相关的账号信息

spring: 
 cloud:
  alicloud: 
   oss:
    endpoint: oss-cn-shanghai.aliyuncs.com
   access-key: xxxxxx
   secret-key: xxxxxx

注意:必须申请 RAM 账号信息,并且分配 OSS 操作权限

在这里插入图片描述

4.4.3 测试使用OssClient 上传

@Autowired
OSSClient ossClient;

@Test
public void testUpload() throws FileNotFoundException {
    InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\removeAll.png");
    ossClient.putObject("gulimall-kaisarh", "removeAll.png", inputStream);
    ossClient.shutdown();
    System.out.println("上传完成");
}

5. 创建微服务,整合第三方功能

5.1. 创建微服务gulimall-third-party

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

5.2. 修改依赖

<dependency>
    <groupId>com.atguigu.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

在这里插入图片描述

将对象存储依赖放到gulimall-third-party

在这里插入图片描述

5.3. 将gulimall-third-parthy添加到nacos中

5.3.1. 新建命名空间

在这里插入图片描述

新建配置oss.yml

在这里插入图片描述

完善配置

在这里插入图片描述

5.3.2. 配置gulimall-third-party配置中心

spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=e5b7c2f9-afd4-4750-94c7-f0be48b97fb8
spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true

在这里插入图片描述

5.3.3. 配置gulimall-third-party注册中心

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-third-party
server:
  port: 30000

在这里插入图片描述

开启服务注册发现

在这里插入图片描述

去除数据源mybatis依赖

<exclusions>
    <exclusion>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </exclusion>
</exclusions>

在这里插入图片描述

5.4. 启动项目

启动项目后可以在Nacos的服务列表查询

在这里插入图片描述

测试上传通过

在这里插入图片描述

6. 服务端签名后直传

6.1. 文档

服务端签名后直传文档

6.2. 流程介绍

时序图

Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。

6.3. 代码

Java代码

  1. gulimall-third-part微服务中创建controller并添加@RestController注解

    在这里插入图片描述

    在这里插入图片描述

  2. 编写获取签名接口

    package com.atguigu.gulimall.thirdparty.controller;
    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.common.utils.BinaryUtil;
    import com.aliyun.oss.model.MatchMode;
    import com.aliyun.oss.model.PolicyConditions;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.nio.charset.StandardCharsets;
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    @RestController
    public class OssController {
    
        @Autowired
        OSS ossClient;
    
        @Value("${spring.cloud.alicloud.oss.endpoint}")
        private String endpoint;
        @Value("${spring.cloud.alicloud.oss.bucket}")
        private String bucket;
        @Value("${spring.cloud.alicloud.access-key}")
        private String accessId;
    
        @RequestMapping("/oss/policy")
        public Map<String, String> policy() {
            String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
            // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
            // String callbackUrl = "http://88.88.88.88:8888";
            String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
            String dir = format + "/"; // 用户上传文件时指定的前缀。
            Map<String, String> respMap = null;
            try {
                long expireTime = 30;
                long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
                Date expiration = new Date(expireEndTime);
                // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
                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(StandardCharsets.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));
                // respMap.put("expire", formatISO8601Date(expiration));
    
            } catch (Exception e) {
                // Assert.fail(e.getMessage());
                System.out.println(e.getMessage());
            } finally {
                ossClient.shutdown();
            }
            return respMap;
        }
    }
    
  3. 启动测试测试接口

    在这里插入图片描述

    • expire:过期时间
    • dir:上传文件名
    • host:上传文件地址
    • policy:签名

6.4. 配置网关

- id: third_part_route
  uri: lb://gulimall-third-party
  predicates:
    ## 前端项目发送请求,带有/api前缀
    - Path=/api/thirdparty/**
  filters:
    - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}

在这里插入图片描述

测试:测试接口

在这里插入图片描述

7. OOS前后端联调上传功能

  1. 文件上传组件el-uploadaction属性对应Bucket域名

    在这里插入图片描述

  2. 更新后端接口,返回R对象

     R policy() {
        ···
        return R.ok().put("data", respMap);
    }
    

    在这里插入图片描述

  3. 配置OSS跨域

    在这里插入图片描述

    在这里插入图片描述

  4. multiUpload.vue

    <template>
      <div>
        <el-upload
          action="http://gulimall-kaisarh.oss-cn-beijing.aliyuncs.com"
          :data="dataObj"
          :list-type="listType"
          :file-list="fileList"
          :before-upload="beforeUpload"
          :on-remove="handleRemove"
          :on-success="handleUploadSuccess"
          :on-preview="handlePreview"
          :limit="maxCount"
          :on-exceed="handleExceed"
          :show-file-list="showFile"
        >
          <i class="el-icon-plus"></i>
        </el-upload>
        <el-dialog :visible.sync="dialogVisible">
          <img width="100%" :src="dialogImageUrl" alt />
        </el-dialog>
      </div>
    </template>
    <script>
    import { policy } from "./policy";
    import { getUUID } from "@/utils";
    export default {
      name: "multiUpload",
      props: {
        //图片属性数组
        value: Array,
        //最大上传图片数量
        maxCount: {
          type: Number,
          default: 30,
        },
        listType: {
          type: String,
          default: "picture-card",
        },
        showFile: {
          type: Boolean,
          default: true,
        },
      },
      data() {
        return {
          dataObj: {
            policy: "",
            signature: "",
            key: "",
            ossaccessKeyId: "",
            dir: "",
            host: "",
            uuid: "",
          },
          dialogVisible: false,
          dialogImageUrl: null,
        };
      },
      computed: {
        fileList() {
          let fileList = [];
          for (let i = 0; i < this.value.length; i++) {
            fileList.push({ url: this.value[i] });
          }
          return fileList;
        },
      },
      mounted() {},
      methods: {
        emitInput(fileList) {
          let value = [];
          for (let i = 0; i < fileList.length; i++) {
            value.push(fileList[i].url);
          }
          this.$emit("input", value);
        },
        handleRemove(file, fileList) {
          this.emitInput(fileList);
        },
        handlePreview(file) {
          this.dialogVisible = true;
          this.dialogImageUrl = file.url;
        },
        beforeUpload(file) {
          let _self = this;
          return new Promise((resolve, reject) => {
            policy()
              .then((response) => {
                console.log("这是什么${filename}");
                _self.dataObj.policy = response.data.policy;
                _self.dataObj.signature = response.data.signature;
                _self.dataObj.ossaccessKeyId = response.data.accessid;
                _self.dataObj.key = response.data.dir + getUUID() + "_${filename}";
                _self.dataObj.dir = response.data.dir;
                _self.dataObj.host = response.data.host;
                resolve(true);
              })
              .catch((err) => {
                console.log("出错了...", err);
                reject(false);
              });
          });
        },
        handleUploadSuccess(res, file) {
          this.fileList.push({
            name: file.name,
            // url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名
            url:
              this.dataObj.host +
              "/" +
              this.dataObj.key.replace("${filename}", file.name),
          });
          this.emitInput(this.fileList);
        },
        handleExceed(files, fileList) {
          this.$message({
            message: "最多只能上传" + this.maxCount + "张图片",
            type: "warning",
            duration: 1000,
          });
        },
      },
    };
    </script>
    <style>
    </style>
    
  5. policy.js

    import http from '@/utils/httpRequest.js'
    export function policy() {
        return new Promise((resolve, reject) => {
            http({
                url: http.adornUrl("/thirdparty/oss/policy"),
                method: "get",
                params: http.adornParams({})
            }).then(({ data }) => {
                resolve(data);
            })
        });
    }
    
  6. singleUpload.vue

    <template>
      <div>
        <el-upload
          action="http://gulimall-kaisarh.oss-cn-beijing.aliyuncs.com"
          :data="dataObj"
          list-type="picture"
          :multiple="false"
          :show-file-list="showFileList"
          :file-list="fileList"
          :before-upload="beforeUpload"
          :on-remove="handleRemove"
          :on-success="handleUploadSuccess"
          :on-preview="handlePreview"
        >
          <el-button size="small" type="primary">点击上传</el-button>
          <div slot="tip" class="el-upload__tip">
            只能上传jpg/png文件,且不超过10MB
          </div>
        </el-upload>
        <el-dialog :visible.sync="dialogVisible">
          <img width="100%" :src="fileList[0].url" alt="" />
        </el-dialog>
      </div>
    </template>
    <script>
    import { policy } from "./policy";
    import { getUUID } from "@/utils";
    export default {
      name: "singleUpload",
      props: {
        value: String,
      },
      computed: {
        imageUrl() {
          return this.value;
        },
        imageName() {
          if (this.value != null && this.value !== "") {
            return this.value.substr(this.value.lastIndexOf("/") + 1);
          } else {
            return null;
          }
        },
        fileList() {
          return [
            {
              name: this.imageName,
              url: this.imageUrl,
            },
          ];
        },
        showFileList: {
          get: function () {
            return (
              this.value !== null && this.value !== "" && this.value !== undefined
            );
          },
          set: function (newValue) {},
        },
      },
      data() {
        return {
          dataObj: {
            policy: "",
            signature: "",
            key: "",
            ossaccessKeyId: "",
            dir: "",
            host: "",
            // callback:'',
          },
          dialogVisible: false,
        };
      },
      methods: {
        emitInput(val) {
          this.$emit("input", val);
        },
        handleRemove(file, fileList) {
          this.emitInput("");
        },
        handlePreview(file) {
          this.dialogVisible = true;
        },
        beforeUpload(file) {
          let _self = this;
          return new Promise((resolve, reject) => {
            policy()
              .then((response) => {
                console.log("响应的数据", response);
                _self.dataObj.policy = response.data.policy;
                _self.dataObj.signature = response.data.signature;
                _self.dataObj.ossaccessKeyId = response.data.accessid;
                _self.dataObj.key = response.data.dir + getUUID() + "_${filename}";
                _self.dataObj.dir = response.data.dir;
                _self.dataObj.host = response.data.host;
                console.log("响应的数据222。。。", _self.dataObj);
                resolve(true);
              })
              .catch((err) => {
                reject(false);
              });
          });
        },
        handleUploadSuccess(res, file) {
          console.log("上传成功...");
          this.showFileList = true;
          this.fileList.pop();
          this.fileList.push({
            name: file.name,
            url:
              this.dataObj.host +
              "/" +
              this.dataObj.key.replace("${filename}", file.name),
          });
          this.emitInput(this.fileList[0].url);
        },
      },
    };
    </script>
    <style>
    </style>
    
  7. brand-add-or-update.vue

    <template>
    	···
        <el-form-item label="品牌logo地址" prop="logo">
            <single-upload v-model="dataForm.logo"></single-upload>
        </el-form-item>
    	···
    </template>
    
    <script>
    import singleUpload from "../../../components/upload/singleUpload.vue";
    export default {
      components: { singleUpload },
      ···
    };
    </script>
    

    在这里插入图片描述

8. 品牌管理功能完善

8.1. 新增品牌界面

  1. 设置switch显示状态开关,激活为0,不激活为1

    <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>
    
  2. 新增数据的时候做校验

    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) => {
            console.log(value);
            console.log(typeof value);
            if (value === "") {
              callback(new Error("排序字段必须填写!"));
            } else if (!Number.isInteger(value) || value < 0) {
              callback(new Error("排序字段必须是大于等于0的整数"));
            } else {
              callback();
            }
          },
          trigger: "blur",
        },
      ],
    },
    

8.2. 品牌列表界面

  1. 设置品牌logo为图片

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

9. JSR303服务端数据校验

  1. 给Bean添加校验注解

    注解可以参考javax\validation\constraints

    在这里插入图片描述

  2. 使用注解@Valid告知SpringMVC进行校验,开启校验功能

    在这里插入图片描述

  3. Postman测试

    效果:校验错误后会有默认响应

    在这里插入图片描述

  4. 错误信息可以通过注解自定义

    在这里插入图片描述

  5. 给校验的bean后紧跟一个BindingResult就可以获取到校验结果,可以自定义错误返回信息

    /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Valid @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);
            return R.ok();
        }
    }
    

    在这里插入图片描述

  6. 给其他字段增加校验注解

    package com.atguigu.gulimall.product.entity;
    
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    
    import java.io.Serializable;
    import java.util.Date;
    
    import lombok.Data;
    import org.hibernate.validator.constraints.URL;
    
    import javax.validation.constraints.*;
    
    /**
     * 品牌
     *
     * @author KaiSarH
     * @email huankai7@163.com
     * @date 2021-04-15 16:20:25
     */
    @Data
    @TableName("pms_brand")
    public class BrandEntity implements Serializable {
        private static final long serialVersionUID = 1L;
    
        /**
         * 品牌id
         */
        @TableId
        private Long brandId;
        /**
         * 品牌名
         */
        @NotBlank(message = "品牌名必须提交")
        private String name;
        /**
         * 品牌logo地址
         */
        @URL(message = "logo必须是一个合法的url地址")
        @NotEmpty
        private String logo;
        /**
         * 介绍
         */
        private String descript;
        /**
         * 显示状态[0-不显示;1-显示]
         */
        private Integer showStatus;
        /**
         * 检索首字母
         */
        @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
        private String firstLetter;
        /**
         * 排序
         */
        @Min(value = 0, message = "排序必须大于等于0")
        private Integer sort;
    }
    

10. 统一异常处理

很多业务都需要进行数据验证,代码大部分都是重复的,可以做统一异常处理

使用SpringMVC提供的@ControllerAdvice

使用步骤:

  1. 抽取异常处理类

    使用@ControllerAdvice注解标识

    使用basePackages标识哪个位置出现异常进行处理

    在这里插入图片描述

  2. 接口使用BindingResult会接收错误,感应异常。

    删除掉后就不再对异常进行处理,而是直接抛出异常。

    GulimallExceptionControllerAdvice的作用就是感应异常,集中处理。

  3. 错误信息以JSON格式返回,需要给类添加@ResponseBody注解

    @ResponseBody注解和@ControllerAdvice(basePackages注解可以合并为@RestControllerAdvice注解

    //@ResponseBody
    //@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")
    @RestControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")
    
  4. 统一处理MethodArgumentNotValidException异常

    package com.atguigu.gulimall.product.exception;
    
    import com.atguigu.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.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 集中处理所有异常
     */
    @Slf4j
    //@ResponseBody
    //@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")
    @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
    public class GulimallExceptionControllerAdvice {
        // 准确匹配某种异常
        @ExceptionHandler(value = MethodArgumentNotValidException.class)
        public R handleVaildException(MethodArgumentNotValidException e) {
            log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass());
            BindingResult bindingResult = e.getBindingResult();
            Map<String, String> errorMap = new HashMap<>();
            bindingResult.getFieldErrors().forEach(fieldError -> {
                errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
            });
            return R.error(400, "数据校验出现问题").put("data", errorMap);
        }
    
        // 无法准确匹配后处理
        @ExceptionHandler(value = Throwable.class)
        public R handleException(Throwable throwable) {
            return R.error();
        }
    }
    

11. 全局状态码枚举类

错误码和错误信息定义类

  • 错误码定义规则为 5 为数字
  • 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
  • 维护错误码后需要维护错误描述,将他们定义为枚举形式

错误码列表:

  • 10: 通用

    001:参数格式校验

  • 11: 商品

  • 12: 订单

  • 13: 购物车

  • 14: 物流

  1. 状态码在很多地方都需要,因此在gulimall-common中定义枚举类BizCodeEnume

    package com.atguigu.common.exception;
    
    /***
     *	错误码和错误信息定义类
     *	1. 错误码定义规则为 5 为数字
     *	2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
     *	3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
     *	错误码列表:
     *	10: 通用
     *	001:参数格式校验
     *	11:  商品
     *	12:  订单
     *	13: 购物车
     *	14: 物流
     */
    public enum BizCodeEnume {
        UNKNOW_EXCEPTION(10000, "系统未知异常"),
        VAILE_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;
        }
    }
    

    在这里插入图片描述

  2. 定义完成后,在接口中不需要再额外写状态码

    原来返回状态码方式:

    return R.error(400, "数据校验出现问题").put("data", errorMap);
    

    在这里插入图片描述

    定义全局状态码枚举类后返回状态码方式:

    return R.error(BizCodeEnume.VAILE_EXCEPTION.getCode(), BizCodeEnume.VAILE_EXCEPTION.getMsg()).put("data", errorMap);
    

    在这里插入图片描述

12. JSR303分组校验

JSR303分组校验可以完成多场景复杂校验。

在新增数据和修改数据的时候,校验的字段可能不同。

例如:

  • 新增品牌的时候,由于ID是自动生成的自增长ID,所以新增的时候不携带ID

  • 修改品牌的时候,需要根据ID进行修改

  • 无论是新增还是修改,品牌名都不能为空

  • 某些字段如logo在新增的时候需要录入必须提交,修改的时候可以不用必须提交

解决:使用JSR303分组校验功能

  1. gulimall-common中定义不同分组,例如新增分组和修改分组。

    在这里插入图片描述

  2. 使用groups属性给校验注解标注什么情况下需要校验。groups为情况数组,可以添加一个或多个。

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

    在这里插入图片描述

  3. Controller中,使用@Validated注解标识接口进行哪一组的校验

    /**
     * 保存
     */
    @RequestMapping("/save")
    public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand/*, BindingResult result*/) {
        brandService.save(brand);
        return R.ok();
    }
    
    /**
     * 修改
     */
    @RequestMapping("/update")
    public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand) {
        brandService.updateById(brand);
        return R.ok();
    }
    

    在这里插入图片描述

  4. 注意

    默认没有指定分组的校验注解,例如下图中@NotEmpty

    在这里插入图片描述

    在分组校验@Validated情况下不会生效,只会在不分组的情况下生效。

    在这里插入图片描述

13. JSR303自定义校验

在一些特殊情况下,例如显示状态[0-不显示 1-显示],没有内置的校验注解使用,需要自己写校验方法。

  • 使用@Pattern注解正则表达式
  • 自定义校验

使用过程:

  1. 编写一个自定义的校验注解

  2. 编写一个自定义的校验器

  3. 关联自定义的校验器和自定义的校验注解

    让校验器校验校验注解标识的字段

13.1. 编写一个自定义的校验注解

希望有一个注解@ListValue(values = {0, 1}),用来规定字段可以使用的值(0和1).

  1. 创建ListValue校验注解

    在这里插入图片描述

  2. 注解必须拥有三个属性

    • message:当校验出错后,错误信息去哪取
    • groups:支持分组校验
    • payload:自定义负载信息
  3. 注解必须有以下原信息数据

    @Documented
    @Constraint(validatedBy = {})
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    
    • Target:注解可以标注在哪些位置
    • Retention:时机,可以在运行时获取到
    • Constraint:注解使用哪个校验器进行校验,可以指定校验器
  4. 导入相关包

    Payload、Constraint依赖validation-api,在pom.xml导入依赖

    在这里插入图片描述

  5. 基础注解ListValue.java

    package com.atguigu.common.valid;
    
    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.*;
    
    @Documented
    @Constraint(validatedBy = {})
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface ListValue {
        String message() default "{javax.validation.constraints.NotEmpty.message}";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    }
    

    定义完成后,可以在BrandEntity.java0中引入使用

    在这里插入图片描述

  6. ListValue.java中指定value数组

    int[] values() default {};
    
  7. 指定错误信息

    • message的默认值改为ListValue全类名.message

      在这里插入图片描述

      String message() default "{com.atguigu.common.valid.ListValue.message}";
      
    • 创建配置文件ValidationMessages.properties

      在这里插入图片描述

    • 在配置文件中配置错误信息

      在这里插入图片描述

13.2. 编写一个自定义的校验器

  1. 创建校验器类文件ListValueConstraintValidator.java

    在这里插入图片描述

  2. ListValueConstraintValidator实现接口ConstraintValidator

    ConstraintValidator接口包含两个泛型,第一个为对应注解,第二个为校验数据类型

  3. 实现ConstraintValidator接口两个方法

    initialize初始化方法 参数constraintAnnotation包含默认合法的值

    isValid校验方法 参数integer为提交过来需要检验的值

  4. 校验器代码

    package com.atguigu.common.valid;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.util.HashSet;
    import java.util.Set;
    
    public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
    
        private Set<Integer> set = new HashSet<>();
    
        // 初始化方法
        @Override
        public void initialize(ListValue constraintAnnotation) {
            // 合法的值
            int[] values = constraintAnnotation.values();
            // 将合法值全部放到set中,便于查找是否存在
            for (int value : values) {
                set.add(value);
            }
        }
    
        // 判断是否校验成功
        @Override
        /*
         *
         * @params value 提交过来需要检验的值
         * @params context 校验的上下文环境信息
         * @return
         */
        public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
            return set.contains(value);
        }
    }
    

    在这里插入图片描述

  5. 注解与代码数据对应关系

    注解使用的时候,会使用values = {0, 1}指定值

    @ListValue(values = {0, 1})
    

    在这里插入图片描述

    这些值对应的就是initialize初始化方法中的合法值。

    isValid校验方法中的值,指的是客户端传递过来需要校验的值。

13.3. 关联校验器和校验注解

在校验注解位置,使用@Constraint注解指定校验器

@Constraint(validatedBy = {ListValueConstraintValidator.class})

可以指定多个校验器,适配不同类型的校验

@Constraint(validatedBy = {A.class,B.class,C.class})

13.4. 完整校验注解代码与校验器

13.4.1. 校验注解

package com.atguigu.common.valid;

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

@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 {
    String message() default "{com.atguigu.common.valid.ListValue.message}";

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

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

    int[] values() default {};
}

13.4.2. 校验器

package com.atguigu.common.valid;

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

public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {

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

    // 初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        // 合法的值
        int[] values = constraintAnnotation.values();
        // 将合法值全部放到set中,便于查找是否存在
        for (int value : values) {
            set.add(value);
        }
    }

    // 判断是否校验成功
    @Override
    /*
     *
     * @params value 提交过来需要检验的值
     * @params context 校验的上下文环境信息
     * @return
     */
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(value);
    }
}

13.4.3. 注解使用

@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 显示状态[0-不显示;1-显示]
     */
    @ListValue(values = {0, 1})
    private Integer showStatus;

}

13.5. 测试

  1. 指定在添加的时候必须携带

    /**
     * 显示状态[0-不显示;1-显示]
     */
    @ListValue(values = {0, 1}, groups = {AddGroup.class})
    private Integer showStatus;
    
  2. save测试

    • 错误测试

      在这里插入图片描述

    • 正确测试

      在这里插入图片描述

14. 将修改品牌状态单独抽取一个方法

  1. 添加修改状态分组

    在这里插入图片描述

  2. 新建接口,指定使用UpdateStatusGroup分组

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

    在这里插入图片描述

  3. BrandEntity中进行配置,在UpdateStatusGroup组只判断showStatus

    /**
     * 显示状态[0-不显示;1-显示]
     */
    @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
    @ListValue(values = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
    private Integer showStatus;
    

    在这里插入图片描述

  4. 修改前端项目更新状态请求

    在这里插入图片描述

    在这里插入图片描述

  5. 测试成功

    在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

KaiSarH

如果觉得文章不错,可以支持下~

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

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

打赏作者

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

抵扣说明:

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

余额充值