商品服务-三级分类
cat_id:分类id,cat代表分类,bigint(20)
name:分类名称
parent_cid:在哪个父目录下
cat_level:分类层级
show_status:是否显示,用于逻辑删除
sort:同层级同父目录下显示顺序
ico图标,product_unit商品计量单位,
InnoDB表,自增大小1437,utf编码,动态行格式
1.在product服务的package com.ljn.gulimall.product.controller中打开CategoryController,添加方法:
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查出所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
2…在product服务的package com.ljn.gulimall.product.service;中打开CategoryService,添加方法:
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
}
3.在product服务的package com.ljn.gulimall.product.service.impl;中打开CategoryServiceImpl实现方法:
package com.ljn.gulimall.product.service.impl;
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
// 注入CategoryDao
@Autowired
CategoryDao categoryDao;
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
// 实现方法
@Override
public List<CategoryEntity> listWithTree() {
// 1.查出所有分类
List<CategoryEntity> entities = categoryDao.selectList(null);
// 2.组装成父子的树形结构
// 2.1 找到所有一级分类,一级分类父id=0,并返回为一个集合
List<CategoryEntity> Level1Menus = entities.stream().filter(categoryEntity -> {
// 一级分类
return categoryEntity.getParentCid() == 0;
}).map((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 Level1Menus;
}
// 递归查找所有菜单的子菜单,root:当前菜单 all:所有菜单
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
// 从所有菜单中过滤出子菜单
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
// 当前菜单的父ID=指定菜单的id 也就是判断在哪个父目录下
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;
}
}
启动renren-fast 前后端项目:
在左侧点击【分类维护】,希望在此展示3级分类
注意地址栏http://localhost:8001/#/product-category 可以注意到product-category我们的/被替换为了-
比如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
export default {
name: 'category',
components: {},
directives: {},
data() {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
};
},
mounted() {
},
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus(){
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(data=>{
console.log(data)
})
}
},
created(){
this.getMenus();
}
};
</script>
<style>
</style>
网关88配置
1.在vue项目的 static/config/index.js里修改
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
// 意思是说本vue项目中要请求的资源url都发给88/api,那么我们就让网关端口为88,然后匹配到/api请求即可,
// 网关可以通过过滤器处理url后指定给某个微服务
// renren-fast服务已经注册到了nacos中
刷新后需要重新登录,此时验证码不显示,因为此时验证码是请求88的,所以不显示。而验证码是来源于fast后台即8080端口的的。
解决:将renren-fast 注册到nacos注册中心,这样请求88网关转发到8080fast。
2.让fast里加入注册中心的依赖:
<!-- Nacos 配置管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!-- Nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
3.在renren-fast项目的application.yml中添加:
spring:
application:
name: renren-fast # 意思是把renren-fast项目也注册到nacos中,这样网关才能转发给
cloud:
nacos:
discovery:
server-addr: localhost:8848 # nacos
4.开启服务注册与发现
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
5.在gateway服务中按格式加入
- id: admin_route
# lb 代表负载均衡
uri: lb://renren-fast # 路由给renren-fast
predicates: # 什么情况下路由给它
- Path=/api/** # 默认前端项目都带上api前缀,就是我们前面的localhost:88/api
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment} # 把/api/* 改变成 /renren-fast/*
解决跨域:
package com.ljn.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration // gateway
public class GulimallCorsConfiguration {
@Bean // 添加过滤器
public CorsWebFilter corsWebFilter() {
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 跨域配置信息
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 允许跨域的头
corsConfiguration.addAllowedHeader("*");
// 允许跨域的请求方式
corsConfiguration.addAllowedMethod("*");
// 允许跨域的请求来源
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
corsConfiguration.setAllowCredentials(true);
// 任意url都要进行跨域配置
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
1、问题描述
在显示商品系统/分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree
但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。
2、解决方法就是定义一个product路由规则,以后/ap/product 的路径都转发给product服务,进行路径重写:
# product服务路由
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- # 把/api/* 去掉,剩下的留下来
- RewritePath=/api/(?>.*),/$\{segment}
# 应用名称
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
#命名空间的唯一ID
spring.cloud.nacos.config.namespace=8a9fe873-8e40-4d56-a77f-0b5993c6f52c
# 配置nacos服务注册与发现
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
(5)GulimallProductApplication 主启动类中开启服务注册与发现功能 @EnableDiscoveryClient
(6)启动服务后访问 localhost:88/api/product/category/list/tree
{"msg":"invalid token","code":401}
invalid token,非法令牌,后台管理系统中没有登录,所以没有带令牌
原因:先匹配的先路由,fast和product路由重叠,fast要求登录
修正:在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。
访问 http://localhost:88/api/product/category/list/tree 正常
访问 http://localhost:8001/#/product-category,也就是点击分类维护,正常,数据获取成功
原因是:先访问网关88,网关路径重写后访问nacos8848,通过nacos找到服务
1、使用scoped slot(插槽)实现:在el-tree标签里把内容写到span标签栏里即可
<template>
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}span>
<span>
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">
Append
el-button>
<el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">
Delete
el-button>
span>
span>
el-tree>
template>
<script>
export default {
name: "category",
components: {},
directives: {},
data() {
return {
menus: [],
data: [],
defaultProps: {
children: "children",
label: "name", // 要显显示的内容
},
};
},
mounted() { },
methods: {
// 3. 增加标签的方法
append(data) {
console.log("append", data)
},
// 2. 删除分类的方法
remove(node, data) {
console.log("delete", node, data)
},
// 1. 获取分类数据的方法
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
// 解构出数据中有用的data
console.log(data.data);
this.menus = data.data;
});
},
},
created() {
this.getMenus();
},
};
script>
<style>style>
2、逻辑删除
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds) {
// 1、删除之前需要判断待删除的菜单那是否被别的地方所引用。
// 2、自定义删除方法
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1、检查当前删除的菜单,是否被其它地方引用
// 2、逻辑删除
baseMapper.deleteBatchIds(asList);
}
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
4、前端请求处理
发送的请求:delete
发送的数据:this.$http.adornData(ids, false)
util/httpRequest.js中,封装了一些拦截器
http.adornParams是封装get请求的数据
http.adornData封装post请求的数据
ajax的get请求会被缓存,就不会请求服务器了。
所以我们在url后面拼接个date时间戳,让他每次都请求服务器
// 2. 删除分类的方法
remove(node, data) {
// 1. 获取当前节点id
var ids = [data.catId]
// 2. 发送请求前弹框提示
this.$confirm(`是否删除${data.name}菜单?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 3. 确认删除,发送post请求
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false)
}).then(({ data }) => {
// 4. 删除成功提示消息
this.$message({
type: "success",
message: "菜单删除成功!",
});
// 5. 删除成功后重新请求菜单
this.getMenus();
// 6. 设置默认展开菜单
this.expandedKey=[node.parent.data.catId]
})
}).catch(() => {
});
console.log("delete", node, data)
},
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId" :default-expanded-keys="expandedKey">
data() {
return {
expandedKey: [], // 展开基准
};
},
:visible.sync="dialogVisible"
<!-- 对话框 -->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false">取 消</el-button>
<el-button type="primary">确 定</el-button>
</span>
</el-dialog>
data() {
return {
dialogVisible: false, // 是否打开对话框,默认为false
};
},
methods: {
append(data) {
// 1. 点击append 按钮打开对话框
this.dialogVisible = true;
console.log("append", data)
},
<!-- 对话框 -->
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<!-- 表单项 -->
<el-form :model="category">
<el-form-item label="分类名称">
<!-- 输入框,双向绑定category中的属性 -->
<el-input v-model="category.name" 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="addCategory">确 定</el-button>
</span>
</el-dialog>
// 4. 添加三级分类的方法
addCategory() {
console.log("提交的三级分类数据", this.category)
// 1. 发送保存请求,提交
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false) // 要发送的数据
}).then(({ data }) => {
// 2. 保存成功提示消息
this.$message({
type: "success",
message: "菜单保存成功!",
});
// 3. 保存成功后关闭对话框
this.dialogVisible = false;
// 4. 刷新出新菜单
this.getMenus();
// 5. 设置默认展示的菜单
this.expandedKey = [this.category.parentCid];
})
},
append(data) {
console.log("append", data)
// 1. 点击append 按钮打开对话框
this.dialogVisible = true;
// 2. 点击按钮为category获取默认值
// 2.1 父id,当前点击append的catId
this.category.parentCid = data.catId;
// 2.2 层级catLevel 当前点击append 的层级+1
this.category.catLevel = data.catLevel * 1 + 1;
},
<!-- 按需展示:所有都展示修改按钮 -->
<el-button type="text" size="mini" @click="() => edit(node, data)">
Edit
</el-button>
methods: {
// 5. 点击修改分类按钮
edit(node, data){
// 显示对话框
this.dialogVisible=true;
// 回显数据
this.category.name=data.name;
console.log(node,data)
},
}
<!-- 对话框 -->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%">
<!-- 表单项 close-on-click-modal 关闭点击关闭空白处关闭对话框 -->
<el-form :model="category" :close-on-click-modal="false">
<el-form-item label="分类名称">
<!-- 输入框,双向绑定category中的属性 -->
<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="submitDate">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'category',
components: {},
directives: {},
data() {
return {
dialogType: "", // 对话框类型
title: "", // 对话框标题
category: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0, catId: null, icon: "", productUnit: "" }, // 表单中的数据对象
dialogVisible: false, // 是否打开对话框,默认为false
expandedKey: [], // 展开基准
menus: [],
data: [],
defaultProps: {
children: "children",
label: "name", // 要显显示的内容
},
};
},
mounted() { },
methods: {
// 7. 对话框确认按钮,提交数据的方法
submitDate() {
if (this.dialogType == "append") {
// 打开的是添加的对话框,保存分类
this.addCategory();
}
if (this.dialogType == "edit") {
// 打开的是修改的对话框,修改分类
this.editCategory();
}
},
// 6. 修改三级分类数据
editCategory() {
// 1. 解构要发送的数据
var { catId, name, icon, productUnit } = this.category;
// 2. 发送修改请求
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: 'post',
data: this.$http.adornData({ catId, name, icon, productUnit }, false)
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单修改成功!",
});
// 3. 关闭对话框
this.dialogVisible = false;
// 4. 刷新菜单
this.getMenus();
// 5. 展开父菜单
this.expandedKey = [this.category.parentCid];
})
},
// 5. 点击修改分类按钮
edit(node, data) {
// 1.1设置对话框类型为deit
this.dialogType = "edit";
// 1.2显示对话框
this.dialogVisible = true;
// 1.3对话框标题
this.title = "修改分类"
// 2. 发送请求回显最新数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get',
}).then(({ data }) => {
// 3. 请求成功,回显数据
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;
})
console.log(node, data)
},
// 4. 添加三级分类数据的方法
addCategory() {
console.log("提交的三级分类数据", this.category)
// 1. 发送保存请求,提交
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false) // 要发送的数据
}).then(({ data }) => {
// 2. 保存成功提示消息
this.$message({
type: "success",
message: "菜单保存成功!",
});
// 3. 保存成功后关闭对话框
this.dialogVisible = false;
// 4. 刷新出新菜单
this.getMenus();
// 5. 设置默认展示的菜单
this.expandedKey = [this.category.parentCid];
})
},
// 3. 点击增加标签按钮
append(data) {
console.log("append", data)
// 0. 设置对话框类型为append
this.dialogType = "append";
// 1. 点击append 按钮打开对话框
this.dialogVisible = true;
// 设置标题
this.title = "添加分类";
// 2. 点击按钮为category获取默认值
// 2.1 父id,当前点击append的catId
this.category.parentCid = data.catId;
// 2.2 层级catLevel 当前点击append 的层级+1
this.category.catLevel = data.catLevel * 1 + 1;
// 3. 清空修改后的回显信息
this.category.name = "";
this.category.showStatus = 1;
this.category.sort = 0;
this.category.catId = null;
this.category.icon = ""; // 输入什么绑定什么
this.category.productUnit = "";
},
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId" :default-expanded-keys="expandedKey" draggable :allow-drop="allowDrop">
// 9. 统计当前拖拽节点总层数的方法
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]);
}
}
},
// 8. 拖拽是否放置的方法
allowDrop(draggingNode, dropNode, type) {
// 可以拖动放置的条件:被拖动的当前节点以及所在的父节点总层数不能大于3
console.log("allowFrop:",draggingNode,dropNode,type)
// 1. 统计当前节点的总层数
this.countNodeLevel(draggingNode.data);
// 2.当前正在拖动的节点+父节点所在的深度不大于3即可
let deep = (this.maxLevel - draggingNode.data.catLevel) + 1;
console.log("深度:", deep);
// 3. this.maxLevel
// 3.1 拖到节点里面
if (type == "inner") {
return deep + dropNode.level <= 3;
} else {
return deep + dropNode.parent.level <=3;
}
},
// 11. 修改子节点层级的方法
data() {
return {
updateNodes: [], // 拖拽时修改的节点
},
};
},
methods: {
updateChildNodeLevlel(node) {
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.updateChildNodeLevlel(node.childNodes[i]);
}
}
},
// 10. 拖拽完成事件处理
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log('handleDrop: ', draggingNode, dropNode, dropType);
// 1. 当前节点最新的父节点id
let pCid = 0;
let siblings = null;
// 1.1 以兄弟关系拖拽
if (dropType == "before" || dropType == "after") {
// 最新父id=进入节点的父id
pCid = dropNode.parent.data.catId == undefined ? 0 : dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
}
// 1.2 inner 方式拖入
else {
// 最新父id=进入节点的id
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 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.updateChildNodeLevlel(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)
},
}
CategoryController
中添加方法 /**
* 批量修改
*/
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity category) {
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
// 12. 批量保存方法
batchSave() {
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单顺序修改成功!",
});
// 3.2. 刷新菜单
this.getMenus();
// 3.3. 展开父菜单
this.expandedKey = [this.pCid];
this.updateNodes = [];
this.maxLevel = 0;
this.pCid = 0;
});
},
<el-button type="danger" @click="batchDelete">批量删除</el-button>
// 13.批量删除
batchDelete() {
// 1. 要删除元素的数组
let catIds = [];
// 2. 获取选中元素
let checkedNodes = this.$refs.menuTree.getCheckedNodes();
console.log("被选中的元素", checkedNodes);
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId);
}
this.$confirm(`是否批量删除【${catIds.name}】菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
// 3.发送请求
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false),
})
.then(({ data }) => {
this.$message({
type: "success",
message: "菜单批量删除成功!",
});
// 刷新出新的菜单
this.getMenus();
})
.catch(() => { });
})
.catch(() => { });
},