谷粒商城总结46-70集
1.获取三级分类
1.1.后端实现
最终就是类似京东那样的页面显示
我们的思路就是先查询每一个以及分类就是根据parent_cid = 0来进行查询 找到全部的一级分类,然后看每一个一级分类下面是否还有二级分类有的话进行保存,然后在查询每一个二级分类下面是否有三级分类。
1.1.2.总体思路就是使用递归的方法来实现的
// 三级分类 查出所欲分类以及子分类 以树形结构组装起来形成我们的列表
@Override
public List<CategoryEntity> listWithTree() {
// 因为我们前端使用的element的组件里面帮助封装成了树形结构的形式
// 里面必须要有list<children> 属性, 但是我们使用这个最后我们三级分类还是有children虽然是null的但还是有这个属性,但是我们前端的那个组件是一旦发现你还有children的属性就还会有下拉箭头,所以这个是不好的
// 因此我们在这个list<children> 属性上面还要加上一个注解,来保证如果你没有子孩子的话你就没有这个属性
// 查询出所有分类
List<CategoryEntity> entities = list(null);
// 组装成父子树形结构
// 找到所有的一级分类 使用流对象进行筛选出我们的一级分类的数字
List<CategoryEntity> level1 = entities.stream().filter((categoryEntity) -> {
// 尽量使用equal
return categoryEntity.getParentCid().equal(0L);
}).map((menu) -> {
// 每一个menu就是对应单独的一级分类
// 这个就是获取我们子分类的数据 使用的就是递归的形式来实现的
menu.setChildren(getChildrens(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
// 小的在前面
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
// 收集成数组
}).collect(Collectors.toList());
return level1;
}
// 递归的方法
// 使用递归来字分类 后面是所有菜单
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
// 获取到一个子菜单 但是可能一个菜单还有子菜单 所以就要进行递归操作
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
// 判断这个是否是root的子孩子
return categoryEntity.getParentCid().equals(root.getCatId());
}).map(categoryEntity -> {
// 手机这个属于这个分类的下一个分类
// 这个就是使用的递归
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
// 菜单的排序小的排序在前面
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
// 手机成我们的子分类有哪些
}).collect(Collectors.toList());
return children;
}
最终我们测试的时候就可以看到我们是否获取到了全部的分类的数据
1.1.3.拖拽后进行批量的修改
就是我们进行拖拽后进行修改然后调用 MyBatisPlus 的批量保存的方法 根据catId来实现
1.2.前端实现
使用我们element-ui的组件来实现我们属性结构的展示
这个expand-on-click-node就是说你只有点击箭头才会跳出二级分类
先来说defaultProps
1). 获取后台的数据
使用axios请求来获取数据我们可以写一个模板来实现然后使用关键词进行调用的话就可以实现
使用es6的解构方法
这样就进行数据的获取了
2). 拖拽的实现
注意:最终还是会有瑕疵
我们首先要开启拖拽,其次还要判断你拖拽的这个位置究竟能否放置, 我们最大层级不能超过3级,
其余添加新增和修改我们就不做过多的解释,很简单的业务的代码
最主要的还是拖拽功能的实现
拖拽主要有这三种的方式:
- inner: 拖拽到目的元素中
- prev: 拖拽到目的元素前
- next: 拖拽到目的元素后
首先我们的树形组件帮助我们封装了三个属性
注意因为我们是批量进行修改,什么意思喃就是可能你数据库的信息是这个结点是3层的然后我们经过了一个拖拽,发现他的层级变了,然后在进行拖拽到另外一个点去,那么这个时候我们数据库的的level的层级就会出错,因为我们在拖拽一次后层级可能就发生变化了,所以我们需要实现 树形组件帮助我们封装好的数据,
下面还有一个问题: 我们怎么判断你能否拖拽到这个结点,就是判断我们拖拽的这个结点以及看他是否有子节点然后加上 算出这个结点的总层级, 这个怎么计算: 算出这个结点最深的子节点的深度是多少,然后减去我们拖拽这个结点的level + 1 就是这个结点包括他子孩子的有多少层级 然后
*****deep 就是你拖拽这个结点的是多少层的几点
***我们如果是inner的话 就判断直接dropNode的level + deep 就是我们总共的层级 看是否大于3
***如果是after或者是before的话 就是dropNode.parent,.level + deep 看是否大于3
1). 算出拖拽这个最大深度是多少
我们传入这个结点draggingNode 注意不能传入data因为他是数据库里面的深度,不能随时变化我们要使用组件封装好的来实现。
this.countNodeLevel(node) {
if(node.childNodes != null && node.childNodes.length > 0) {
for(let i = 0; i < node.childNodes.length; i++) {
// 判断孩子结点的深度是否大于最大深度,大于的话就赋值给最大深度
if(node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level
}
// 如果还有子孩子就继续查询
this.countNodeLevel(node.childNodes[i]);
}
} else {
// 我们还要判断一种情况就是什么
// 如果我们拖拽的这级分类是没有子孩子的那么他的最终层级应该是1层
// 那么我们应该让maxLevel 等于拖拽这个结点的level 因为我们的deep是等于
// 拖拽这个结点的(最深层级 - 这个层级) + 1
this.maxLevel = node.level; // 就可以完美解决这个问题 因为我们要考虑拖拽的结点是没有子结点的
}
}
// 最终我们的maxLevel就是这个拖拽结点的最大深度
2). 算出拖拽这个结点的是多少层级
他最大深度 - 自己的深度 + 1 就是这个结点的层级是多少
这个就不用加绝对值了 因为本来就不用加
let deep = (this.maxLevel - draggingNode.level) + 1
3). 根据不同类型来判断是否能放置到这个位置
如果是inner类型就是这个dropNode.level + deep <= 3
如果是 before 或者是 after 的话 dropNode.parent.level + deep <= 3
if(type === "inner") {
return dropNode.level + deep <= 3;
} else {
return dropNode.parent.level + deep <= 3;
}
4). 下面就是实现拖拽成功后触发的方法
4.1). 下面就是实现更新我们改变了哪些自孩子序列
现在先来分析,
1.如果我们拖拽的这个结点的层级发生了变化那么层级就要进行更新,并且这个结点的全部子节点的层级都要进行更新。
2.我们还需要得到拖拽这个结点的拖拽后的父CatId是谁要进行修改
// 拖拽成功后触发的方法
handleDrop(draggingNode, dropNode, dropType, ev) {
let pCid = 0;
// 这个是存放我们的拖拽后这个结点父节点的全部childNodes有哪些
let siblings = null;
// 根据不同的拖拽类型做不同的判断
if(dropType === "after" || dropType === "before") {
// 这里我们还要进行判断 如果拖拽的结点来到了一级分类他的pCid应该为0
// 注意这里为什么要使用data因为catId数据库里面的catId
// 如果是一级分类相邻结点的父亲的parent.data是空的 所以要特判
pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
// 这个childNodes包括我们刚拖拽过去的那个结点
siblings = dropNode.parent.childNodes;
} else {
// 拖拽的类型是inner的话 这个结点就是他的父节点 那么pCid 就直接算出
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 将我们的得到的其中一个pCid放进数组里面 因为我们可能不止拖动一个结点可能是多个 所以pCid是进行批量修改的
this.pCid.push(pCid);
// 更新当前拖拽结点的就是sibLings的顺序
for(let i = 0; i < siblings.length; i++) {
// 如果你是新加入进来的就要进行修改层级 以及 父id 以及排序的顺序
if(draggingNode.data.catId == siblings[i].data.catId) {
// 我们先判断层级变化没有
let catLevel = draggingNode.level;
// 表示拖拽到这个结点后你的层级也发生了变化
if(siblings[i].level != draggingNode.level) {
// 修改为现在的层级
catLevel = siblings[i].level;
// 那么我们还要使用递归修改这个结点自孩子的全部层级(如果这个结点层级没变那么子的也不需要变化)
this.updateChildNodeLevel(siblings[i]);
}
} else {
// 就是原本这个childNodes里面的数据
// 我们只需要根据catId 来修改顺序
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i
})
}
let catLevel = draggingNode.level;
}
}
// 修改这个层级子孩子的层级
updateChildNodeLevel(node) {
if(node.childNodes != null && node.childNodes.length > 0) {
for(let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i];
// 这个updateNodes就是我们最终修改的
this.updateNodes.push({
catId: cNode.data.catId,
sort: i, // 这个其实不需要排序的 因为你移动后会自动根据上面后面又会排序的
catLevel: cNode.level // 用的是属性组件进行封装的
})
// 如果有子孩子还是这样处理
this.updateChildNodeLevel(node.childNodes[i]);
}
}
}
5). 最终进行保存修改
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
message: "菜单顺序等修改成功",
type: "success"
});
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单 因为我们可能改变了多个
this.expandedKey = this.pCid;
this.updateNodes = [];
// 最大深度也进行重置
this.maxLevel = 0;
// this.pCid = 0;
});
},
最终经过自己的复盘拖拽功能已经完全实现
2.oss对象存储实现图片的上传
因为我们这个项目后面可能要用到很多第三方服务,我们新建了一个模块,专门用来整合第三方服务这块的。
首先是引入springcloud帮我们整合的oss服务,比原生的oss要好用很多,不用复制很多东西了
2.1.依赖的引入
<!-- 引入oss服务-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<!-- <version>2.1.0.RELEASE</version>-->
</dependency>
<!--这样就不用创建ossClient 直接注入就可以使用了-->
<!--引入common 也就是nacos配置-->
单独创建命名空间。
这些基本都是固定的
@RestController // 将响应体返回出去
public class OssController {
// 我们直接引入就可以了 因为引入了那个依赖
@Resource
private OSSClient ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}") // 这些是放在nacos的配置中心里面的
private String endpoint;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
// @Value("${spring.cloud.alicloud.oss.bucket}")
// private String bucket;
@RequestMapping("oss/policy")
public R policy() {
// 填写Bucket名称,例如examplebucket。
String bucket = "gulimall-anlemon";
// 填写Host地址,格式为https://bucketname.endpoint。
String host = "https://" + bucket + "." + endpoint;
// 设置上传回调URL,即回调服务器地址,用于处理应用服务器与OSS之间的通信。OSS会在文件上传完成后,把文件上传信息通过此回调URL发送给应用服务器。
// String callbackUrl = "https://192.168.0.0:8888";
// 设置上传到OSS文件的前缀,可置空此项。置空后,文件将上传至Bucket的根目录下。
// 形成我们的日期形式的文件夹
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 文件的前缀
String dir = format + "/";
Map<String, String> respMap = null;
// 创建ossClient实例。
// OSS ossClient = new OSSClientBuilder().build(endpoint, accessId, accessKey);
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
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;
return R.ok().put("data",respMap);
}
}
2.2.我们将value里面的东西全部放在nacos里面
2.3下面就是写bootstrap进行引入
3.使用common模块
最终要变成这个样式 每一个模块左边都要有这个 那么我们就可以引入common组件 然后直接实现就可以了
我们定义组件的点击事件
3.1.子组件点击后传递给父类触发事件
里面有我们穿过来得data node 和 component
父类引用
父类要想引入就要使用component来引入子组件
然后我们父组件里面就可以直接得到子组件传递过来的三个参数。
4.使用JSR303校验
这个是干什么的,就是怕防止有人使用postman进行直接添加数据,然后不用通过前端的校验来添加或者修改数据。
4.1.基本使用
步骤1:使用校验注解
在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。
在非空处理方式上提供了@NotNull,@Blank和**@**
(1)@NotNull
注解元素禁止为null,能够接收任何类型
(2)@NotEmpty
该注解修饰的字段不能为null或""
支持以下几种类型
字符序列(字符序列长度的计算)
集合长度的计算
map长度的计算
数组长度的计算
(3)@NotBlank
该注解不能为null,并且至少包含一个非空白字符。接收字符序列。
步骤2:在请求方法种,使用校验注解@Valid,开启校验,
// @Valid 注解开启校验功能
@RequestMapping("/save")
public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);
return R.ok();
}
在postman种发送上面的请求
{
"timestamp": "2020-04-29T09:20:46.383+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.brandEntity.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"brandEntity.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
"defaultMessage": "不能为空",
"objectName": "brandEntity",
"field": "name",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='brandEntity'. Error count: 1",
"path": "/product/brand/save"
}
能够看到"defaultMessage": “不能为空”,
我们也可以自定义一个message
但是这个defaultMessage显然是不符合我们的观看方法的,因为我们可以进行修改
步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。
使用统一的返回模式map集合进行返回
/**
* 保存
*/
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
// Valid 开启校验
// BindContext 包含了校验结果
// Validated 来实现我们只对增加那个进行校验
public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand,
BindingResult result) {
// 如果发生了错误
if (result.hasErrors()) {
Map<String, String> map = new HashMap<>();
// 获取错误的校验结果
result.getFieldErrors().forEach((item) -> {
// FieldError
// 获取到错误提示
String message = item.getDefaultMessage();
// 获取错误属性的名字(name)
String field = item.getField();
map.put(field, message);
});
return R.error(400, "提交的数据不合法").put("data",map);
} else {
// 如果没有错误就要进行保存
brandService.save(brand);
}
// brandService.save(brand);
return R.ok();
}
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
步骤4:统一异常处理
这个就是处理校验发生异常的时候和其他未知的方法
我们可以在common里面定义一个枚举然后返回我们校验错误的信息 和 状态码, 以及未知错误的校验错误的信息和状态码
可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。
(1)抽取一个异常处理类
package com.bigdata.gulimall.product.exception;
import com.bigdata.common.utils.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* 集中处理所有异常
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.bigdata.gulimall.product.controller")
public class GulimallExceptionAdvice {
@ExceptionHandler(value = Exception.class)
public R handleValidException(MethodArgumentNotValidException exception){
Map<String,String> map=new HashMap<>();
BindingResult bindingResult = exception.getBindingResult();
bindingResult.getFieldErrors().forEach(fieldError -> {
String message = fieldError.getDefaultMessage();
String field = fieldError.getField();
map.put(field,message);
});
log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
return R.error(400,"数据校验出现问题").put("data",map);
}
// 还可以定义一个其他的未知异常
@ExceptionHandler(value = throwable.class)
public R handleValidException(Throwable throwable) {
// 返回未知错误的验证 并且返回信息 我们使用的是一个枚举类
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
(2)定义一个枚举异常类
package com.atguigu.common.exception;
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*
*
*/
public enum BizCodeEnume {
// 要使用直接 BizeCOdeEnume.VAILD_EXCEPTION.getMsg() 这个就可以获取错误的消息 BizeCOdeEnume.VAILD_EXCEPTION.getCode() 这个就可以获取错误的状态码
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
4.2.分组使用
19. 分组校验功能(完成多场景的复杂校验)
1、给校验注解,标注上groups,指定什么情况下才需要进行校验
如:指定在更新和添加的时候,都需要进行校验
@NotEmpty
@NotBlank(message = "品牌名必须非空",groups = {UpdateGroup.class,AddGroup.class})
private String name;
在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。
2、设置分组 我们只需要写分别的接口就可以了
给每一个需要校验的加上分组
比如你是向添加的时候才进行校验 还是 修改的时候进行校验
然后我们还要在controller里面写上这个方法的分组有哪些