Spring Cloud Alibaba商城实战项目基础篇(day03)
Posted 钱难有~
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud Alibaba商城实战项目基础篇(day03)相关的知识,希望对你有一定的参考价值。
五、后台管理
5.1、商品服务
5.1.1、三级分类
5.1.1.1、查询所有菜单与子菜单
我们需要维护所有菜单以及各种子菜单,子菜单里面可能还有子菜单,所以我们采用递归的方式进行书写。
我们先在CategoryController中修改list方法,让他以组装树形结构进行返回。
/**
* 查询列表,并且以树形结构进行返回
*/
@RequestMapping("/list")
public R list()
List<CategoryEntity> categoryEntityList = categoryService.listWithTree();
return R.ok().put("categoryEntityList", categoryEntityList);
由于数据库表中是没有子菜单这个属性,所以我们需要在实体类中添加这个属性,一般开发中可以重新写一个VO,更加解耦。在CategoryEntity中添加一个children属性,但是需要加一个注解,告诉Mybatis-plus,这个属性我的表中没有,不需要理他。
/**
* 子分类
*/
@TableField(exist = false)
private List<CategoryEntity> children;
CategoryService中写一个接口。
/**
* 查出所有分类,并且组装成父子结构
* @return
*/
List<CategoryEntity> listWithTree();
CategoryServiceImpl也顺带写一写。
@Override
public List<CategoryEntity> listWithTree()
// 查询所有
List<CategoryEntity> categoryEntities = baseMapper.selectList(null);
// 找到所有一级分类
List<CategoryEntity> collect = categoryEntities.stream().filter((categoryEntity) ->
// 返回父分类id为0的,父id等于0说明他是一级分类
return categoryEntity.getParentCid() == 0;
).map((menu)->
// 设置子菜单
menu.setChildren(getChildren(menu,categoryEntities));
return menu;
).sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
.collect(Collectors.toList());
// 设置每一个父分类的子分类
return collect;
由于需要递归遍历,所以我们把遍历方法抽取出来。
/**
* 获取子菜单
* @param currentMenu 当前菜单
* @param allMenu 所有菜单
* @return 所有子菜单
*/
private List<CategoryEntity> getChildren(CategoryEntity currentMenu,List<CategoryEntity> allMenu)
List<CategoryEntity> childrents = allMenu.stream().filter(categoryEntity -> categoryEntity.getParentCid() == currentMenu.getCatId())
// 每个子菜单可能还有子菜单
.map(categoryEntity ->
categoryEntity.setChildren(getChildren(categoryEntity, allMenu));
return categoryEntity;
)
// 排序
.sorted(Comparator.comparingInt(menu -> (menu.getSort() == null ? 0 : menu.getSort())))
.collect(Collectors.toList());;
return childrents;
然后启动商品服务就可以开始测试。
5.1.1.2、配置网关和路由重写
我们需要维护后台管理系统,启动renren-fast-vue项目和mall-admin后端项目,直接npm run dev
开跑。我们首先写的功能是分类维护。
renren-fast-vue的页面规则是http://localhost:8001/#/product-category
,页面在src/views/modules/product下的目录里面。
我们新建一个prodect文件夹和category.vue,用于保存目录管理页面。为了便于开发,我们队vscdoe进行一些配置。
首先是配置vue模板,输入vue时可以直接跳出模板。
输入vue可以直接开始配置模板。
把原来的注释掉,直接换成下面的这段配置。
"Print to console":
"prefix": "vue",
"body": [
"<!-- $1 -->",
"<template>",
"<div class='$2'>$5</div>",
"</template>",
"",
"<script>",
"//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)",
"//例如:import 《组件名称》 from '《组件路径》';",
"",
"export default ",
"//import引入的组件需要注入到对象中才能使用",
"components: ,",
"data() ",
"//这里存放数据",
"return ",
"",
";",
",",
"//监听属性 类似于data概念",
"computed: ,",
"//监控data中的数据变化",
"watch: ,",
"//方法集合",
"methods: ",
"",
",",
"//生命周期 - 创建完成(可以访问当前this实例)",
"created() ",
"",
",",
"//生命周期 - 挂载完成(可以访问DOM元素)",
"mounted() ",
"",
",",
"beforeCreate() , //生命周期 - 创建之前",
"beforeMount() , //生命周期 - 挂载之前",
"beforeUpdate() , //生命周期 - 更新之前",
"updated() , //生命周期 - 更新之后",
"beforeDestroy() , //生命周期 - 销毁之前",
"destroyed() , //生命周期 - 销毁完成",
"activated() , //如果页面有keep-alive缓存功能,这个函数会触发",
"",
"</script>",
"<style lang='scss' scoped>",
"//@import url($3); 引入公共css类",
"$4",
"</style>"
],
"description": "Log output to console"
模板设置好以后,我们需要配置格式化,首先先安装一个插件,Vetur。
进入配置界面。进入json界面编辑。
把原来的注释掉,直接换成下面这段配置。
// tab 大小为2个空格
"editor.tabSize": 2,
// 编辑器换行
"editor.wordWrap": "off",
// 保存时格式化
"editor.formatOnSave": true,
// 开启 vscode 文件路径导航
"breadcrumbs.enabled": true,
// prettier 设置语句末尾不加分号
"prettier.semi": false,
// prettier 设置强制单引号
"prettier.singleQuote": true,
// 选择 vue 文件中 template 的格式化工具
"vetur.format.defaultFormatter.html": "js-beautify-html",
// vetur 的自定义设置
"vetur.format.defaultFormatterOptions":
"js-beautify-html":
"wrap_line_length": 30,
"wrap_attributes": "auto",
"end_with_newline": false
,
"prettier":
"singleQuote": true,
"semi": false,
"printWidth": 100,
"wrapAttributes": false,
"sortAttributes": false
,
"[vue]":
"editor.defaultFormatter": "octref.vetur"
,
"vetur.completion.scaffoldSnippetSources":
"workspace": "💼",
"user": "🗒️",
"vetur": "✌"
后面你ctrl+s可以直接保存加格式化了。
这个后台管理系统我们使用的是Element-UI的树形控件:https://element.eleme.cn/#/zh-CN/component/tree, 我们先把官方文档的代码copy进去看看效果。
<!-- -->
<template>
<el-tree :data="data"
:props="defaultProps"
@node-click="handleNodeClick">
</el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default
//import引入的组件需要注入到对象中才能使用
components: ,
data()
return
data: [
label: '一级 1',
children: [
label: '二级 1-1',
children: [
label: '三级 1-1-1',
,
],
,
],
,
label: '一级 2',
children: [
label: '二级 2-1',
children: [
label: '三级 2-1-1',
,
],
,
label: '二级 2-2',
children: [
label: '三级 2-2-1',
,
],
,
],
,
label: '一级 3',
children: [
label: '二级 3-1',
children: [
label: '三级 3-1-1',
,
],
,
label: '二级 3-2',
children: [
label: '三级 3-2-1',
,
],
,
],
,
],
defaultProps:
children: 'children',
label: 'label',
,
,
methods:
handleNodeClick(data)
console.log(data)
,
,
//监听属性 类似于data概念
computed: ,
//监控data中的数据变化
watch: ,
//方法集合
//生命周期 - 创建完成(可以访问当前this实例)
created() ,
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() ,
beforeCreate() , //生命周期 - 创建之前
beforeMount() , //生命周期 - 挂载之前
beforeUpdate() , //生命周期 - 更新之前
updated() , //生命周期 - 更新之后
beforeDestroy() , //生命周期 - 销毁之前
destroyed() , //生命周期 - 销毁完成
activated() , //如果页面有keep-alive缓存功能,这个函数会触发
</script>
<style lang='scss' scoped>
//@import url(); 引入公共css类
</style>
这里的数据都是写死的死数据,我们需要替换成数据库查询的数据。先将data中数据库删除掉,根据vue生命周期,我们直接把方法写在created里面。我们在methods中写一个方法。
methods:
handleNodeClick(data)
console.log(data)
,
getMenus()
this.$http(
url: this.$http.adornUrl('/product/category/list'),
method: 'get',
).then(( data ) =>
console.log(data)
)
,
,
在created里面调用即可。
//生命周期 - 创建完成(可以访问当前this实例)
created()
this.getMenus();
,
一启动直接404,一看原来是端口不对,我们还需要修改端口。
在src/config/index.js下定义了接口请求地址。
为了统一地址,我们需要给网关发请求,由网关进行统一路由。
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88';
保存后发现发现他要求我们重新登录,且验证码都没有了,原来是他直接给我们网关发请求。
由于需要网关路由,我们还需要把mall-admin这个项目注册到nacos中。在application.yml中加入这两段配置。
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: mall-admin
最后在启动类上加入这这个注解,允许服务发现功能@EnableDiscoveryClient
。
成果配置。我们接着去gateway配置中心的gateway.yml配置中心去配置网关,这里约定前端发的请求全部带上/api
前缀。
spring:
cloud:
gateway:
routes:
# 路由到何处
- id: admin-route
# url规则
uri: lb://mall-admin
# 当请求为/api/xxx的全部路由到上面配置的url
predicates:
- Path=/api/**
启动网关和重启前端项目,再次看看是否可以拿到验证码。
很不幸,还是404,这个时候我们需要使用网关的路径重写,使用gateway的filters
的路径重写功能。
filters:
- RewritePath=/api/?(?<segment>.*), /mall-admin/$\\segment
开始登录!登录后发现出现了跨域问题。
5.1.1.3、解决跨域问题
跨域指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。一般是使用同源策略进行限制。
同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域。
跨域请求流程:
我们可以发现,他只是发了一个option请求,真正的登录请求还没有发过去就被跨域拦截了。
解决办法:
- 使用nginx部署为同一域。
- 配置当次请求允许跨域:
-
Access-Control-Allow-Origin:支持哪些来源的请求跨域
-
Access-Control-Allow-Methods:支持哪些方法跨域
-
Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含 cookie
-
Access-Control-Expose-Headers:跨域请求暴露的字段
-
CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段: Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如 果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
-
Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无 须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果 该首部字段的值超过了最大有效时间,将不会生效。
每个请求都添加这么多请求头,太麻烦了,所以我们可以写一个filter,我们可以在网关写一个Filter,由网关统一进行跨域配置。我们在网关新建一个config目录用于存放配置类,新建一个MallConfiguration类用于解决跨域问题。
package cn.linstudy.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;
/**
* @author XiaoLin
* @date 2023/1/7
* @description 解决跨域问题配置类
*/
@Configuration
public class MallConfiguration
@Bean
public CorsWebFilter corsWebFilter()
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨域
// 允许哪些头进行跨域
corsConfiguration.addAllowedHeader("*");
// 允许哪些请求当时进行跨域
corsConfiguration.addAllowedMethod("*");
// 允许哪些请求来源进行跨域
corsConfiguration.addAllowedOrigin("*");
// 是否允许携带cookie进行跨域
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter(source);
启动后会发现仍然报错。
这是因为,脚手架项目也配置了跨域,我们需要把他原来的跨域配置给注释掉。
注释掉后重启mall-admin即可。
5.1.1.4、三级分类树形展示
我们接着写网关路由,即在nacos的网关配置中书写路由规则即可。
# 路由到何处
- id: product-route
# url规则
uri: lb://mall-product
# 当请求为/api/xxx的全部路由到上面配置的url
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/?(?<segment>.*), /$\\segment
启动后发现路由配置似乎没有生效。实际上是被上面的路由配置覆盖了。上面这条路由信息会先匹配上,所以会直接走上面的路由配置。我们可以调整一下顺序,把精确的路由放到高的优先级,模糊的路由放到低的优先级即可。
刷新配置,完成!数据成果拿到
接着开始显示数据,通过data.categoryEntityList
可以获取到后台接口传过来的数据。
这里即为展示数据的地方,我们把他改为menus
,修改的地方要和这里对应上即可。由官方文档我们可以看到,
因为我们的列表数据里面,标签名的属性是name,所以我们把label的值改为name即可展示数据。
5.1.1.4、菜单删除
我们可以使用Vue的slot功能。
可以看到只需要给Vue Tree里面写一个span标签即可。
<span class="custom-tree-node" slot-scope=" node, data ">
<span> node.label </span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)">
Append
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)">
Delete
</el-button>
</span>
</span>
放到el-tree这个标签里面即可。
再把原生的append和remove方法复制过来即可,清空实现,自己实现。
append(data)
,
remove(node, data)
,
我们可以发现,在点击append和remove的时候,菜单栏会展开和收缩,我们也需要把这个效果给去掉,参照文档,原来是这个属性在作怪,再把这个属性加到el-tree中。
:expand-on-click-node="false">
这样就去掉了这个烦人的效果,在点击按钮的时候就不会展开与合并了,只有点击箭头的时候才会。接下来就要开始判断啥时候展示append和remove菜单了,只有一级、二级菜单才可以添加,只有菜单下没有子菜单了以后才可以删除。
在slot插槽中有两个对象,node表示当前节点对象
我们打印一下node对象,可以发现一个好东西。
level表示当前的层级,是一级还是二级菜单。node.childNodes表示他的子节点
我们通过node.level
这个属性来判断是否是一级、二级菜单,node.level <= 2
表示为二级菜单。
我们在button中加上两个判断即可实现这个功能。
由于我们还需要做批量删除,所以我们还需要做上单选框,我们需要在el-tree上加入show-checkbox
这个属性。
由于我们使用的是MyBatis-Plus,我们需要配置一下MyBatis-Plus的逻辑删除。老规矩先去官方文档上瞅一眼:https://baomidou.com/pages/6b03c5/。
我们要去实体类字段上加上@TableLogic
注解。我们在CategoryEntity这个实体的showStatus字段上加入这个注解,但是发现一个问题,我们自己定义的规则是0表示不显示,1表示显示,别慌MyBatis-Plus会出手,我们点进去@TableLogic
注解,可以看到,我们可以自己配置删除和不删除的规则。
所以我们给这个注解上加一点东西。
/**
* 是否显示[0-不显示,1显示],@TableLogic代表这是逻辑删除字段
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
接下来我们可以开始去后台项目写逻辑删除接口。
/**
* RequestBody请求体,必须发post请求,SpringMVC会自动将请求体数据(JSON),转为对应的对象
* @param catIds 商品id
* @return
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds)
// 检查当前菜单是否被其他地方引用
categoryService.removeMenusByIds(Arrays.asList(catIds));
return R.ok();
去Service写接口。
/**
* 批量逻辑删除
* @param asList
*/
void removeMenusByIds(List<Long> asList);
接着去写实现类。
/**
* 用showStatus状态来完成逻辑删除
* @param asList id列表
*/
@Override
public void removeMenusByIds(List<Long> asList)
baseMapper.deleteBatchIds(asList);
后端接口写完以后,就去前端项目开始去测试。在category.vue中的remove方法去书写逻辑删除代码。为了便于开发方便,我们可以直配置好发送get、post请求模板。我们需要新建一个代码模板,不可以在原有的进行添加,否则会无法添加。
"http-get 请求":
"prefix": "httpget",
"body": [
"this.\\\\$http(",
"url: this.\\\\$http.adornUrl(''),",
"method: 'get',",
"params: this.\\\\$http.adornParams()",
").then((data) => ",
")"
],
"description": "httpGET 请求"
,
"http-post 请求":
"prefix": "httppost",
"body": [
"this.\\\\$http(",
"url: this.\\\\$http.adornUrl(''),",
"method: 'post',",
"data: this.\\\\$http.adornData(data, false)",
").then(( data ) => );"
],
"description": "httpPOST 请求"
接着我们去写remove方法。
remove(node, data)
var ids = [data.catId]
this.$http(
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false),
).then(( data ) =>
// 会重新发送请求,更新一次菜单数据
this.getMenus()
)
,
写到这里,删除大体功能就做完了,但是我们还需要一些细化细节:
- 删除前弹出提示框。
- 删除成功后有消息提示。
- 删除完后还是展开状态。
我们首先做删除前弹出提示框,提示框我们可以使用MessageBox弹框组件(https://element.eleme.cn/#/zh-CN/component/message-box)。
开搞!
this.$confirm('此操作将永久删除该文件, 是否继续?', '提示',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
).then(() =>
this.$message(
type: 'success',
message: '删除成功!'
);
).catch(() =>
this.$message(
type: 'info',
message: '已取消删除'
);
);
为了拿到删除菜单的名字,我们可以使用票号(`)和插值表达式来取值,带data中有一个name属性就是这个菜单的名字。
this.$confirm(`是否删除【$data.name】菜单, 是否继续?`, '提示',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
)
.then(() =>
this.$message(
type: 'success',
message: '删除成功!',
)
)
.catch(() =>
this.$message(
type: 'info',
message: '已取消删除',
)
)
接着我们改变一下代码顺序,点击确认删除后才发送删除请求即可。
remove(node, data)
var ids = [data.catId]
this.$confirm(`是否删除【$data.name】菜单, 是否继续?`, '提示',
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
).then(() =>
this.$http(
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids, false),
).then(( data ) =>
// 会重新发送请求,更新一次菜单数据
this.getMenus()
)
this.$message(
type: 'success',
message: '删除成功!',
)
)
成功是成功了,但是在点击取消的时候控制台报错了。
以上是关于Spring Cloud Alibaba商城实战项目基础篇(day03)的主要内容,如果未能解决你的问题,请参考以下文章
Spring Cloud Alibaba商城实战项目基础篇(day01)
Spring Cloud Alibaba商城实战项目基础篇(day03)
Spring Cloud Alibaba商城实战项目基础篇(day03)
五分钟带你玩转spring cloud alibaba越玩越溜!实战Spring Cloud Alibaba Sentinel
五分钟带你玩转spring cloud alibaba越玩越溜!实战Spring Cloud Alibaba Sentinel