欢迎访问 生活随笔!

生活随笔

当前位置: 首页 >

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

发布时间:2024/3/12 46 豆豆
生活随笔 收集整理的这篇文章主要介绍了 第5章-商品服务-品牌管理 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

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

文章目录

  • 第5章 商品服务-品牌管理
    • 1. 使用逆向工程的代码
      • 1.1 导入代码
      • 1.2 显示状态优化
    • 2. 文件上传技术
      • 2.1 阿里云---对象存储OSS 云存储开启
      • 2.2 OSS整合测试
        • 2.2.1 普通上传
        • 2.2.2 SpringCloud Alibaba上传 -- 普通上传
        • 2.2.3 服务器签名直传OSS---配置
        • 2.2.4 服务器签名直传OSS---编写逻辑
        • 2.2.5 服务器签名直传OSS---配置网关
        • 2.2.5 服务器签名直传OSS---前端
    • 3. 表单校验&自定义校验
      • 3.1 前端校验
      • 3.2 后端校验 --- JSR303
    • 4. 统一异常处理 @ControllerAdvice
    • 5. JSR303分组校验 & 自定义校验注解: 完成多场景的复杂校验
      • 5.1 JSR303分组校验
      • 5.2 JSR303自定义校验注解
    • 6. SPU和SKU
      • 6.1 基本概念
      • 6.2 API-属性分组-前端组件抽取&父子组件交互
        • 6.2.1 属性分组-前端
        • 6.2.2 属性分组-前后端联调
        • 6.2.3 分组新增&级联选择器
        • 6.2.4 分组修改&级联选择器修改
        • 6.2.5 品牌分类关联与级联更新

1. 使用逆向工程的代码

1.1 导入代码

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

  • 运行项目

  • 测试阶段–去除权限

全局搜索 Ctrl + Shift + F

isAuth

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

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

1.2 显示状态优化

目的:

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-columnprop="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-switchv-model="scope.row.showStatus"active-color="#13ce66"inactive-color="#ff4949"></el-switch></template>
  • 优化新增修改的界面

显示状态换位Switch开关

<el-form-item label="显示状态" prop="showStatus"><!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态"></el-input> --><el-switchv-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">

  • 监听开关事件

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

<!-- 使用slot-scope自定义显示效果 --><template slot-scope="scope"><!-- Switch开关:通过slot-scope绑定显示状态--><el-switchv-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 / number—true
inactive-valueswitch 关闭时的值boolean / string / numberfalse
<!-- Switch开关:通过slot-scope绑定显示状态--><el-switchv-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

后端

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

2. 文件上传技术

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

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

  • 开通服务

  • 上传后访问图片地址

文件上传形式

  • 利用防伪签名

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

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

访问密钥

RAW访问控制

分配权限

代码片段

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 {@Testpublic 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 {@Autowiredprivate 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:8848alicloud:access-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxsecret-key: xxxxxxxxxxxxxxxxxxxxxxxxxoss:endpoint: oss-cn-shanghai.aliyuncs.com
  • 测试 success
@Testpublic 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—配置

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

  • 创建模块

  • 导入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-partyspring.cloud.nacos.config.server-addr=xx.xx.xx.xx:8848 spring.cloud.nacos.config.namespace=third-party
  • 抽取对象存储配置

spring.application.name=gulimall-third-partyspring.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: xxxxxxxxxxxxxxxxxxxxxxxxxxsecret-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxoss: endpoint: oss-cn-shanghai.aliyuncs.com
  • 编写配置中心 application.yml
spring:cloud:nacos:discovery:server-addr: xx.xx.xx.xx:8848application:name: gulimall-third-partyserver: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 {@AutowiredOSSClient ossClient;@Testpublic 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—编写逻辑

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

  • 服务端签名直传并设置上传回调 代码实例 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:8848alicloud:access-key: LTAI5tFgyxgqZfKTDbgVWJbdsecret-key: TYXFVknpv3RhLFcf2Cel3OdspRs6bXoss:endpoint: oss-cn-shanghai.aliyuncs.combucket: gulimall-lif314application:name: gulimall-third-partyserver: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 {@AutowiredOSS 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;} }

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

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

spring:cloud:gateway:routes:# product- id: product_routeuri: lb://gulimall-productpredicates:- Path=/api/product/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}- id: third_party_routeuri: lb://gulimall-third-partypredicates:- Path=/api/thirdparty/**filters:- RewritePath=/api/thirdparty/(?<segment>/?.*),/$\{segment}# renren-fast- id: admin_routeuri: lb://renren-fastpredicates: # 什么情况下路由给它- 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

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

Upload组件:

  • mutipleUpload.html
<template><div><el-uploadaction="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-uploadaction="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 {@AutowiredOSS 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

  • 测试

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

3.1 前端校验

优化logo显示

  • 添加激活 0 和 1
<el-form-item label="显示状态" prop="showStatus"><!-- <el-input v-model="dataForm.showStatus" placeholder="显示状态"></el-input> --><el-switchv-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的hiif (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的hiif (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)]

/*** 品牌名*/@NotBlankprivate 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中写的messageString 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 测试

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

// 数据校验异常处理@ExceptionHandlerpublic 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)@ResponseBodypublic 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) // @ResponseBodypublic R handleValidException(Exception exception ){log.error("数据校验异常: {}, 异常类型: {}", exception.getMessage(), exception.getClass());return R.error();}}

异常处理逻辑

  • 捕捉异常

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

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

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

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

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) // @ResponseBodypublic 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());}}

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})@TableIdprivate 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})@TableIdprivate 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 })@NotEmptyprivate 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}@Overridepublic void initialize(ListValue constraintAnnotation) {int[] vals = constraintAnnotation.vals();for (int val : vals) {set.add(val);}}/*** 判断是否校验成功* @param value 提交的值,判断改值知否在constraintAnnotation.vals();范围中* @param context 校验的上下文环境信息*/@Overridepublic 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;
  • 测试

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

  • 后端添加新的groups,UpdateStatusGroup以及新的API接口,如/update/status
  • 在前端传入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拥有的特性叫基本属性。如机身长度,这个是手机共用的属性。而每
    款手机的属性值不同

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

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

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

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

    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销售属性值表 属性关系-规格参数-销售属性-三级分类 关联关系

    表关系

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

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

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

    • 添加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>
    • 页面效果

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

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

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

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

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

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

    • 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: [], // 刷新展开菜单iddefaultProps: {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-inputv-model="dataForm.key"placeholder="参数名"clearable></el-input></el-form-item><el-form-item><el-button @click="getDataList()">查询</el-button><el-buttonv-if="isAuth('product:attrgroup:save')"type="primary"@click="addOrUpdateHandle()">新增</el-button><el-buttonv-if="isAuth('product:attrgroup:delete')"type="danger"@click="deleteHandle()":disabled="dataListSelections.length <= 0">批量删除</el-button></el-form-item></el-form><el-table:data="dataList"borderv-loading="dataListLoading"@selection-change="selectionChangeHandle"style="width: 100%"><el-table-columntype="selection"header-align="center"align="center"width="50"></el-table-column><el-table-columnprop="attrGroupId"header-align="center"align="center"label="分组id"></el-table-column><el-table-columnprop="attrGroupName"header-align="center"align="center"label="组名"></el-table-column><el-table-columnprop="sort"header-align="center"align="center"label="排序"></el-table-column><el-table-columnprop="descript"header-align="center"align="center"label="描述"></el-table-column><el-table-columnprop="icon"header-align="center"align="center"label="组图标"></el-table-column><el-table-columnprop="catelogId"header-align="center"align="center"label="所属分类id"></el-table-column><el-table-columnfixed="right"header-align="center"align="center"width="150"label="操作"><template slot-scope="scope"><el-buttontype="text"size="small"@click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button><el-buttontype="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-updatev-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>
    • 测试

    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
    @Overridepublic PageUtils queryPage(Map<String, Object> params, Long catelogId) {//如果没有选中三级分类,则查询指定的数据,id传0if(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==3if (node.level == 3) {this.catId = data.catId;// 重新查询this.getDataList();}},
    • 测试

    6.2.3 分组新增&级联选择器

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

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

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

    attrgroup-add-or-update.html

    <el-form-item label="所属分类" prop="catelogId"><el-cascaderv-model="dataForm.catelogId":options="categories":props="props"></el-cascader><!-- <el-inputv-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"},

    问题:会显示空集合

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

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

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

    • 修改中的回显问题

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

    • 添加字段
    /*** 菜单路径,用于修改时回显*/@TableField(exist = false) // 数据库不存在private Long[] catelogPath;
    • 递归获取菜单路径
    // [2,25,225]@Overridepublic 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,2private List<Long> findParentPath(Long catelogId, List<Long> paths ){// 获取当前分类的idpaths.add(catelogId);CategoryEntity byId = this.getById(catelogId);// 如果存在父分类,则需要递归查询if(byId.getParentCid()!=0){//递归查找父节点findParentPath(byId.getParentCid(), paths);}return paths;}
    • 前端回显
    this.dataForm.catelogPath = data.attrGroup.catelogPath;

    细化

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

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

    <el-dialog:title="!dataForm.attrGroupId ? '新增' : '修改'":close-on-click-modal="false":visible.sync="visible"@closed="dialogClose">// 数据清空dialogClose() {this.dataForm.catelogPath = [];},

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

    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-cascaderv-model="dataForm.catelogPath"placeholder="试试搜索:手机":options="categories":props="props"filterable></el-cascader><!-- <el-inputv-model="dataForm.catelogId"placeholder="所属分类id"></el-input> --></el-form-item>

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

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

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

    @Configuration @MapperScan("scan.your.mapper.package") public class MybatisPlusConfig {/*** 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)*/@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));return interceptor;}@Beanpublic 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* 避免缓存出现问题(该属性会在旧插件移除后一同移除)*/@Beanpublic 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; // }}

    模糊查询

    给查询添加条件

    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 {@Overridepublic 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);}}

    关联分类

    • 前端添加关联分类

    /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-cascaderfilterableclearableplaceholder="试试搜索:手机"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-buttontype="text"size="small"@click="updateCatelogHandle(scope.row.brandId)">关联</el-button>

    在div内加入对话框

    <!-- 关联分类对话框 --><el-dialogtitle="关联分类":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-buttonsize="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-columnfixed="right"header-align="center"align="center"label="操作"><template slot-scope="scope"><el-buttontype="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();} /*** 保存品牌分类关联的完整信息*/@Overridepublic 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);}

    级联更新

    如:品牌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();}

    实现方法

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

    关联实现方法

    /*** 级联更新*/@Overridepublic 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));}

    测试

    • 类名级联更新

    controller更新

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

    实现方法

    /*** 级联更新*/@Overridepublic void updateCascade(CategoryEntity category) {this.updateById(category);//级联更新categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());}

    更新name

    @Overridepublic 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>

    测试

    这种情况是 事务

    需要在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@Overridepublic 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@Overridepublic void updateCategoryName(Long catId, String name) {this.baseMapper.updateCategory(catId, name);}// 更新与更新条件this.update(categoryBrandRelationEntity, new > UpdateWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));}

    测试

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

    实现方法

    /*** 级联更新*/@Overridepublic void updateCascade(CategoryEntity category) {this.updateById(category);//级联更新categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());}

    更新name

    @Overridepublic 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>

    测试

    这种情况是 事务

    需要在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@Overridepublic 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@Overridepublic void updateCategoryName(Long catId, String name) {this.baseMapper.updateCategory(catId, name);}

    总结

    以上是生活随笔为你收集整理的第5章-商品服务-品牌管理的全部内容,希望文章能够帮你解决所遇到的问题。

    如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。