第5章-商品服务-品牌管理-爱代码爱编程
第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-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>
- 优化新增修改的界面
显示状态换位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">
- 监听开关事件
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-value switch 打开时的值 boolean / string / number — true inactive-value switch 关闭时的值 boolean / string / number — false
<!-- 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
后端
/**
* 修改
*/
@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访问控制
用户登录名称 gulimall@1885413714140910.onaliyun.com
AccessKey ID LTAI5tFgyxgqZfKTDbgVWJbd
AccessKey Secret TYXFVknpv3RhLFcf2Cel3OdspRs6bX
分配权限
代码片段
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上传 – 普通上传
文档
- 引入依赖
<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—配置
创建模块:整合第三方服务,短信、邮箱、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-party
spring.cloud.nacos.config.server-addr=xx.xx.xx.xx:8848
spring.cloud.nacos.config.namespace=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
# 抽取配置
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—编写逻辑
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: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;
}
}
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
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
- 测试
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 测试
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();
}
}
异常处理逻辑
- 捕捉异常
- 精确处理异常
// 数据校验异常处理 -- 精确处理
@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();
}
在编写业务逻辑时,先将异常跑出去,然后再统一进行处理
- 业务规范 — 一异常状态码
使用枚举:为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
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());
}
}
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;
- 测试
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: [], // 刷新展开菜单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>
- 测试
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();
}
},
- 测试
6.2.3 分组新增&级联选择器
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"
},
问题:会显示空集合
解决方法:让后端,当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]
@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;
细化
- 在新增时清除回显数据;
监听修改对话框,一但对话框关闭,则清空数据
<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-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>
6.2.5 品牌分类关联与级联更新
统计错误是因为引入了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;
// }
}
模糊查询
给查询添加条件
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);
}
}
关联分类
- 前端添加关联分类
/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);
}
级联更新
如:品牌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));
}
测试
- 类名级联更新
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>
测试
这种情况是 事务
需要在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)); }
测试
- 类名级联更新 ---- 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>
测试
这种情况是 事务
需要在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); }