文章目录
**数据库:**首先gulimall_pms数据库中的pms_category表要复制粘贴内容
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/bf1574620ae9d04a95d513131b4c2572.png)
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/87d73e4e69f2ccba0c6b7d3a69ab47e4.png)
4.1 递归树形结构获取商品分类(pms_category)数据
①在注册中心中“product”命名空间中,创建“gulimall-product.yml”配置文件:
②将idea的application.yml内容复制到nacos配置中心去
gulimall-product的application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://192.168.146.129:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
application:
name: gulimall-product
cloud:
nacos:
discovery:
server-addr: localhost:8848
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-value: 1
logic-not-delete-value: 0
server:
port: 10000
③在本地创建“bootstrap.properties”文件,指明配置中心的位置和使用到的配置文件:
gulimall-product的bootstrap.properties
spring.application.name=gulimall-product
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.namespace=902b203e-41b0-47b2-8b87-56f90b4e4162
spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-product.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
④然后启动gulimall-product,查看到该服务已经出现在了nacos的注册中心中了
⑤业务代码编写
CategoryEntity实体类
@TableField(exist=false)//该注解的false表示表中不存在该字段,只是自定义的字段,方便编码
private List<CategoryEntity> children;
gulimall-product的controller层
(1)在CategoryController 中编写请求入口方法:
/**
* 查询出所有分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
//listWithTree()该方法被CategoryServiceImpl重写
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
gulimall-product的service层
(2) 在CategoryServiceImpl中编写方法递归查找所有分类,以树形结构组装起来:一级分类ParentCid=0,一级分类的下的二级分类满足categoryEntity.getParentCid() == root.getCatId()
CategoryService接口
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
}
CategoryServiceImpl实现类,重写上述的接口方法
@Override
public List<CategoryEntity> listWithTree() {
//1、查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2、组装成父子的树形结构
//2.1)、找到所有的一级分类
List<CategoryEntity> level1Menus = entities.stream().filter(
//一级分类的parentId=0,根据这个条件构建出一级分类的数据
categoryEntity -> 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;
}
//递归查找所有菜单的子菜单
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1、找到子菜单
categoryEntity.setChildren(getChildrens(categoryEntity,all));
return categoryEntity;
}).sorted((menu1,menu2)->{
//2、菜单的排序
return (menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort());
}).collect(Collectors.toList());
return children;
}
(3)访问:http://localhost:10000/product/category/list/tree
4.2 配置网关路由与路径重写
1、 前端修改
① 后端启动renren-fast项目(为了发送验证码给页面),右键以管理员身份运行vscode,打开前端renren-fast-vue项目,在终端运行前端项目:npm run dev
② 进入人人后台管理系统,创建一级菜单:
同时数据库中便生成了该数据
③ 给商品系统添加一个子菜单,点击新增,选择菜单:
同样这个数据也会在数据库中生成,这个url:product/category
④ 创建renren-fast-vue\src\views\modules\product目录,这样创建,是因为url:product/category,对应于product-category,在该目录下,新建“category.vue”文件 ,编写一个方法:
编写category.vue代码
Vue的代码框架就是由这三个标签组成:**、
<template>
<el-tree
:data="data"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</template>
<script>
//这里可以导入其他文件(比如: 组件, 工具 js, 第三方插件 js, json文件, 图片文件等等)
//例如: import 《组件名称》 from '《组件路径》 ';
export default {
//import 引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
return {
data: [],
defaultProps: {
children: "children",
label: "label",
},
};
},
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 scoped>
</style>
⑤ 刷新人人项目页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”,这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree
需要修改renren-fast-vue项目的 static\config\index.js文件中的api接口请求地址,改成给网关发请求,让网关路由到指定的地址:
//修改前为:
window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
//修改后为:
// api接口请求地址,改成给网关发请求,让网关路由到指定的地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
2、后端-配置网关路由
http://localhost:88,这个地址是我们网关微服务的接口,我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心:
① 在renren-fast项目下的pom文件中导入gulimall-common坐标依赖:
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
② 在renren-fast项目下的application.yml中配置服务名称和注册中心地址,将这个服务注册进nacos:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
③ 在renren-fast项目下的bootstrap.properties文件中配置服务名称和配置中心nacos的地址:
spring.application.name=renren-fast
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.namespace=4e38920c-8a9f-4d6b-b2c1-3907862d7f3a
③ 在renren-fast项目的主启动类上添加@EnableDiscoveryClient注解,开启服务注册与发现:
@EnableDiscoveryClient
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
④ 前台的所有请求都是经由“http://localhost:88/api”来转发的,配置网关路由,在gulimall-gateway服务的applicaiton.yml文件中添加路由规则:
gateway项目的yml文件
- id: admin_route
uri: lb://renren-fast # 负载均衡到renren-fast服务
predicates:
- Path=/api/** # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
⑤ 启动renren-fast项目,访问http://localhost:8001/#/login,发现验证码报错:没有验证码
分析原因:
现在验证码请求路径为:http://localhost:88/api/captcha.jpg?uuid=?
原始验证码请求路径为:http://localhost:8080/renren-fast/captcha.jpg?uuid=?
在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写,希望网关将现在验证码的请求路径转成原来验证码的请求路径。
⑥修改gulimall-gateway服务下的application.yml文件中“admin_route”路由规则:添加过滤器filter
- id: admin_route
uri: lb://renren-fast # 负载均衡到renren-fast服务
predicates:
- Path=/api/** # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
filters:
# 意思是将路径 /api/xxx 重写为 /renren-fast/xxx
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
# http://localhost:88/api/captcha.jpg?uuid=?
# http://localhost:8080/renren-fast/captcha.jpg?uuid=?
⑦再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。
但是填写验证码准备登陆时,出现了403状态码, 原因:CORS 头缺少 Access-Control-Allow-Origin,这是一种跨域问题,访问的域名和端口和原来的请求不同,请求就会被限制,因此需要解决跨域问题。
为了解决上述问题,见下一节
3、跨域问题的解决
①跨域:
② 跨域流程:
③ 解决跨域方法1:使用ngnix部署为同一域
④ 解决跨域方法2:配置当次请求允许跨域,添加请求头:
解决方法:在gulimall-gateway中创建config包并定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。
@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);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
}
}
⑤ 重启项目,填写验证码访问http://localhost:8001/#/login,报错:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)
因为renren-fast项目中也配置了跨域,因此出现了多个跨域,需要把renren-fast中配置的去掉,即去掉该项目下的io.renren.config.CorsConfig类。(注释)
⑥ 重启renren-fast项目,访问http://localhost:8001/#/login登录成功,进入分类维护菜单,打开f12,发现出现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异常。
4、再次树形展示商品数据
需要在网关gulimall-gateway的application.yml文件中,定义一个gulimall-product服务的路径重写:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/** # 只要请求为/api/product/**,就会路由到gulimall-product服务下
filters:
# 意思是将路径 /api/xxx 重写为/xxx
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
访问:http://localhost:88/api/product/category/list/tree,出现{"msg":"invalid token","code":401}
因为/api/product/**
路由规则会拦截/api/**
的路由规则,因此在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,调整路由规则顺序:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/** # 只要请求为/api/product/**,就会路由到gulimall-product服务下
filters:
# 意思是将路径 /api/xxx 重写为/xxx
- RewritePath=/api/(?<segment>/?.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast # 负载均衡到renren-fast服务
predicates:
- Path=/api/** # 只要请求是localhost:88/api/**这个路径就路由到renren-fast服务
filters:
# 意思是将路径 /api/xxx 重写为 /renren-fast/xxx
- RewritePath=/api/(?<segment>/?.*), /renren-fast/$\{segment}
123456789101112131415
再次访问http://localhost:88/api/product/category/list/tree,即可得到所有分类数据:
到前端人人页面不再报错
4.3 逻辑删除
逻辑删除:就是并不删除数据库的数据,只是以一种状态不显示它。仅是标记它被删除了,这就是逻辑删除,比如后面可以设置show_status为0,标记它已经被删除。
物理删除:表示真正的删除数据库的数据(某一行或几行)
1、前端页面展示数据库的数据
①在category.vue添加商品栏显示、append、delete、框选择等
<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>
data() {
return {
menus: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
methods: {
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("remove", node, data);
},
}
2、后端逻辑删除业务处理
(1)物理删除
① 测试代码生成工具(renren-generate)生成的com.bigdata.gulimall.product.controller.CategoryController类中delete()方法:
/**
* 删除
* @RequestBody:获取请求体,必须发送post请求
* SpringMvc自动将请求体的数据(json),转换为对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//传入的是数组
categoryService.removeByIds(Arrays.asList(catIds));
return R.ok();
}
②使用postman,模拟前端发送请求localhost:88/api/product/category/delete,删除catid=1432的那行数据:
发现数据库的1432行数据已经被删除
(2)逻辑删除
参考Mybatis-plus官方文档:https://baomidou.com/guide/logic-delete.html
①gulimall-product的application.yml
#在gulimall-product服务的application.yml文件中配置逻辑删除的规则
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)
#另外在gulimall-product服务的application.yml文件中,设置日志级别,方便打印出SQL语句:
logging:
level:
com.atguigu.gulimall: debug
②controller:业务上删除菜单的时候,不能直接删除,需要检查当前删除的菜单是否被别的地方引用,修改delete()方法。
自定义逻辑删除方法removeMenuByIds
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
// categoryService.removeByIds(Arrays.asList(catIds));
//自定义逻辑删除方法
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
③service:接口(省略)和实现类(在com.bigdata.gulimall.product.service.impl.CategoryServiceImpl中编写业务逻辑:)
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1、检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
④entity:实体类字段上加上@TableLogic
注解:
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")//自定义显示,1表示逻辑删除(与模板相反)
private Integer showStatus;
⑤我们并不希望将数据库中的数据删除,而是标记它被删除了,这就是逻辑删除,可以设置show_status为0,标记它已经被删除:
⑥在postman模拟前端发送请求localhost:88/api/product/category/delete,删除catid=1431的数据,检查数据库发现show_status=0;
3、前端逻辑删除和新增展示(了解即可)
删除delete;新增Append
(1)逻辑删除
点击Delete删除菜单:
- 参考ElementUI中的Tree树形控件,在templeate标签中添加
<el-tree>
- 在script标签中添加remove()方法,同时参考ElementUI中的 Message 消息提示组件和 MessageBox 弹框组件
(2)新增append
点击Append新增菜单:
- 参考ElementUI中的Dialog 对话框组件,在templeate标签中添加
<el-dialog>
- 在script标签中添加append (data) 方法和addCategory ()方法
4、前端-批量删除:
① 在template中新增批量删除按钮
② 在script标签中编写batchDelete ()方法
4.4 数据修改(update)
因为后端的代码基本用代码生成器生成,所以只需要简单修改
1、后端gulimall-product的controller
后端修改package com.atguigu.gulimall.product.controller**.CategoryController()**类中的方法:
/**
* 信息
*/
@RequestMapping("/info/{catId}")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
2、前端继续修改category.vue
点击edit修改菜单:
① 修改<el-dialog>
,添加一个按钮edit
② 修改<el-dialog>
,添加绑定的数据
<el-dialog
:title="title"
:visible.sync="dialogVisible"
width="30%"
: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>
③ 在script标签中添加edit(data)方法,submitData () 方法,editCategory ()方法,并对append (data) 方法和addCategory ()方法进行修改
<el-button type="text" size="mini" @click="edit(data)"
>edit</el-button
>
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
//修改三级分类数据
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];
});
},
4.5 拖拽功能
1、通过拖拽节点改变节点顺序以及父子关系:
① 参考ElementUI中的Tree 树形控件,找到可拖拽节点,给<el-tree>
添加属性draggable
② 在script标签中编写 allowDrop ()方法和 countNodeLevel ()方法
2、前端-拖拽数据收集并保存:
拖动菜单时需要修改顺序和级别,收集拖拽后节点发生变化的节点数据:
① 参考ElementUI中的Tree 树形控件,找到可拖拽节点,给<el-tree>
添加@node-drop=“handleDrop”
② 在script标签中编写 handleDrop ()方法和updateChildNodeLevel ()方法,将收集的节点数据放进 updateNodes: []
中
③ 在script标签中编写batchSave ()方法将 updateNodes: []
中的数据发送给服务器
④ 后端编写方法接收数据并插入数据库
@RequestMapping("/update/sort")
public R updateSort(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}