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请求,真正的登录请求还没有发过去就被跨域拦截了。

解决办法:

  1. 使用nginx部署为同一域。
  2. 配置当次请求允许跨域:
  • 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()
      )
    ,

写到这里,删除大体功能就做完了,但是我们还需要一些细化细节:

  1. 删除前弹出提示框。
  2. 删除成功后有消息提示。
  3. 删除完后还是展开状态。

我们首先做删除前弹出提示框,提示框我们可以使用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

springcloud 微服务Spring Cloud Alibaba 整合Nacos实战