八、商品服务&三级分类
上一级页面:index-la
八、商品服务&三级分类
8.1 基础概念
8.1.1、三级分类
一级分类查出二级分类数据,二级分类中查询出三级分类数据
数据库设计
8.1.2、SPU 和 SKU
SPU:Standard Product Unit (标准化产品单元)
是商品信息聚合的最小单位,是一组可复用,易检索的标准化信息的组合,该集合描述了一个产品的特性
IPhoneX 是 SPU,MI8 是 SPU
IPhoneX 64G 黑曜石 是 SKU
MIX8 + 64G 是 SKU
SKU: Stock KeepingUnit (库存量单位)
8.1.3、基本属性 【规格参数】与 销售属性
每个分共下的商共享规格参数、与销售属性,只是有些商品不一定更用这个分类下全部的属性:
属性是以三级分类组织起来的
规格参数中有些是可以提供检索的
规格参数也是基本属性,他们具有自己的分组
属性的分组也是以三级分类组织起来的
属性名确定的,但是值是每一个商品不同来决定的
【属性分组-规格参数-销售属性-三级分类】关联关系
SPU-SKU-属性表
8.1.4、接口文档位置
https://easydoc.xyz/s/78237135/ZUqEdvA4/hKJTcbfd
8.1.5、 Object 划分
1、PO (persistant object) 持久化对象
po 就是对应数据库中某一个表的一条记录,多个记录可以用 PO 的集合,PO 中应该不包含任何对数据库到操作
2、DO ( Domain Object) 领域对象
就是从现实世界抽象出来的有形或无形的业务实体
3、TO (Transfer Object) 数据传输对象
不同的应用程序之间传输的对象
4、DTO (Data Transfer Object) 数据传输对象
这个概念来源于 J2EE 的设计模式,原来的目的是为了 EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分数调用的性能和降低网络负载,但在这里,泛指用于展示层与服务层之间的数据传输对象
5、VO(value object) 值对象
通常用于业务层之间的数据传递,和 PO 一样也是仅仅包含数据而已,但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要,用 new 关键字创建,由 GC 回收
view Object 试图对象
接受页面传递来的数据,封装对象,封装页面需要用的数据
6、BO(business object) 业务对象
从业务模型的角度看,见 UML 原件领域模型中的领域对象,封装业务逻辑的, java 对象,通过调用 DAO 方法,结合 PO VO,进行业务操作,business object 业务对象,主要作用是把业务逻辑封装成一个对象,这个对象包括一个或多个对象,比如一个简历,有教育经历,工作经历,社会关系等等,我们可以把教育经历对应一个 PO 、工作经验对应一个 PO、 社会关系对应一个 PO, 建立一个对应简历的的 BO 对象处理简历,每 个 BO 包含这些 PO ,这样处理业务逻辑时,我们就可以针对 BO 去处理
7、POJO ( plain ordinary java object) 简单无规则 java 对象
传统意义的 java 对象,就是说一些 Object/Relation Mapping 工具中,能够做到维护数据库表记录的 persisent object 完全是一个符合 Java Bean 规范的 纯 java 对象,没有增加别的属性和方法,我们的理解就是最基本的 Java bean 只有属性字段 setter 和 getter 方法
POJO 时是 DO/DTO/BO/VO 的统称
8、DAO(data access object) 数据访问对象
是一个 sun 的一个标准 j2ee 设计模式,这个模式有个接口就是 DAO ,他负持久层的操作,为业务层提供接口,此对象用于访问数据库,通常和 PO 结合使用,DAO 中包含了各种数据库的操作方法,通过它的方法,结合 PO 对数据库进行相关操作,夹在业务逻辑与数据库资源中间,配合VO 提供数据库的 CRUD 功能
8.2 三级分类接口编写
// 返回查询所有分类以及子子分类,以树形结构组装起来
List<CategoryEntity> listWithTree();
实现类:
@Override
public List<CategoryEntity> listWithTree() {
// 1、查出所有分类 设置为null查询全部
List<CategoryEntity> entities = baseMapper.selectList(null);
// 2、组装成父子的树形结构
List<CategoryEntity> levelList = entities.stream().filter(categoryEntity -> {
// parentCid ==0 为父目录默认0
return categoryEntity.getParentCid() == 0;
}).map(menu -> {
// 设置二三级分类 递归
menu.setChildren(getChildrens(menu,entities));
return menu;
}).sorted((menu1, menu2) -> {
// 排序 Sort字段排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return levelList;
}
/**
* 递归查询子分类
* @param root 当前category对象
* @param all 全部分类数据
* @return
*/
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> collect = all.stream().filter(categoryEntity -> {
// 遍历所有的category对象的父类id = 等于root的分类id 说明是他的子类
return categoryEntity.getParentCid() == root.getCatId();
}).map(menu -> {
// 1、递归遍历菜单
menu.setChildren(getChildrens(menu, all));
return menu;
}).sorted((menu1, menu2) -> {
// 2、菜单排序
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return collect;
}
跨域
跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;
下图详细说明了 URL 的改变导致是否允许通信
跨域流程
浏览器发请求都要实现发送一个请求询问是否可以进行通信 ,我直接给你返回可以通信不就可以了吗?
相关资料参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS
解决跨越( 一 ) 使用nginx部署为同一域
开发过于麻烦,上线在使用
解决跨域 ( 二 )配置当次请求允许跨域
1、添加响应头
Access-Control-Allow-Origin: 支持哪些来源的请求跨域
Access-Control-Allow-Methods: 支持哪些方法跨域
Access-Control-Allow-Credentials: 跨域请求默认不包含cookie,设置为true可以包含cookie
Access-Control-Expose-Headers: 跨域请求暴露的字段
CORS请求时, XML .HttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: CacheControl、Content-L anguage、Content Type、Expires、
Last-Modified、 Pragma。 如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
Access-Control-MaxAge: 表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一-请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
网关配置文件
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast # lb负载均衡
predicates:
- Path=/api/** # path指定对应路径
filters: # 重写路径
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
跨越设置
请求先发送到网关,网关在转发给其他服务 事先都要注册到注册中心
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨越
corsConfiguration.addAllowedHeader("*"); // 允许那些头
corsConfiguration.addAllowedMethod("*"); // 允许那些请求方式
corsConfiguration.addAllowedOrigin("*"); // 允许请求来源
corsConfiguration.setAllowCredentials(true); // 是否允许携带cookie跨越
// 注册跨越配置
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
8.2.1 树形展示三级分类数据
1、用到的前端组件 Tree 树形控件
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
<!--
data 展示数据
props 配置选项
children 指定子树为节点对象的某个属性值
label 指定节点标签为节点对象的某个属性值
disabled 节点选择框是否禁用为节点对象的某个属性值
@node-click 节点被点击时的回调
-->
配置静态数据就能显示出对应的效果
2、编写方法获取全部菜单数据
getMenus() {
this.$http({
// 请求接口见上面
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get"
}).then(({ data }) => {
console.log("返回的菜单数据" + data.data);
this.menus = data.data;
});
}
3、最终展示结果 ( append ,edit 会在后面介绍)
8.2.2 逻辑删除&删除效果细化
效果图
1、节点的内容支持自定义,可以在节点区添加按钮或图标等内容
<el-button
v-if="node.childNodes.length == 0"
type="text"
size="mini"
@click="() => remove(node, data)"
> Delete</el-button>
<!--
v-if="node.childNodes.length == 0" 没有子节点可以删除
type 对应类型
size 大小
@click 点击后出发的方法 此处使用了 箭头函数
-->
2、前端remove方法进行删除
remove(node, data) {
this.$confirm(`是否删除【${data.name}】菜单 ? `, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
// 拿到当前节点的catId
var ids = [data.catId];
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
this.$message({
message: "菜单删除成功",
type: "success"
});
// 刷新菜单
this.getMenus();
// 设置默认需要展开的菜单
/**
default-expanded-keys 默认展开节点的 key 数组
*/
this.expandedKey = [node.parent.data.catId];
console.log(node.parent.data.catId);
});
})
.catch(() => {});
console.log("remove", node, data);
}
3、后端接口 -- 逻辑删除配置
3.1 第一种方式 mybatisplus 逻辑删除参考官网:https://baomidou.com/guide/logic-delete.html
在appliction.yml 中配置 myabtisplus 逻辑删除
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
db-config:
id-type: auto # 数据库主键自增
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
3.2 第二种方式 在 Entity中 使用注解
/**
是否为数据库表字段
默认 true 存在,false 不存在
标记为false 说明 该字段不在数据库
**/
@TableField(exist = false)
private List<CategoryEntity> children;
4、Controller 实现 使用了代码生成器
/**
* 删除
* @RequestBody:获取请求体,必须发送post请求才有 get请求没有
* SpringMvc 自动将请求体的数据 ( json ) 转为对应的对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
// 1、检查当前删除的菜单,是否被别的地方应用
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
//Service 实现
@Override
public void removeMenuByIds(List<Long> asList) {
// 1、逻辑删除
baseMapper.deleteBatchIds(asList);
}
8.2.3 新增效果&基本修改
1、前端组件 button 组件 Dialog 对话框
<!-- 层级小于2 才能新增 -->
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>Append
</el-button>
<el-button type="text" size="mini" @click="() => edit(data)"
>edit
</el-button>
<!-- 上面组件在tree中-->
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
:before-close="handleClose"
:close-on-click-modal="false"
>
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<el-input
v-model="category.productUnit"
autocomplete="off"
></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary" @click="submitData">确 定</el-button>
</span>
</el-dialog>
2、新增&修改
append(data) {
this.dialogVisible = true;
console.log("append", data);
this.category.name = "";
this.dialogType = "add";
this.title = "添加分类";
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.name = "";
this.category.catId = null;
this.category.icon = "";
this.category.productUnit = "";
}, // 要修改的数据
edit(data) {
console.log("要修改的数据", data);
this.dialogType = "edit";
this.title = "修改分类";
this.dialogVisible = true;
// 发送请求获取最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get"
}).then(({ data }) => {
// 请求成功
console.log("要回显的数据", data);
this.category.name = data.data.name;
this.category.catId = data.data.catId;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
});
},
submitData() {
if (this.dialogType == "add") {
// 进行新增
this.addCategory();
}
if (this.dialogType == "edit") {
// 进行修改
this.editCategory();
}
},
// 添加三级分类
addCategory() {
console.log("添加三级分类的数据", this.category);
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
message: "菜单保存成功",
type: "success"
});
// 关闭对话框
this.dialogVisible = false;
// 重新刷新数据
this.getMenus();
// 默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
}, // 修改三级分类数据
editCategory() {
// 解构出单独的几个对象 用来提交
var { catId, name, icon, productUnit } = this.category;
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success"
});
// 关闭对话框
this.dialogVisible = false;
// 重新刷新数据
this.getMenus();
// 默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
3、要考虑到的点 以及细节
- 添加完成后要把表单的数据进行清除,否则第二次打开任然会有上次表单提交剩下的数据 this.category.name = "";
- 修改和新增用的是同一个表单,因此在方法对话框中 动态的绑定了 :title="title" 标题 用于显示是新增还是修改
- 一个表单都是一个提交方法 因此在提交方法的时候进行了判断,根据变量赋值决定调用那个方法 this.dialogType = "add"; this.dialogType = "edit";
8.2.4 拖拽功能&数据收集&批量删除
效果演示
1、前端用的组件 Tree 树形控件 可拖拽节点
通过 draggable 属性可让节点变为可拖拽。
<el-tree
:expand-on-click-node="false"
:data="menus"
:props="defaultProps"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
ref="menuTree"
>
<!--
:expand-on-click-node 是否在点击节点的时候展开或者收缩节点 默认 true 则只有点箭头图标的时候才会展开或者收缩节点。
show-checkbox 节点是否可被选择
node-key 每个树节点用来做唯一标识的属性
default-expanded-keys 默认展开节点的节点
draggable 表示是否可以被拖拽 true&false
allow-drop 拖拽时判定目标节点能否被放置
node-drop 拖拽成功完成出发的事件
ref 该组件tree的引用
详细解释参考官网 https://element.eleme.cn/#/zh-CN/component/tree
-->
2、主要业务逻辑 TODO:暂时不懂 回头再来看
allowDrop:
拖拽时判定目标节点能否被放置。type
参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后
@node-drop
拽成功完成时触发的事件
共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event
allowDrop(draggingNode, dropNode, type) {
// 1、被拖动的当前节点以及所在的父节点总层次不能大于3
// 1) 被拖动节点的总层数
console.log("allowDrop", draggingNode, dropNode, type);
this.countNodeLevel(draggingNode.data);
// 当前正在拖动的节点 + 父节点所在的深度不大于3即可
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
console.log("深度", deep);
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <= 3;
}
},
countNodeLevel(node) {
// 找到所有子节点,求出最大深度
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
// 递归查找
this.countNodeLevel(node.children[i]);
}
}
}, handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
// 1、当前节点最新的父节点
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// this.PCid = pCid
this.pCid.push(pCid);
// 2、当前拖拽节点的最新顺序
for (let i = 0; i < siblings.length; i++) {
if (siblings[i].data.catId == draggingNode.data.catId) {
// 如果遍历的是当前正在拖拽的节点
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
// 当前结点的层级发生变化
catLevel = siblings[i].level;
// 修改它子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
// 如果遍历当前正在拖拽的节点
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid
});
} else {
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
// 3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
updateChildNodeLevel() {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
3、批量删除
<el-button type="danger" @click="batchDelete">批量删除</el-button>
batchDelete方法
batchDelete() {
let catIds = [];
let names = [];
// 返回目前被选中的节点所组成的数组
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
// 遍历节点数组 拿到需要的值
catIds.push(checkedNodes[i].catId);
names.push(checkedNodes[i].name);
}
this.$confirm(`是否批量删除【${names}】菜单 ? `, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false)
}).then(({ data }) => {
this.$message({
message: "删除成功",
type: "success"
});
// 刷新菜单
this.getMenus();
});
});
},
后端接口也是逻辑批量删除
void removeMenuByIds(List<Long> asList); //接收的是一个id数组
总结:
前端用到的组件
Dialog 对话框、可拖拽节点、Switch 开关、Button 按钮、Tree组件(属性较多)
细节点:
没开启拖拽
开启拖拽:
通过 draggable 属性可让节点变为可拖拽。