第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>- 绑定显示状态
- 优化新增修改的界面
显示状态换位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
| inactive-value | switch 关闭时的值 | boolean / string / number | — | false |
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
- 上传文件流实例代码
- 单元测试
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
文档
- 引入依赖
- 配置OSS
- 引入
- Spring结合Resource
**使用 **
- 依赖导入到common中
- 在application.yml中配置
- 测试 success
2.2.3 服务器签名直传OSS—配置
创建模块:整合第三方服务,短信、邮箱、OSS等
- 创建模块
- 导入common依赖和OSS依赖以及common中的依赖管理
- 注册到注册中心和配置中心 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- 抽取对象存储配置
oss.yml中
spring:cloud:alicloud:access-key: xxxxxxxxxxxxxxxxxxxxxxxxxxsecret-key: xxxxxxxxxxxxxxxxxxxxxxxxxxxoss: endpoint: oss-cn-shanghai.aliyuncs.com- 编写配置中心 application.yml
- 排除common中引入的MyBatisPLUS/MySQL
- 启动服务发现
- 启动测试
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
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();}}- 配置相关参数
- 逻辑代码
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-a07fd09001e62.2.5 服务器签名直传OSS—前端
Upload组件:
- mutipleUpload.html
- singleUnload.html
- policy.js
修改
- 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
- 品牌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
- 添加校验规则 – 使用校验器
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" }],},使用自定义校验器
- 校验搜索符
- 校验数字
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
响应码是400,意味着校验是失败的
但这种不符合业务规定
- 自定义校验消息提示
符合规范的消息提示
给校验的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
- @Pattern 自定义注解
- @Min 数字
- Postman 测试
4. 统一异常处理 @ControllerAdvice
使用步骤:
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
- 抽取异常处理类 exception.GulimallExceptionControllerAdvice
- 标注注解 @ControllerAdvice
- 所有的Controller不处理异常,只需要将异常抛出去:去除BindingResult
- 处理异常逻辑
编写数据校验异常处理,使用注解 @ExceptionHandler
// 数据校验异常处理@ExceptionHandlerpublic void handleValidException(){}使用value指定处理什么类的异常, Exception.class 处理所有异常
// 数据校验异常处理@ExceptionHandler(value = Exception.class)public void handleValidException(){}- 感知异常 编写参数 Exception
- 使用lombok中@Slf4j记录日志
- 异常处理返回值:如果可以处理页面,可以返回ModelAndView,相当于跳转页面。我们统一向前端传送json,所以使用R,并标注注解@ResponseBody
所有的方法都返回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();}}异常处理逻辑
- 捕捉异常
- 精确处理异常
- 使用BindingResult获取异常并进行处理
- 添加公共处理异常
处理逻辑:如果能够精确匹配异常,则进行处理,否则使用公共异常处理方法
// 公共处理异常@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;} }- 使用异常状态码
5. JSR303分组校验 & 自定义校验注解: 完成多场景的复杂校验
5.1 JSR303分组校验
分组校验:新增和修改的时候规则不一样,如brandId。
- 每一个校验注解都可以添加group属性:标注什么情况需要进行校验
由于每一个模块都会使用,直接在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)
问题是一旦加了@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状态,可以用正则,但我们可以利用其他方式解决
复杂场景。比如我们想要下面的场景
-
编写自定义的校验注解
-
每一注解关联一个校验器,编写自定义的校验器
-
关联校验器和校验注解
在common引入依赖
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version></dependency>编写校验注解
- 注解
- Message默认配置提示
- 校验器
- 关联
- 使用
- 测试
Error:在单独修改状态时因为前端只传入了id和状态,此时后端会校验update时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模板
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()
- category组件
- 属性分组 attrgroup.html
- 测试
6.2.2 属性分组-前后端联调
/product/attrgroup/list/{catelogId}
后端添加方法
@PathVariable URL中的路径参数
- controller
- 实现 MyBatisPLUS
前端获取数据与显示
// 感知子组件,节点被迪纳基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>- 绑定数据并获得
- 在组件创建时获取
- 定义显示props
| label | 指定选项标签为选项对象的某个属性值 | string | — | ‘label’ |
| children | 指定选项的子选项为选项对象的某个属性值 | string | — | ‘children’ |
问题:会显示空集合
解决方法:让后端,当children为空时就不要返回
// 使用@JsonInclude() 设置字段/*** 子分类 */@JsonInclude(JsonInclude.Include.NON_EMPTY) // 不为空才返回@TableField(exist = false) // 数据表中不存在private List<CategoryEntity> children;这样提交存在bug,表单里拿到的时每一个菜单节点的id数组,我们只需要发送最后一个
- 修改中的回显问题
6.2.4 分组修改&级联选择器修改
- 添加字段
- 递归获取菜单路径
- 前端回显
细化
- 在新增时清除回显数据;
监听修改对话框,一但对话框关闭,则清空数据
<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);}- 后端-新增关联关系
级联更新
如:品牌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更新
实现方法
/*** 级联更新*/@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章-商品服务-品牌管理的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: 解决Vue中重复点击相同路由控制台报错问
- 下一篇: CentOS6 安装gcc编译器,解决【