代码编织梦想

第5章 商品服务-品牌管理

1. 使用逆向工程的代码

1.1 导入代码

  • 新增“”品牌管理“菜单
image-20220223212704620
  • 添加品牌管理html文件

image-20220223211923204

  • 运行项目

image-20220223212852530

  • 测试阶段–去除权限

全局搜索 Ctrl + Shift + F

isAuth

/**
 * 是否有权限
 * @param {*} key
 */
export function isAuth(key) {
    return true;
    // return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
}

关闭ESlint的语法检查,太严格了

image-20220223213725678

image-20220223213819755

1.2 显示状态优化

目的:image-20220223214002955

Table组件:https://element.eleme.cn/#/zh-CN/component/table#table-column-scoped-slot

自定义显示模板:通过 Scoped slot 可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据

   </el-table-column>
      <el-table-column
        prop="showStatus"
        header-align="center"
        align="center"
        label="显示状态"
      >
      <!-- 使用slot-scope自定义显示效果 -->
        <template slot-scope="scope">
          <i class="el-icon-time"></i>
          <span style="margin-left: 10px">{{ scope.row.date }}</span>
        </template>
  • 绑定显示状态
  <!-- 使用slot-scope自定义显示效果 -->
        <template slot-scope="scope">
          <!-- Switch开关:通过scope获取,绑定显示状态-->
          <el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
          >
          </el-switch>
        </template>
  • 优化新增修改的界面

image-20220223215009416

显示状态换位Switch开关

 <el-form-item label="显示状态" prop="showStatus">
      <!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态"></el-input> -->
           <el-switch
            v-model="dataForm.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
          >
          </el-switch>
    </el-form-item>

调整表单字体长度

 <el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="130px">

image-20220223225332060

  • 监听开关事件

change: switch 状态发生变化时的回调函数新状态的值

 <!-- 使用slot-scope自定义显示效果 -->
        <template slot-scope="scope">
          <!-- Switch开关:通过slot-scope绑定显示状态-->
          <el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            @change="updateBrandStatus"
          >
          </el-switch>
        </template>

前端发送请求

 // 监听开关状态 -- 传递整行数据
    updateBrandStatus(data) {
      // console.log("data:", rowData)
      // 只需要发送id和状态 -- 解构
      let { brandId, showStatus } = data;
      // 发送请求修改状态
      this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        data: this.$http.adornData({ brandId: brandId, showStatus: showStatus ? 1 : 0 }, false)
      }).then(({ data }) => {
        this.$message({
          type: "sucess",
          message: "状态修改成功",
        });
      });
    },

状态更新 – 设置激活属性 改为0和1

active-valueswitch 打开时的值boolean / string / numbertrue
inactive-valueswitch 关闭时的值boolean / string / numberfalse
          <!-- Switch开关:通过slot-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>

2022.2.23 网络不好,明天继续

路由写错了,少写了/product

image-20220224081906398

后端

    /**
     * 修改
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:brand:update")
    public R update(@RequestBody BrandEntity brand){
		brandService.updateById(brand);

        return R.ok();
    }

2. 文件上传技术

image-20220224082218064

传统模式:单个文件上传库

多个文件服务器--分布式:上传到分布式文件系统中

2.1 阿里云—对象存储OSS 云存储开启

img

  • 开通服务

image-20220224083446557

  • 上传后访问图片地址

image-20220224083652709

文件上传形式

image-20220224083847435

  • 利用防伪签名

上传的账号信息存储在应用服务器
上传先找应用服务器要一个policy上传策略,生成防伪签名

image-20220224083906752

2.2 OSS整合测试

2.2.1 普通上传

查看帮助文档

  • 在pom中引入依赖包 product
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
</dependency>
  • 上传文件流实例代码
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import java.io.FileInputStream;
import java.io.InputStream;

public class Demo {

    public static void main(String[] args) throws Exception {
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-hangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "yourAccessKeyId";
        String accessKeySecret = "yourAccessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "examplebucket";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        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();
            }
        }
    }
} 
  • 单元测试

EndPoint

image-20220224085508303

访问密钥

RAW访问控制

img

image-20220224085849388

用户登录名称 gulimall@1885413714140910.onaliyun.com
AccessKey ID LTAI5tFgyxgqZfKTDbgVWJbd
AccessKey Secret TYXFVknpv3RhLFcf2Cel3OdspRs6bX

分配权限

image-20220224090146360

代码片段

package com.lif314.gulimall.product;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import com.lif314.gulimall.product.entity.BrandEntity;
import com.lif314.gulimall.product.service.BrandService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;


@SpringBootTest
class GulimallProductApplicationTests {
    @Test
    public void uploadFile(){
        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "https://oss-cn-shanghai.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "LTAI5tFgyxgqZfKTDbgVWJbd";
        String accessKeySecret = "TYXFVknpv3RhLFcf2Cel3OdspRs6bX";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "gulimall-lif314";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "github.png";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
        String filePath= "C:\\Users\\lilinfei\\Pictures\\github.png";

        // 创建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());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
                System.out.println("上传成功---");
            }
        }
    }
}

2.2.2 SpringCloud Alibaba上传 – 普通上传

Spring Cloud AliCloud OSS

文档

  • 引入依赖
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
  • 配置OSS
spring.cloud.alicloud.access-key=你的阿里云AK
spring.cloud.alicloud.secret-key=你的阿里云SK
spring.cloud.alicloud.oss.endpoint=***.aliyuncs.com
  • 引入
@SpringBootApplication
public class OssApplication {

    @Autowired
    private OSS ossClient;

    @RequestMapping("/")
    public String home() {
        ossClient.putObject("bucketName", "fileName", new FileInputStream("/your/local/file/path"));
        return "upload success";
    }

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

}
  • Spring结合Resource
@SpringBootApplication
public class OssApplication {

    @Value("oss://bucketName/fileName")
    private Resource file;

    @GetMapping("/file")
    public String fileResource() {
        try {
            return "get file resource success. content: " + StreamUtils.copyToString(
                file.getInputStream(), Charset.forName(CharEncoding.UTF_8));
        } catch (Exception e) {
            return "get resource fail: " + e.getMessage();
        }
    }

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

}

**使用 **

  • 依赖导入到common中
<!--阿里云OSS-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>
  • 在application.yml中配置
spring:
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx:8848
    alicloud:
      access-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
      secret-key: xxxxxxxxxxxxxxxxxxxxxxxxx
      oss:
        endpoint: oss-cn-shanghai.aliyuncs.com
  • 测试 success
    @Test
    public void uploadFileOssClient() throws FileNotFoundException {
        ossClient.putObject("gulimall-lif314", "test.png", new FileInputStream("C:\\Users\\lilinfei\\Pictures\\github.png"));
        System.out.println("上传成功 ------");
    }

2.2.3 服务器签名直传OSS—配置

image-20220224092524895

创建模块:整合第三方服务,短信、邮箱、OSS等

  • 创建模块

image-20220224133837883

image-20220224134110033

  • 导入common依赖和OSS依赖以及common中的依赖管理
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lif314.gulimall</groupId>
    <artifactId>gulimall-third-party</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gulimall-third-party</name>
    <description>gulimall-third-party</description>
    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>2021.0.1</spring-cloud.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>com.lif314.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
<!--        阿里云OSS-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alicloud-oss</artifactId>
            <version>2.2.0.RELEASE</version>
        </dependency>

<!--        引入依赖管理-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.7.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <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>
    </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>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 注册到注册中心和配置中心 bootstrap.properties

新建third-party命名空间

spring.application.name=gulimall-third-party

spring.cloud.nacos.config.server-addr=xx.xx.xx.xx:8848
spring.cloud.nacos.config.namespace=third-party
  • 抽取对象存储配置

image-20220224135320157

spring.application.name=gulimall-third-party

spring.cloud.nacos.config.server-addr=xx.xx.xx.xx:8848
spring.cloud.nacos.config.namespace=third-party


# 抽取配置
spring.cloud.nacos.config.extension-configs[0].data-id=oss.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
# 动态配置
spring.cloud.nacos.config.extension-configs[0].refresh=true

oss.yml中

spring:
    cloud:
        alicloud:
            access-key: xxxxxxxxxxxxxxxxxxxxxxxxxx
            secret-key: xxxxxxxxxxxxxxxxxxxxxxxxxxx
            oss: 
                endpoint: oss-cn-shanghai.aliyuncs.com
  • 编写配置中心 application.yml
spring:
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx:8848
  application:
    name: gulimall-third-party
    
    
server:
  port: 30000 
  • 排除common中引入的MyBatisPLUS/MySQL
        <dependency>
            <groupId>com.lif314.gulimall</groupId>
            <artifactId>gulimall-common</artifactId>
            <version>1.0-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>com.baomidou</groupId>
                    <artifactId>mybatis-plus-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
  • 启动服务发现
package com.lif314.gulimall.thirdparty;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;


@EnableDiscoveryClient
@SpringBootApplication
public class GulimallThirdPartyApplication {

    public static void main(String[] args) {
        SpringApplication.run(GulimallThirdPartyApplication.class, args);
    }
  • 启动测试

org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘ossClient’ defined in class path resource [com/alibaba/alicloud/context/oss/OssContextAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.aliyun.oss.OSS]: Factory method ‘ossClient’ threw exception; nested exception is java.lang.IllegalArgumentException: Oss endpoint can’t be empty.

版本问题,但在OSS配置放在application.yml(本地)就好了,应该是spring.alicloud的版本与springcloud之间的版本问题

测试问题:SpringBoot 项目中如果没有依赖 spring-cloud-context 的话,是不会读取bootstrap.properties 文件

问题就是没有读到bootstrap.propertites中的配置,所以需要加入依赖,该依赖会启动时优先读取bootstrap配置文件

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
  • 编写测试 Done!!1
package com.lif314.gulimall.thirdparty;

import com.aliyun.oss.OSSClient;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.FileInputStream;
import java.io.FileNotFoundException;

@SpringBootTest
class GulimallThirdPartyApplicationTests {

    @Autowired
    OSSClient ossClient;

    @Test
    public void uploadFileOssClient() throws FileNotFoundException {
        ossClient.putObject("gulimall-lif314", "gulimall-3th.png", new FileInputStream("C:\\Users\\lilinfei\\Pictures\\github.png"));
        System.out.println("上传成功 ------");
    }

}

2.2.4 服务器签名直传OSS—编写逻辑

image-20220224154701320

https://help.aliyun.com/document_detail/31926.html

img

  • 服务端签名直传并设置上传回调 代码实例 https://help.aliyun.com/document_detail/91868.htm?spm=a2c4g.11186623.0.0.16073967awmyCU#concept-ahk-rfz-2fb

签名直传服务响应客户端发送给应用服务器的GET消息

protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String accessId = "<yourAccessKeyId>"; // 请填写您的AccessKeyId。
        String accessKey = "<yourAccessKeySecret>"; // 请填写您的AccessKeySecret。
        String endpoint = "oss-cn-hangzhou.aliyuncs.com"; // 请填写您的 endpoint。
        String bucket = "bucket-name"; // 请填写您的 bucketname 。
        String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
        // callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
        String callbackUrl = "http://88.88.88.88:8888";
        String dir = "user-dir-prefix/"; // 用户上传文件时指定的前缀。

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
        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("utf-8");
            String encodedPolicy = BinaryUtil.toBase64String(binaryData);
            String postSignature = ossClient.calculatePostSignature(postPolicy);

            Map<String, String> 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));

            JSONObject jasonCallback = new JSONObject();
            jasonCallback.put("callbackUrl", callbackUrl);
            jasonCallback.put("callbackBody",
                    "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
            jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
            String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
            respMap.put("callback", base64CallbackBody);

            JSONObject ja1 = JSONObject.fromObject(respMap);
            // System.out.println(ja1.toString());
            response.setHeader("Access-Control-Allow-Origin", "*");
            response.setHeader("Access-Control-Allow-Methods", "GET, POST");
            response(request, response, ja1.toString());

        } catch (Exception e) {
            // Assert.fail(e.getMessage());
            System.out.println(e.getMessage());
        } finally { 
            ossClient.shutdown();
        }
    }
  • 配置相关参数
spring:
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx:8848
    alicloud:
      access-key: LTAI5tFgyxgqZfKTDbgVWJbd
      secret-key: TYXFVknpv3RhLFcf2Cel3OdspRs6bX
      oss:
        endpoint: oss-cn-shanghai.aliyuncs.com
        bucket: gulimall-lif314
  application:
    name: gulimall-third-party

server:
  port: 30000

  • 逻辑代码
package com.lif314.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
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.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> getPolicy() {

        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("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());
        }
        return respMap;
    }
}

image-20220224161315007

2.2.5 服务器签名直传OSS—配置网关

以后在上传文件时的访问路径为“ http://localhost:8888/api/thirdparty/oss/policy”

spring:
  cloud:
    gateway:
      routes:
        # product
        - id: product_route
          uri: lb://gulimall-product
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?<segment>.*),/$\{segment}
        - id: third_party_route
          uri: lb://gulimall-third-party
          predicates:
            - Path=/api/thirdparty/**
          filters:
            - RewritePath=/api/thirdparty/(?<segment>/?.*),/$\{segment}
        # renren-fast
        - id: admin_route
          uri: lb://renren-fast
          predicates: # 什么情况下路由给它
            - Path=/api/** # 默认前端项目都带上api前缀,指定路径断言
          filters:
            - RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}   # 重写路径
#  现在的验证码请求路径为,http://localhost:88/api/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
#  原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

image-20220224162217777

2.2.5 服务器签名直传OSS—前端

Upload组件:

  • mutipleUpload.html
<template>
  <div>
    <el-upload
      action="http://gulimall-clouds.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>
  • singleUnload.html
<template> 
  <div>
    <el-upload
      action="http://gulimall-clouds.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>
  • 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)
        })
    })
}

修改

  • Bucket域名:gulimall-lif314.oss-cn-shanghai.aliyuncs.com

品牌中使用组件

  • brand-add-or-update.html

替换

<el-form-item label="品牌logo地址" prop="logo">
      <single-upload v-model="dataForm.logo"></single-upload>
      <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> -->
    </el-form-item>

导入组件

import singleUpload from "@/components/upload/singleUpload"

export default {
  components: { singleUpload },

修改后端返回

package com.lif314.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import com.lif314.common.utils.R;
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.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 R getPolicy() {

        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("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());
        }

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

修改CORS

image-20220224194704300

  • 测试

image-20220224195018491

image-20220224195043980

3. 表单校验&自定义校验

3.1 前端校验

优化logo显示

  • 添加激活 0 和 1
    <el-form-item label="显示状态" prop="showStatus">
      <!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态"></el-input> -->
           <el-switch
            v-model="dataForm.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
          >
          </el-switch>
    </el-form-item>
  • 品牌logo显示图片

Table自定义显示:

      <template slot-scope="scope">
        <i class="el-icon-time"></i>
        <span style="margin-left: 10px">{{ scope.row.date }}</span>
      </template>

图片显示组件:https://element.eleme.cn/#/zh-CN/component/image

表单校验

https://element.eleme.cn/#/zh-CN/component/form

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

  • 绑定rules
<el-form :model="dataForm" :rules="dataRule" ref="dataForm" @keyup.enter.native="dataFormSubmit()" label-width="130px">
  • 添加校验规则 – 使用校验器

https://github.com/yiminghe/async-validator

  // 校验规则
 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" }],
        firstLetter: [
          { required: true, message: "检索首字母不能为空", trigger: "blur" },
        ],
        sort: [{ required: true, message: "排序不能为空", trigger: "blur" }],
      },

使用自定义校验器

  • 校验搜索符
firstLetter: [
          {
            validator: (rule, value, callback) => {
              // 主要检查value的hi
              if (value == "") {
                callback(new Error("首字母必须填写"));
              } else if (!/^[a-zA-Z]$/.test(value)) {
                callback(new Error("首字母必须在a-z或A-Z之间"));
              } else {
                callback();
              }
            },
            trigger: "blur",
          },
        ],
  • 校验数字
<el-form-item label="排序" prop="sort">
        // 告诉接受一个数字
        <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
      </el-form-item>
 sort: [
          {
            validator: (rule, value, callback) => {
              // 主要检查value的hi
              if (value == null) {
                callback(new Error("排序字段必须填写"));
              } else if (!Number.isInteger(value) || value < 0) {
                callback(new Error("必须填写大于等于0的整数"));
              } else {
                callback();
              }
            },
            trigger: "blur",
          },
        ],

3.2 后端校验 — JSR303

问题引入:填写form时应该有前端校验,后端也应该有校验

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

  • 后端:@NotNull, @Email

步骤

使用校验注解 – Bean

在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c2ctyAhS-1646411493114)(C:/Users/lilinfei/AppData/Roaming/Typora/typora-user-images/image-20220224211738044.png)]

	/**
	 * 品牌名
	 */
	@NotBlank
	private String name;
  • 在controller发送请求时启动校验注解 @Valid
    /**
     * 保存
     */
    @RequestMapping("/save")
   // @RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand){
		brandService.save(brand);

        return R.ok();
    }

响应码是400,意味着校验是失败的

但这种不符合业务规定

  • 自定义校验消息提示
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名不能为空")
	private String name;

符合规范的消息提示

给校验的Bean后紧跟一个BindingResult,就可以获取校验的结果

实例:校验name,不能为空

    /**
     * 保存
     * @param brand 请求体 Post
     * @param validateResult 校验结果
     * @return 统一消息提示
     */
    @RequestMapping("/save")
   // @RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult validateResult){
		if(validateResult.hasErrors()) {
            Map<String, String> map = new HashMap<>();
            // 获取校验的错误结果
            validateResult.getFieldErrors().forEach((item) -> {
                // 获取所有的错误结果
                // 获取@NotBlank中写的message
                String message = item.getDefaultMessage();
                // 获取错误属性的名字
                String field = item.getField();
                map.put(field, message);
            });
            return R.error(400, "提交数据不合法").put("data", map);
        }else{
            brandService.save(brand);
            return R.ok();
        }
    }

实例:校验logo地址

@URL

        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.18.Final</version>
            <scope>compile</scope>
        </dependency>
  • @URL
	/**
	 * 品牌logo地址
	 */
	@URL(message = "logo地址必须是合法的URL")
	private String logo;
  • @Pattern 自定义注解
	/**
	 * 检索首字母
	 */
	@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母")
	private String firstLetter;
  • @Min 数字
	/**
	 * 排序
	 */
	@Min(value = 0, message = "排序必须大于等于0")
	private Integer sort;
  • Postman 测试

image-20220224215548055

4. 统一异常处理 @ControllerAdvice

使用步骤:

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。

  • 抽取异常处理类 exception.GulimallExceptionControllerAdvice
  • 标注注解 @ControllerAdvice
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component   // 组件
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};

    @AliasFor("value")
    String[] basePackages() default {}; // 指明出现异常的包

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

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

    Class<? extends Annotation>[] annotations() default {};
}
  • 所有的Controller不处理异常,只需要将异常抛出去:去除BindingResult

    /**
     * 保存
     * @param brand 请求体 Post
//     * @param validateResult 校验结果
     * @return 统一消息提示
     */
    @RequestMapping("/save")
   // @RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand /*, BindingResult validateResult*/){
//		if(validateResult.hasErrors()) {
//            Map<String, String> map = new HashMap<>();
//            // 获取校验的错误结果
//            validateResult.getFieldErrors().forEach((item) -> {
//                // 获取所有的错误结果
//                // 获取@NotBlank中写的message
//                String message = item.getDefaultMessage();
//                // 获取错误属性的名字
//                String field = item.getField();
                System.out.println(field + ":" + message);
//                map.put(field, message);
//            });
//            return R.error(400, "提交数据不合法").put("data", map);
//        }

        brandService.save(brand);
        return R.ok();

    }
  • 处理异常逻辑

编写数据校验异常处理,使用注解 @ExceptionHandler

    // 数据校验异常处理
    @ExceptionHandler
    public void handleValidException(){

    }

使用value指定处理什么类的异常, Exception.class 处理所有异常

    // 数据校验异常处理
    @ExceptionHandler(value = Exception.class)
    public void handleValidException(){

    }
  • 感知异常 编写参数 Exception
    // 数据校验异常处理
    @ExceptionHandler(value = Exception.class)
    public void handleValidException(Exception exception ){

    }
  • 使用lombok中@Slf4j记录日志
/**
 * 集中处理所有异常
 */
@Slf4j  // 日志记录
@ControllerAdvice(basePackages = "com.lif314.gulimall.product.controller") // 统一处理异常
public class GulimallExceptionControllerAdvice {

    // 数据校验异常处理
    @ExceptionHandler(value = Exception.class)
    public void handleValidException(Exception exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

    }
}
  • 异常处理返回值:如果可以处理页面,可以返回ModelAndView,相当于跳转页面。我们统一向前端传送json,所以使用R,并标注注解@ResponseBody
    // 数据校验异常处理
    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public R handleValidException(Exception exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

        return R.error();
    }

所有的方法都返回json数据,所有直接标注在类上。而@RestControllerAdvice等同于

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
/**
 * 集中处理所有异常
 */
@Slf4j  // 日志记录
//@ControllerAdvice(basePackages = "com.lif314.gulimall.product.controller") // 统一处理异常
//@ResponseBody
@RestControllerAdvice(basePackages = "com.lif314.gulimall.product.controller") // 等同于上面两个
public class GulimallExceptionControllerAdvice {

    // 数据校验异常处理
    @ExceptionHandler(value = Exception.class)
//    @ResponseBody
    public R handleValidException(Exception exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

        return R.error();
    }

}

异常处理逻辑

  • 捕捉异常

image-20220225150649676

  • 精确处理异常
    // 数据校验异常处理 -- 精确处理
    @ExceptionHandler(value = Exception.class)
//    @ResponseBody
    public R handleValidException(MethodArgumentNotValidException exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

        return R.error();
    }
  • 使用BindingResult获取异常并进行处理
    // 数据校验异常处理 -- 精确处理
    @ExceptionHandler(value = Exception.class)
//    @ResponseBody
    public R handleValidException(MethodArgumentNotValidException exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

        // 获取异常具体信息
        Map<String,String> errorMap = new HashMap<>();
        BindingResult bindingResult = exception.getBindingResult();
        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();
    }

在编写业务逻辑时,先将异常跑出去,然后再统一进行处理

  • 业务规范 — 一异常状态码

image-20220225151653715

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

package com.lif314.gulimall.product.exception;

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  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;
    }
}
  • 使用异常状态码
package com.lif314.gulimall.product.exception;

import com.lif314.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.*;

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

/**
 * 集中处理所有异常
 */
@Slf4j  // 日志记录
//@ControllerAdvice(basePackages = "com.lif314.gulimall.product.controller") // 统一处理异常
//@ResponseBody
@RestControllerAdvice(basePackages = "com.lif314.gulimall.product.controller") // 等同于上面两个
public class GulimallExceptionControllerAdvice {

    // 数据校验异常处理 -- 精确处理
    @ExceptionHandler(value = Exception.class)
//    @ResponseBody
    public R handleValidException(MethodArgumentNotValidException exception ){
        log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());

        // 获取异常具体信息
        Map<String,String> errorMap = new HashMap<>();
        BindingResult bindingResult = exception.getBindingResult();
        bindingResult.getFieldErrors().forEach((fieldError) -> {
            errorMap.put(fieldError.getField(), fieldError.getDefaultMessage());
        });
        return R.error(BizCodeEnum.VALID_EXCEPTION.getCode(), BizCodeEnum.VALID_EXCEPTION.getMsg()).put("data", errorMap);
    }

    // 公共处理异常
    @ExceptionHandler(value = Throwable.class)
    public R handleException(Throwable throwable){

        return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(), , BizCodeEnum.UNKNOW_EXEPTION.getMsg());
    }

}

image-20220225152524235

5. JSR303分组校验 & 自定义校验注解: 完成多场景的复杂校验

5.1 JSR303分组校验

分组校验:新增和修改的时候规则不一样,如brandId。

  • 每一个校验注解都可以添加group属性:标注什么情况需要进行校验
public @interface Null {

	String message() default "{javax.validation.constraints.Null.message}";

	Class<?>[] groups() default { }; // 必须是一个接口类型

由于每一个模块都会使用,直接在common中添加 valid.AddGroup 接口只是标识,不需要些什么内容

	/**
	 * 品牌id
	 */
	// 如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带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;
  • 在controller上注解 业务方法参数上使用@Validated,可以指定校验分组 @Validated(AddGroup.class)
    /**
     * 保存
     * @param brand 请求体 Post
//     * @param validateResult 校验结果
     * @return 统一消息提示
     */
    @RequestMapping("/save")
   // @RequiresPermissions("product:brand:save")
	// 只在新增时进行校验
    public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand ){
        brandService.save(brand);
        return R.ok();

    }

问题是一旦加了@Validated,它就只会校验添加groups的字段,所以必须都添加groups属性

分组情况下,校验注解生效问题

默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

package com.lif314.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;

import com.lif314.common.valid.AddGroup;
import com.lif314.common.valid.UpdateGroup;
import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

/**
 * 品牌
 *
 * @author lif314
 * @email lifer314@163.com
 * @date 2022-02-07 22:12:41
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	// 如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带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;
	/**
	 * 品牌logo地址
	 */
	@NotBlank(message = "logo地址不能为空", groups = {AddGroup.class })
	@URL(message = "logo地址必须是合法的URL", groups = {UpdateGroup.class,AddGroup.class })
	@NotEmpty
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotBlank(groups = {AddGroup.class})
	@Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {UpdateGroup.class,AddGroup.class })
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotNull(groups = {AddGroup.class})
	@Min(value = 0, message = "排序必须大于等于0", groups = {UpdateGroup.class,AddGroup.class })
	private Integer sort;

}

5.2 JSR303自定义校验注解

场景:要校验showStatus的01状态,可以用正则,但我们可以利用其他方式解决
复杂场景。比如我们想要下面的场景

/**
	 * 显示状态[0-不显示;1-显示]
	 */
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0,1}, groups = {AddGroup.class, UpdateGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
  • 编写自定义的校验注解

  • 每一注解关联一个校验器,编写自定义的校验器

  • 关联校验器和校验注解

在common引入依赖

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

编写校验注解

  • 注解
package com.lif314.common.valid;



import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.ElementType.TYPE_USE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
/**
 * 指明校验器
 *
 * 自定义校验器
 *
 * 关联校验器
 */
@Constraint(validatedBy = { ListValueConstraintValidator.class})
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {

    /**
     * 一般默认值为全类名:在配置文件中去除配置作为返回的消息
     * 配置文件:搜索ValidationMessages.properties
     *
     * 创建配置文件
     */
    String message() default "{com.lif314.common.valid.ListValue.message}";


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

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

    int[] vals() default {};
}
  • Message默认配置提示
com.lif314.common.valid.ListValue.message=必须提交指定的值
  • 校验器
package com.lif314.common.valid;

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

/**
 * ListValue校验器
 *
 * 必须实现接口  ConstraintValidator<ListValue, Integer>
 *     - 第一个参数是校验的注解
 *
 */
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {

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

    // 初始化方法:获取注解上详细信息 {0,1}
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * 判断是否校验成功
     * @param value 提交的值,判断改值知否在constraintAnnotation.vals();范围中
     * @param context 校验的上下文环境信息
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        return set.contains(value);
    }
}
  • 关联
@Constraint(validatedBy = { ListValueConstraintValidator.class})
  • 使用
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	@ListValue(vals = {0,1 }, message = "显示状态必须是0或1", groups = {AddGroup.class})
	private Integer showStatus;
  • 测试

image-20220225164255703

Error:在单独修改状态时因为前端只传入了id和状态,此时后端会校验update时name不能为空,此时有两种解决办法。

  1. 后端添加新的groups,UpdateStatusGroup以及新的API接口,如/update/status
  2. 在前端传入name字段

这里选择第二种!

// 监听开关状态 -- 传递整行数据
    updateBrandStatus(data) {
      console.log("最新状态:", data);
      // 只需要发送id和状态 -- 解构
      let { brandId,name,showStatus } = data;
      // 发送请求修改状态
      this.$http({
        url: this.$http.adornUrl("/product/brand/update"),
        method: "post",
        data: this.$http.adornData(
          { brandId, name, showStatus: showStatus ? 1 : 0 },
          false
        ),
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "状态修改成功",
        });
      });
    },

6. SPU和SKU

6.1 基本概念

https://www.bilibili.com/video/BV1np4y1C7Yf?p=70&spm_id_from=pageDriver

SPU:standard product unit(标准化产品单元):是商品信息聚合的最小单位,是
一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

如iphoneX是SPU

**SKU:stock keeping unit(库存量单位):**库存进出计量的基本单元,可以是件/盒/
托盘等单位。SKU是对于大型连锁超市DC配送中心物流管理的一个必要的方法。
现在已经被引申为产品统一编号的简称,每种产品对应有唯一的SKU号。

如iphoneX 64G 黑色 是SKU

SPU是类(聚合),SKU是实例(具体)

基本属性[规格参数]与销售属性

同一个SPU拥有的特性叫基本属性。如机身长度,这个是手机共用的属性。而每
款手机的属性值不同

能决定库存量的叫销售属性。如颜色

image-20220225182618483

每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分
类下全部的属性;

  • 属性是以三级分类组织起来的
  • 规格参数中有些是可以提供检索的
  • 规格参数也是基本属性,他们具有自己的分组
  • 属性的分组也是以三级分类组织起来的
  • 属性名确定的,但是值是每一个商品不同来决定的

image-20220225183505272

基于上述概念设计的数据库表

pms数据库下:
pms_attr:属性表
pms_attr_group:分组表,两个相互关联
pms_attr_attrgroup_relation:关联表
pms_product_attr_value:属性值表
pms_sku_info:SKU详细信息
pms_sku_images:SKU图片表
pms_sku_sale_sttr_value:SKU销售属性值表
属性关系-规格参数-销售属性-三级分类 关联关系

表关系

image-20220225184257649

image-20220225184406918

6.2 API-属性分组-前端组件抽取&父子组件交互

  • 目标:现在想要实现点击菜单的左边,能够实现在右边展示数据

image-20220225184701297

  • 商品菜单表 – sys_menus.sql:在admin中执行

image-20220225190319564

  • 添加html模板
{
	"生成html模板": {
		"prefix": "html",
		"body": [
			"<template>",
			"<div></div>",
			"</template>",
			"",
			"<script>",
			"//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
			"//例如:import 《组件名称》 from '《组件路径》';",
			"",
			"export default {",
			"  //import引入的组件需要注入到对象中才能使用",
			"components: {},",
			"props: {},",
			"data() {",
			"//这里存放数据",
			"return {",
			"",
			"};",
			"},",
			"//计算属性 类似于data概念",
			"computed: {},",
			"//监控data中的数据变化",
			"watch: {},",
			"//方法集合",
			"methods: {",
			"",
			"},",
			"//生命周期 - 创建完成(可以访问当前this实例)",
			"created() {",
			"",
			"},",
			"//生命周期 - 挂载完成(可以访问DOM元素)",
			"mounted() {",
			"",
			"},",
			"beforeCreate() {}, //生命周期 - 创建之前",
			"beforeMount() {}, //生命周期 - 挂载之前",
			"beforeUpdate() {}, //生命周期 - 更新之前",
			"updated() {}, //生命周期 - 更新之后",
			"beforeDestroy() {}, //生命周期 - 销毁之前",
			"destroyed() {}, //生命周期 - 销毁完成",
			"activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发",
			"}",
			"</script>",
			"<style scoped>",
			"//@import url($3); 引入公共css类",
			"$4",
			"</style>"
		],
		"description": "生成html模板"
	}
}

6.2.1 属性分组-前端

接口文档:https://easydoc.xyz/s/78237135

在modules下新建common文件夹,这是一些公共的组件, 使用html创建模板

布局:layout

<el-row :gutter="20">
  <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
  <el-col :span="6"><div class="grid-content bg-purple"></div></el-col>
</el-row>
  • 页面效果

image-20220225200338534

父子组件
要实现功能:点击左侧,右侧表格对应内容显示。

父子组件传递数据:category.html点击时,引用它的attgroup.html能感知到, 然后
通知到add-or-update

比如嵌套div,里层div有事件后冒泡到外层div(是指一次点击调用了两个div的点
击函数)

子组件(category)给父组件(attrgroup)传递数据,事件机制;

去element-ui的tree部分找event事件,看node-click()

node-click节点被点击时的回调共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点对应的 Node、节点组件本身。

image-20220225201144686

  • category组件
<template>
  <el-tree
    :data="menus"
    node-key="catId"
    :props="defaultProps"
    ref="menuTree"
    @node-click="nodeclick"
  >
  </el-tree>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  props: {},
  data() {
    //这里存放数据
    return {
      menus: [], // 菜单
      expandedKey: [], // 刷新展开菜单id
      defaultProps: {
        children: "children",
        label: "name", // 显示的标签
      },
    };
  },
  //计算属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    //   获取菜单 -- 发送请求模板
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        // 解构data
        // console.log(data.data)
        this.menus = data.data;
      });
    },
    // 点击回调
    nodeclick(data, node, componet) {
    //   console.log("数据,当前节点,当前组件:", data, node, componet);
    // 向父组件传递事件 事件名(任意) 任意数据
    this.$emit("tree-node-click", data, node, componet)
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getMenus();
  },
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
/* @import url(); 引入公共css类 */
</style>
  • 属性分组 attrgroup.html
<template>
  <el-row :gutter="20">
    <el-col :span="6">
      <!-- 感知子组件 然后散发一个事件 -->
      <category @tree-node-click="treenodeclick"></category>
    </el-col>
    <el-col :span="18">
      <div class="mod-config">
        <el-form
          :inline="true"
          :model="dataForm"
          @keyup.enter.native="getDataList()"
        >
          <el-form-item>
            <el-input
              v-model="dataForm.key"
              placeholder="参数名"
              clearable
            ></el-input>
          </el-form-item>
          <el-form-item>
            <el-button @click="getDataList()">查询</el-button>
            <el-button
              v-if="isAuth('product:attrgroup:save')"
              type="primary"
              @click="addOrUpdateHandle()"
              >新增</el-button
            >
            <el-button
              v-if="isAuth('product:attrgroup:delete')"
              type="danger"
              @click="deleteHandle()"
              :disabled="dataListSelections.length <= 0"
              >批量删除</el-button
            >
          </el-form-item>
        </el-form>
        <el-table
          :data="dataList"
          border
          v-loading="dataListLoading"
          @selection-change="selectionChangeHandle"
          style="width: 100%"
        >
          <el-table-column
            type="selection"
            header-align="center"
            align="center"
            width="50"
          >
          </el-table-column>
          <el-table-column
            prop="attrGroupId"
            header-align="center"
            align="center"
            label="分组id"
          >
          </el-table-column>
          <el-table-column
            prop="attrGroupName"
            header-align="center"
            align="center"
            label="组名"
          >
          </el-table-column>
          <el-table-column
            prop="sort"
            header-align="center"
            align="center"
            label="排序"
          >
          </el-table-column>
          <el-table-column
            prop="descript"
            header-align="center"
            align="center"
            label="描述"
          >
          </el-table-column>
          <el-table-column
            prop="icon"
            header-align="center"
            align="center"
            label="组图标"
          >
          </el-table-column>
          <el-table-column
            prop="catelogId"
            header-align="center"
            align="center"
            label="所属分类id"
          >
          </el-table-column>
          <el-table-column
            fixed="right"
            header-align="center"
            align="center"
            width="150"
            label="操作"
          >
            <template slot-scope="scope">
              <el-button
                type="text"
                size="small"
                @click="addOrUpdateHandle(scope.row.attrGroupId)"
                >修改</el-button
              >
              <el-button
                type="text"
                size="small"
                @click="deleteHandle(scope.row.attrGroupId)"
                >删除</el-button
              >
            </template>
          </el-table-column>
        </el-table>
        <el-pagination
          @size-change="sizeChangeHandle"
          @current-change="currentChangeHandle"
          :current-page="pageIndex"
          :page-sizes="[10, 20, 50, 100]"
          :page-size="pageSize"
          :total="totalPage"
          layout="total, sizes, prev, pager, next, jumper"
        >
        </el-pagination>
        <!-- 弹窗, 新增 / 修改 -->
        <add-or-update
          v-if="addOrUpdateVisible"
          ref="addOrUpdate"
          @refreshDataList="getDataList"
        ></add-or-update>
      </div>
    </el-col>
  </el-row>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
import Category from "../common/category";
import AddOrUpdate from "./attrgroup-add-or-update";

export default {
  //import引入的组件需要注入到对象中才能使用
  components: { Category, AddOrUpdate },
  props: {},
  data() {
    //这里存放数据
    return {
      dataForm: {
        key: "",
      },
      dataList: [],
      pageIndex: 1,
      pageSize: 10,
      totalPage: 0,
      dataListLoading: false,
      dataListSelections: [],
      addOrUpdateVisible: false,
    };
  },
  //计算属性 类似于data概念
  computed: {},
  //监控data中的数据变化
  watch: {},
  //方法集合
  methods: {
    // 感知子组件,节点被迪纳基
    treenodeclick(data, node, componet) {
      console.log("感知子组件:", data, node, componet);
      console.log("被点击节点:", data.catId, data.name);
    },
    // 获取数据列表
    getDataList() {
      this.dataListLoading = true;
      this.$http({
        url: this.$http.adornUrl("/product/attrgroup/list"),
        method: "get",
        params: this.$http.adornParams({
          page: this.pageIndex,
          limit: this.pageSize,
          key: this.dataForm.key,
        }),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.dataList = data.page.list;
          this.totalPage = data.page.totalCount;
        } else {
          this.dataList = [];
          this.totalPage = 0;
        }
        this.dataListLoading = false;
      });
    },
    // 每页数
    sizeChangeHandle(val) {
      this.pageSize = val;
      this.pageIndex = 1;
      this.getDataList();
    },
    // 当前页
    currentChangeHandle(val) {
      this.pageIndex = val;
      this.getDataList();
    },
    // 多选
    selectionChangeHandle(val) {
      this.dataListSelections = val;
    },
    // 新增 / 修改
    addOrUpdateHandle(id) {
      this.addOrUpdateVisible = true;
      this.$nextTick(() => {
        this.$refs.addOrUpdate.init(id);
      });
    },
    // 删除
    deleteHandle(id) {
      var ids = id
        ? [id]
        : this.dataListSelections.map((item) => {
            return item.attrGroupId;
          });
      this.$confirm(
        `确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      ).then(() => {
        this.$http({
          url: this.$http.adornUrl("/product/attrgroup/delete"),
          method: "post",
          data: this.$http.adornData(ids, false),
        }).then(({ data }) => {
          if (data && data.code === 0) {
            this.$message({
              message: "操作成功",
              type: "success",
              duration: 1500,
              onClose: () => {
                this.getDataList();
              },
            });
          } else {
            this.$message.error(data.msg);
          }
        });
      });
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {},
  //生命周期 - 挂载完成(可以访问DOM元素)
  mounted() {},
  beforeCreate() {}, //生命周期 - 创建之前
  beforeMount() {}, //生命周期 - 挂载之前
  beforeUpdate() {}, //生命周期 - 更新之前
  updated() {}, //生命周期 - 更新之后
  beforeDestroy() {}, //生命周期 - 销毁之前
  destroyed() {}, //生命周期 - 销毁完成
  activated() {
    this.getDataList();
  }, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
/* @import url(); 引入公共css类 */
</style>
  • 测试

image-20220225203132295

6.2.2 属性分组-前后端联调

/product/attrgroup/list/{catelogId}

后端添加方法

@PathVariable URL中的路径参数

  • controller
    /**
     * 根据类名id查询属性分组
     */
    @RequestMapping("/list")
    //@RequiresPermissions("product:attrgroup:list")
    public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId") Long catelogId){
//        PageUtils page = attrGroupService.queryPage(params);
        // 分页数据查询
        PageUtils page = attrGroupService.queryPage(params, catelogId);

        return R.ok().put("page", page);
    }
  • 实现 MyBatisPLUS
 @Override
    public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
        //如果没有选中三级分类,则查询指定的数据,id传0
        if(catelogId == 0){
            /**
             * Query里面就有个方法getPage(),传入map,将map解析为mybatis-plus的IPage对象
             * 自定义PageUtils类用于传入IPage对象,得到其中的分页信息
             * AttrGroupServiceImpl extends ServiceImpl,其中ServiceImpl的父类中有方法
             * page(IPage, Wrapper)。对于wrapper而言,没有条件的话就是查询所有
             * queryPage()返回前还会return new PageUtils(page);,把page对象解析好页码信
             * 息,就封装为了响应数据
             */
            IPage<AttrGroupEntity> page = this.page(
                    new Query<AttrGroupEntity>().getPage(params),
                    new QueryWrapper<AttrGroupEntity>()
            );
            // 返回分类数据
            return new PageUtils(page);
        }else{
            /**
             * 按照三级分类数据查询
             *
             * 前端会返回key,作为检索条件:如果key不是空的,则要使用key或者id进行查询
             * select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
             */
            String key = (String) params.get("key");
            QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId);
            if(!StringUtils.isEmpty(key)){
                wrapper.and((obj)->{
                    obj.eq("attr_group_id", key).or().like("attr_group_name", key);
                });
            }

            IPage<AttrGroupEntity> page = this.page(
                    new Query<AttrGroupEntity>().getPage(params)
                    , wrapper);
            return  new PageUtils(page);
        }
    }

前端获取数据与显示

    // 感知子组件,节点被迪纳基
    treenodeclick(data, node, componet) {
      //   console.log("感知子组件:", data, node, componet);
      //   console.log("被点击节点:", data.catId, data.name);
      // 如果当前点击的节点是三级,则进行查询 node.level==3
      if (node.level == 3) {
        this.catId = data.catId;
        // 重新查询
        this.getDataList();
      }
    },
  • 测试

image-20220225211345764

6.2.3 分组新增&级联选择器

image-20220225211843690

https://element.eleme.cn/#/zh-CN/component/cascader#events

只需为 Cascader 的options属性指定选项数组即可渲染出一个级联选择器。通过props.expandTrigger可以定义展开子级菜单的触发方式。

 <el-cascader
    v-model="value"
    :options="options"
    @change="handleChange"></el-cascader>

attrgroup-add-or-update.html

      <el-form-item label="所属分类" prop="catelogId">
        <el-cascader
          v-model="dataForm.catelogId"
          :options="categories"
          :props="props"
        ></el-cascader>
        <!-- <el-input
          v-model="dataForm.catelogId"
          placeholder="所属分类id"
        ></el-input> -->
      </el-form-item>
  • 绑定数据并获得
 // 获取表单数据
      getCategories(){
           this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        // 解构data
        // console.log(data.data)
        this.categories = data.data;
      });
  • 在组件创建时获取
  created(){
      this.getCategories();
  }
  • 定义显示props
value指定选项的值为选项对象的某个属性值string‘value’
label指定选项标签为选项对象的某个属性值string‘label’
children指定选项的子选项为选项对象的某个属性值string‘children’
// 三级分类数组
      categories: [],
      props: {
          value: "CatId",
          label: "name", 
          children: "children"
      },

问题:会显示空集合

image-20220225213050641

解决方法:让后端,当children为空时就不要返回

// 使用@JsonInclude() 设置字段

	/**
	 * 子分类 
	 */
	@JsonInclude(JsonInclude.Include.NON_EMPTY) // 不为空才返回
	@TableField(exist = false) // 数据表中不存在
	private List<CategoryEntity> children;

这样提交存在bug,表单里拿到的时每一个菜单节点的id数组,我们只需要发送最后一个

image-20220225214153906

  • 修改中的回显问题

image-20220225215315254

6.2.4 分组修改&级联选择器修改

  • 添加字段
	/**
	 * 菜单路径,用于修改时回显
	 */
	@TableField(exist = false) // 数据库不存在
	private Long[] catelogPath;
  • 递归获取菜单路径
 // [2,25,225]
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        // 逆序转换
        Collections.reverse(paths);
        return parentPath.toArray(new Long[parentPath.size()]);
    }

    // 递归查询并收集路径信息 225,25,2
    private List<Long> findParentPath(Long catelogId, List<Long> paths ){
        // 获取当前分类的id
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        // 如果存在父分类,则需要递归查询
        if(byId.getParentCid()!=0){
            //递归查找父节点
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;
    }
  • 前端回显
this.dataForm.catelogPath = data.attrGroup.catelogPath;

image-20220226101245307

细化

  • 在新增时清除回显数据;

监听修改对话框,一但对话框关闭,则清空数据

  <el-dialog
    :title="!dataForm.attrGroupId ? '新增' : '修改'"
    :close-on-click-modal="false"
    :visible.sync="visible"
    @closed="dialogClose"
  >
      

   // 数据清空
    dialogClose() {
      this.dataForm.catelogPath = [];
    },

image-20220226101929702

  • 并提供快速搜索的功能–

https://element.eleme.cn/#/zh-CN/component/cascader#events

filterable赋值为true即可打开搜索功能,默认会匹配节点的label或所有父节点的label(由show-all-levels决定)中包含输入值的选项。你也可以用filter-method自定义搜索逻辑,接受一个函数,第一个参数是节点node,第二个参数是搜索关键词keyword,通过返回布尔值表示是否命中。

   <el-form-item label="所属分类" prop="catelogId">
        <el-cascader
          v-model="dataForm.catelogPath"
           placeholder="试试搜索:手机"
          :options="categories"
          :props="props"
          filterable
        ></el-cascader>
        <!-- <el-input
          v-model="dataForm.catelogId"
          placeholder="所属分类id"
        ></el-input> -->
      </el-form-item>

image-20220226102241513

6.2.5 品牌分类关联与级联更新

image-20220226102842253

统计错误是因为引入了MyBatisPLUS,需要使用分页插件

https://baomidou.com/pages/97710a/#paginationinnerinterceptor

@Configuration
@MapperScan("scan.your.mapper.package")
public class MybatisPlusConfig {

    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }

    @Bean
    public ConfigurationCustomizer configurationCustomizer() {
        return configuration -> configuration.setUseDeprecatedExecutor(false);
    }
}

配置分页插件

package com.lif314.gulimall.product.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement // 开启事务功能
@MapperScan("com.lif314.gulimall.product.dao")  // 包扫描 Mapper接口
public class MyBatisConfig {

    // 引入分页插件
    /**
     * 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置
     * MybatisConfiguration#useDeprecatedExecutor = false
     * 避免缓存出现问题(该属性会在旧插件移除后一同移除)
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }

    // 新版不需要配置
//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        ConfigurationCustomizer configurationCustomizer = configuration -> configuration.setUseDeprecatedExecutor(false);
//        return configurationCustomizer;
//    }

}

image-20220226104619579

模糊查询

给查询添加条件

package com.lif314.gulimall.product.service.impl;

import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import java.util.Map;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.lif314.common.utils.PageUtils;
import com.lif314.common.utils.Query;

import com.lif314.gulimall.product.dao.BrandDao;
import com.lif314.gulimall.product.entity.BrandEntity;
import com.lif314.gulimall.product.service.BrandService;


@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {

    @Override
    public PageUtils queryPage(Map<String, Object> params) {

        // 获取key id或者名字
        String key = (String) params.get("key");
        // 封装查询条件
       QueryWrapper<BrandEntity> wrapper =  new QueryWrapper<BrandEntity>();
        if(!StringUtils.isEmpty(key)){
            wrapper.eq("brand_id",key).or().like("name", key);
        }
        // 加入查询条件
        IPage<BrandEntity> page = this.page(
                new Query<BrandEntity>().getPage(params),
                wrapper
        );

        return new PageUtils(page);
    }

}

image-20220226105416424

关联分类

image-20220226110913548

  • 前端添加关联分类

/common/category-cascader.html组件

<template>
  <!-- 
使用说明:
1)、引入category-cascader.html
2)、语法:<category-cascader :catelogPath.sync="catelogPath"></category-cascader>
    解释:
      catelogPath:指定的值是cascader初始化需要显示的值,应该和父组件的catelogPath绑定;
          由于有sync修饰符,所以cascader路径变化以后自动会修改父的catelogPath,这是结合子组件this.$emit("update:catelogPath",v);做的
      -->
  <div>
    <el-cascader
      filterable
      clearable
      placeholder="试试搜索:手机"
      v-model="paths"
      :options="categorys"
      :props="setting"
    ></el-cascader>
  </div>
</template>

<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';

export default {
  //import引入的组件需要注入到对象中才能使用
  components: {},
  //接受父组件传来的值
  props: {
    catelogPath: {
      type: Array,
      default() {
        return [];
      },
    },
  },
  data() {
    //这里存放数据
    return {
      setting: {
        value: "catId",
        label: "name",
        children: "children",
      },
      categorys: [],
      paths: this.catelogPath,
    };
  },
  watch: {
    catelogPath(v) {
      this.paths = this.catelogPath;
    },
    paths(v) {
      this.$emit("update:catelogPath", v);
      //还可以使用pubsub-js进行传值
      this.PubSub.publish("catPath", v);
    },
  },
  //方法集合
  methods: {
    getCategorys() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        this.categorys = data.data;
      });
    },
  },
  //生命周期 - 创建完成(可以访问当前this实例)
  created() {
    this.getCategorys();
  },
};
</script>
<style scoped>
</style>

/product/brand.html

引入button

  <el-button
            type="text"
            size="small"
            @click="updateCatelogHandle(scope.row.brandId)"
            >关联</el-button
          >

在div内加入对话框

<!-- 关联分类对话框 -->
    <el-dialog
      title="关联分类"
      :visible.sync="cateRelationDialogVisible"
      width="30%"
    >
      <el-popover placement="right-end" v-model="popCatelogSelectVisible">
        <category-cascader :catelogPath.sync="catelogPath"></category-cascader>
        <div style="text-align: right; margin: 0">
          <el-button
            size="mini"
            type="text"
            @click="popCatelogSelectVisible = false"
            >取消</el-button
          >
          <el-button type="primary" size="mini" @click="addCatelogSelect"
            >确定</el-button
          >
        </div>
        <el-button slot="reference">新增关联</el-button>
      </el-popover>
      <el-table :data="cateRelationTableData" style="width: 100%">
        <el-table-column prop="id" label="#"></el-table-column>
        <el-table-column prop="brandName" label="品牌名"></el-table-column>
        <el-table-column prop="catelogName" label="分类名"></el-table-column>
        <el-table-column
          fixed="right"
          header-align="center"
          align="center"
          label="操作"
        >
          <template slot-scope="scope">
            <el-button
              type="text"
              size="small"
              @click="deleteCateRelationHandle(scope.row.id, scope.row.brandId)"
              >移除</el-button
            >
          </template>
        </el-table-column>
      </el-table>
      <span slot="footer" class="dialog-footer">
        <el-button @click="cateRelationDialogVisible = false">取 消</el-button>
        <el-button type="primary" @click="cateRelationDialogVisible = false"
          >确 定</el-button
        >
      </span>
    </el-dialog>

绑定数据

 brandId: 0,
      catelogPath: [],
      dataList: [],
      cateRelationTableData: [],
      pageIndex: 1,
      pageSize: 10,
      totalPage: 0,
      dataListLoading: false,
      dataListSelections: [],
      addOrUpdateVisible: false,
      cateRelationDialogVisible: false,
      popCatelogSelectVisible: false

方法

// 添加关联分类表
    addCatelogSelect() {
      //{"brandId":1,"catelogId":2}
      this.popCatelogSelectVisible =false;
      this.$http({
        url: this.$http.adornUrl("/product/categorybrandrelation/save"),
        method: "post",
        data: this.$http.adornData({brandId:this.brandId,catelogId:this.catelogPath[this.catelogPath.length-1]}, false)
      }).then(({ data }) => {
        this.getCateRelation();
      });
    },
    // 移除关联分类表
    deleteCateRelationHandle(id, brandId) {
      this.$http({
        url: this.$http.adornUrl("/product/categorybrandrelation/delete"),
        method: "post",
        data: this.$http.adornData([id], false)
      }).then(({ data }) => {
        this.getCateRelation();
      });
    },
    // 添加关联分类
    updateCatelogHandle(brandId) {
      this.cateRelationDialogVisible = true;
      this.brandId = brandId;
      this.getCateRelation();
    },
    getCateRelation() {
      this.$http({
        url: this.$http.adornUrl("/product/categorybrandrelation/catelog/list"),
        method: "get",
        params: this.$http.adornParams({
          brandId: this.brandId
        })
      }).then(({ data }) => {
        this.cateRelationTableData = data.data;
      });
    },
  • 后端-查询关联分类

/product/categorybrandrelation/catelog/list


    /**
     * 获取品牌关联的分类
     *
     * @param brandId 品牌id
     */
//    @RequestMapping(value = "/catelog/list", method = RequestMethod.GET)
    @GetMapping("/catelog/list")
    //@RequiresPermissions("product:categorybrandrelation:list")
    public R catelogList(@RequestParam Long brandId){
        /**
         * {
         * 	"msg": "success",
         * 	"code": 0,
         * 	"data": [{
         * 		"catelogId": 0,
         * 		"catelogName": "string",
         *        }]
         * }
         */
        // 使用list查询,传入查询条件
        List<CategoryBrandRelationEntity> catelogList = categoryBrandRelationService.list(
                new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId)
        );
        return R.ok().put("data", catelogList);
    }
  • 后端-新增关联关系
 /**
     * 新增品牌与分类关联关系
     *
     * 参数:{"brandId":1,"catelogId":2}
     */
    @PostMapping("/save")
    //@RequiresPermissions("product:categorybrandrelation:save")
    public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation){
        categoryBrandRelationService.saveDetail(categoryBrandRelation);

        return R.ok();
    }
/**
     * 保存品牌分类关联的完整信息
     */
    @Override
    public void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {
        Long brandId = categoryBrandRelation.getBrandId();
        Long catelogId = categoryBrandRelation.getCatelogId();
        // 查询品牌名和分类名
        BrandEntity brandEntity = brandDao.selectById(brandId);
        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
        categoryBrandRelation.setBrandName(brandEntity.getName());
        categoryBrandRelation.setCatelogName(categoryEntity.getName());
        // 保存
        this.save(categoryBrandRelation);
    }

image-20220226134241970

级联更新

image-20220226134556271

如:品牌name更新时

  • 品牌级联更新

从update开始

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

        return R.ok();
    }

实现方法

   /**
     * 级联更新
     */
    @Override
    public void updateDetail(BrandEntity brand) {

        // 保证冗余字段数据的一致性
        this.updateById(brand);
        if(!StringUtils.isEmpty(brand.getName())){
            // 同步更新其它关联表中的数据
            categoryBrandRelationService.updateBrandName(brand.getBrandId(), brand.getName());

            // TODO 更新其它关联表
        }
    }

关联实现方法

    /**
     * 级联更新
     */
    @Override
    public void updateBrandName(Long brandId, String name) {
        CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
        categoryBrandRelationEntity.setBrandId(brandId);
        categoryBrandRelationEntity.setBrandName(name);
        // 更新与更新条件
        this.update(categoryBrandRelationEntity, new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
    }

测试

image-20220226140142224

  • 类名级联更新

controller更新

    /**
     * 修改
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:category:update")
    public R update(@RequestBody CategoryEntity category){
//		categoryService.updateById(category);
        // 级联更新
		categoryService.updateCascade(category);
        return R.ok();
    }

实现方法

    /**
     * 级联更新
     */
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);

        //级联更新
        categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());
    }

更新name

    @Override
    public void updateCategoryName(Long catId, String name) {
        this.baseMapper.updateCategory(catId, name);
    }

使用MyBatisPLUS-- Dao

package com.lif314.gulimall.product.dao;

import com.lif314.gulimall.product.entity.CategoryBrandRelationEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 品牌分类关联
 *
 * @author lif314
 * @email lifer314@163.com
 * @date 2022-02-07 22:12:40
 */
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

    void updateCategory(@Param("catId") Long catId,@Param("name") String name);
}

SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.lif314.gulimall.product.dao.CategoryBrandRelationDao">

	<!-- 可根据自己的需求,是否要使用 -->
    <resultMap type="com.lif314.gulimall.product.entity.CategoryBrandRelationEntity" id="categoryBrandRelationMap">
        <result property="id" column="id"/>
        <result property="brandId" column="brand_id"/>
        <result property="catelogId" column="catelog_id"/>
        <result property="brandName" column="brand_name"/>
        <result property="catelogName" column="catelog_name"/>
    </resultMap>
    <update id="updateCategory">
        UPDATE `pms_category_brand_relation` SET catelog_name=#{name} WHERE catelog_id=#{catId}
    </update>
</mapper>

测试

image-20220226142735871

这种情况是 事务

需要在config上开启事务注解,才能使用

package com.lif314.gulimall.product.config;

import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement // 开启事务功能
@MapperScan("com.lif314.gulimall.product.dao")  // 包扫描 Mapper接口
public class MyBatisConfig {
	...
}

然后在Service实现的方法上添加注解 @Transactional

    /**
     * 级联更新
     */
    @Transactional
    @Override
    public void updateBrandName(Long brandId, String name) {
        CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
        categoryBrandRelationEntity.setBrandId(brandId);
        categoryBrandRelationEntity.setBrandName(name);
        // 更新与更新条件
        this.update(categoryBrandRelationEntity, new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
    }

    @Transactional
    @Override
    public void updateCategoryName(Long catId, String name) {
        this.baseMapper.updateCategory(catId, name);
    }
     // 更新与更新条件
       this.update(categoryBrandRelationEntity, new > UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
   }

测试

image-20220226140142224

  • 类名级联更新 ---- controller更新
    /**
     * 修改
     */
    @RequestMapping("/update")
    //@RequiresPermissions("product:category:update")
    public R update(@RequestBody CategoryEntity category){
//		categoryService.updateById(category);
        // 级联更新
		categoryService.updateCascade(category);
        return R.ok();
    }

实现方法

    /**
     * 级联更新
     */
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);

        //级联更新
        categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());
    }

更新name

    @Override
    public void updateCategoryName(Long catId, String name) {
        this.baseMapper.updateCategory(catId, name);
    }

使用MyBatisPLUS-- Dao

package com.lif314.gulimall.product.dao;

import com.lif314.gulimall.product.entity.CategoryBrandRelationEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

/**
 * 品牌分类关联
 *
 * @author lif314
 * @email lifer314@163.com
 * @date 2022-02-07 22:12:40
 */
@Mapper
public interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {

    void updateCategory(@Param("catId") Long catId,@Param("name") String name);
}

SQL语句

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.lif314.gulimall.product.dao.CategoryBrandRelationDao">

	<!-- 可根据自己的需求,是否要使用 -->
    <resultMap type="com.lif314.gulimall.product.entity.CategoryBrandRelationEntity" id="categoryBrandRelationMap">
        <result property="id" column="id"/>
        <result property="brandId" column="brand_id"/>
        <result property="catelogId" column="catelog_id"/>
        <result property="brandName" column="brand_name"/>
        <result property="catelogName" column="catelog_name"/>
    </resultMap>
    <update id="updateCategory">
        UPDATE `pms_category_brand_relation` SET catelog_name=#{name} WHERE catelog_id=#{catId}
    </update>
</mapper>

测试

image-20220226142735871

这种情况是 事务

需要在config上开启事务注解,才能使用

package com.lif314.gulimall.product.config;

import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@EnableTransactionManagement // 开启事务功能
@MapperScan("com.lif314.gulimall.product.dao")  // 包扫描 Mapper接口
public class MyBatisConfig {
	...
}

然后在Service实现的方法上添加注解 @Transactional

    /**
     * 级联更新
     */
    @Transactional
    @Override
    public void updateBrandName(Long brandId, String name) {
        CategoryBrandRelationEntity categoryBrandRelationEntity = new CategoryBrandRelationEntity();
        categoryBrandRelationEntity.setBrandId(brandId);
        categoryBrandRelationEntity.setBrandName(name);
        // 更新与更新条件
        this.update(categoryBrandRelationEntity, new UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));
    }

    @Transactional
    @Override
    public void updateCategoryName(Long catId, String name) {
        this.baseMapper.updateCategory(catId, name);
    }
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_45617958/article/details/123288127

elasticsearch7.1中文文档-第一章-入门_清水l的博客-爱代码爱编程

入门 引言 Elasticsearch是一个高度可扩展开源的全文搜索引擎.它搜索几乎是实时的,用ES作为搜索引擎,为复杂搜索功能的需求提供解决方案. ES的使用场景: 网上商场,搜索商品.ES配合logstash,k

闪聚支付 第3章-C扫B支付-爱代码爱编程

需求分析 C扫B的概念 C扫B,即顾客(Customer)扫描商户(Business)提供的二维码来完成支付。下图是支付宝提供的C扫B业务流程: 商家出示付款二维码客户打开支付宝或微信的扫一扫,扫描二维码确认支付,完成支付。 C扫B支付分为两种方式:一是固定金额支付,顾客扫描后无需输入金额直接确认支付即可;另外一种是输入金额,顾客扫描后需自己输入待支

【高软】系统与规划管理师教材笔记-第一章-信息系统综合知识-爱代码爱编程

笔记也是自己看教材和在网上搜集的一些重点知识内容整合下来的,希望能对大家有作用吧。软考不易,且行且珍惜,祝愿大家早日领证! 建议大家还是先过一遍教材,有一个初步的理解和印象之后再着重记忆重点知识,有一些知识点还是非常拗口的,直接硬背会有点困难。 一、信息的定义和属性 1、信息的基本概念 信息是客观事物状态和运动特征的一种普遍形式,客观世界中大

springcloud alibaba - (第一章~第五章)-爱代码爱编程

第一章 微服务介绍 1.1 系统架构演变 随着互联网的发展,网站应用的规模也在不断的扩大,进而导致系统架构也在不断的进行变化。 从互联网早起到现在,系统架构大体经历了下面几个过程: 单体应用架构—>垂直应用架构—>分布式架构—>SOA架构—>微服务架构,当然还有悄然兴起的Service Mesh(服务网格化)。 接下来我们就

电商项目——商品服务-API-三级分类——第九章——上篇-爱代码爱编程

电商项目——初识电商——第一章——上篇电商项目——分布式基础概念和电商项目微服务架构图,划分图的详解——第二章——上篇电商项目——电商项目的虚拟机环境搭建_VirtualBox,Vagrant——第三章——上篇电商项目——Linux虚拟机中安装docker,mysql,redis_VirtualBox——第四章——上篇电商项目——电商项目的环境搭建_开发工

电商项目——商品服务-API-品牌管理&品牌分类关联与级联更新——第十二章——上篇-爱代码爱编程

电商项目——初识电商——第一章——上篇电商项目——分布式基础概念和电商项目微服务架构图,划分图的详解——第二章——上篇电商项目——电商项目的虚拟机环境搭建_VirtualBox,Vagrant——第三章——上篇电商项目——Linux虚拟机中安装docker,mysql,redis_VirtualBox——第四章——上篇电商项目——电商项目的环境搭建_开发工

《管理学》第五章 组织(MOOC+思维导图)-爱代码爱编程

《管理学》第五章 组织(MOOC+思维导图) 前言 9月28号写了课本上的组织部分(《管理学》第五章 组织)。今天写一下MOOC上的笔记。 文章目录 《管理学》第五章 组织(MOOC+思维导图)前言组织(课本)思维导图MOOC笔记碳原子、雁群和组织组织结构设计的工具箱部门从何而来管多少人合适金字塔与扁平化组织结构设计的影响因素经典的组织结构组

电商项目——商品服务-API-商品管理——第十五章——上篇-爱代码爱编程

电商项目——初识电商——第一章——上篇电商项目——分布式基础概念和电商项目微服务架构图,划分图的详解——第二章——上篇电商项目——电商项目的虚拟机环境搭建_VirtualBox,Vagrant——第三章——上篇电商项目——Linux虚拟机中安装docker,mysql,redis_VirtualBox——第四章——上篇电商项目——电商项目的环境搭建_开发工