谷粒商城-分布式基础【业务编写】

  1. 谷粒商城-分布式基础篇【环境准备】
  2. 谷粒商城-分布式基础【业务编写
  3. 谷粒商城-分布式高级篇【业务编写】持续更新
  4. 谷粒商城-分布式高级篇-ElasticSearch
  5. 谷粒商城-分布式高级篇-分布式锁与缓存
  6. 项目托管于gitee

一、三级分类

此处三级分类最起码得启动renren-fastnacosgatewayproduct

pms_category表说明

代表商品的分类

  • cat_id:分类id,cat代表分类,bigint(20)
  • name:分类名称
  • parent_cid:在哪个父目录下
  • cat_level:分类层级
  • show_status:是否显示,用于逻辑删除
  • sort:同层级同父目录下显示顺序
  • ico图标,product_unit商品计量单位,
  • InnoDB表,自增大小1437,utf编码,动态行格式
# 导入数据,在对应的数据库下执行资料里的 `pms_catelog.sql` 文件
# /Users/hgw/Documents/Data/Project/谷粒商城/1.分布式基础篇/docs/代码/sql/pms_catelog.sql

谷粒商城-分布式基础【业务编写】_第1张图片

1.1、业务编写 (查询、递归树形结构获取)


第一步、编写Controller层

在分类Controller层加上一个三级分类的业务

@RestController
@RequestMapping("product/category")
public class CategoryController {
    @Autowired
    private CategoryService categoryService;

    /**
     * 查处所有分类以及子分类,以树形结构组装起来
     */
    @RequestMapping("/list/tree")
    public R list(){
        List entities =  categoryService.listWithTree();

        return R.ok().put("data", entities);
    }
  //......
}

第二步、编写Service层

CategoryService 接口:

/**
 * 商品三级分类
 *
 * @author hgw
 * @email [email protected]
 * @date 2022-03-07 13:28:36
 */
public interface CategoryService extends IService<CategoryEntity> {

    PageUtils queryPage(Map<String, Object> params);

    List<CategoryEntity> listWithTree();
}

CategoryServiceImpl 实现类 :

谷粒商城-分布式基础【业务编写】_第2张图片

Stream 的 map()方法: 转换流数据返回, 当前流的泛型变为返回值的类型,
Stream 的 peek()方法: 修饰流数据, 无返回值

1.2、配置路由网关 与 路径重写 (实现三级分类查询操作)


启动 renren-fastnacosproduct 还有前端项目 renren-fast-vue

1.2.1、创建 菜单目录

创建一个一级目 : 商品系统

添加的这个菜单其实是添加到了guli-admin.sys_menu表里

(新增了memu_id=31 parent_id=0 name=商品系统 icon=editor )

在 商品系统 下创建一个菜单: 分类维护

guli-admin.sys_menu表又多了一行,父id是刚才的商品系统id

1.2.2、菜单路由


在左侧点击【商品系统-分类维护】,希望在此展示3级分类。可以看到

  • url是http://localhost:8001/#/product-category
  • 填写的菜单路由是 product/category
  • 对应的视图是 src/view/modules/product/category.vue

再如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue

所以要自定义我们的product/category视图的话,就是创建 mudules/product/category.vue

输入vue快捷生成模板,然后去https://element.eleme.cn/#/zh-CN/component/tree. 看如何使用多级目录

创建 mudules/product/category.vue





1.2.3、网关配置


第一步、修改Api接口请求地址

第一步、在 /static/config/index.js 文件中修改Api接口请求地址指向网关端口:88

在登录管理后台的时候,我们会发现,他要求localhost:8080/renrenfast/product/category/list/tree这个url, 但是报错404找不到,此处就解决登录页验证码不显示的问题。

他要给8080发请求读取数据,但是数据是在10000端口上,如果找到了这个请求改端口那改起来很麻烦。

  • 方法1: 是改vue项目里的全局配置,
  • 方法2: 是搭建个网关,让网关路由到10000(即将vue项目里的请求都给网关,网关经过url处理后,去nacos里找到管理后台的微服务,就可以找到对应的端口了,这样我们就无需管理端口,统一交给网关管理端口接口)
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
// 意思是说本vue项目中要请求的资源url都发给88/api,那么我们就让网关端口为88,然后匹配到/api请求即可,
// 网关可以通过过滤器处理url后指定给某个微服务
// renren-fast服务已经注册到了nacos中

谷粒商城-分布式基础【业务编写】_第3张图片

问题:他要去nacos中查找api服务,但是nacos里有的是fast服务,就通过网关过滤器把api改成fast服务

所以让fast注册到服务注册中心,这样请求88网关转发到8080fast

第二步、将fast注册到服务注册中心

第二步、将fast注册到服务注册中心,这样请求88网关转发到8080fast

  1. 在fast里加入注册中心的依赖

    
    		<dependency>
    			<groupId>com.alibaba.cloudgroupId>
    			<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
    			<version>2.1.0.RELEASEversion>
    		dependency>
    
    		
    		<dependency>
    			<groupId>com.alibaba.cloudgroupId>
    			<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
    			<version>2.1.0.RELEASEversion>
    		dependency>
    
  2. 在renren-fast项目中 src/main/resources/application.yml添加nacos配置

    spring:
      application:
        name: renren-fast	# 意思是把renren-fast项目也注册到nacos中(后面不再强调了),这样网关才能转发给
      cloud:
        nacos:
          discovery:
            server-addr: 127.0.0.1:8848 # nacos
            
    
  3. 然后在fast启动类上加上注解@EnableDiscoveryClient,重启

    @EnableDiscoveryClient
    @SpringBootApplication
    public class RenrenApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(RenrenApplication.class, args);
    	}
    
    }
    

    然后在nacos的服务列表里看到了renren-fast

问题解决:

  • 如果报错gson依赖,就导入google的gson依赖

  • 如果一直获取不到nacos信息, 则在resources路径下创建一个 bootstrap.properties

    spring.application.name=renren-fast
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    
第三步、添加网关

第三步、配置**gateway(网关)**模块中的application.yml文件, 添加网关

        - id: admin_route
          uri: lb://renren-fast # 路由给renren-fast (lb)负载均衡
          predicates:  # 什么情况下路由给它
            - Path=/api/**  # 默认前端项目都带上api前缀,就是我们前面题的localhost:88/api
          filters:
            - RewritePath=/api/(?>.*),/renren-fast/$\{segment}  # 把/api/* 改变成 /renren-fast/*fast找
  • lb代表负载均衡

修改过vue的api之后, 此时验证码请求的是 http://localhost:88/api/captcha.jpg?uuid=72b9da67-0130-4d1d-8dda-6bfe4b5f7935

也就是说, 他请求网关, 路由到了renren-fast , 然后去nacos里找fast.

找到后拼接成了: http://renren-fast:8080/api/captcha.jpg

但是正确的是: localhost:8080/renren-fast/captcha.jpg

所以要利用网关带路径重写, 参考https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-rewritepath-gatewayfilter-factory

照猫画虎,在网关里写了如上,把api换成renren-fast,

登录,还是报错:(出现了跨域的问题,就是说vue项目是8001端口,却要跳转到88端口,为了安全性,不可以)

:8001/#/login:1 Access to XMLHttpRequest at ‘http://localhost:88/api/sys/login’ from origin ‘http://localhost:8001’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求。具体来说当前页面是8001端口,但是要跳转88端口,这是不可以的(post请求json可以)

问题描述:已拦截跨源请求:同源策略禁止8001端口页面读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。

问题分析:这是一种跨域问题。访问的域名或端口和原来请求的域名端口一旦不同,请求就会被限制

第四步、网关统一配置跨域

第四步、网关统一配置跨域

解决方法:在网关中定义“GulimallCorsConfiguration”类,该类用来做过滤,允许所有的请求跨域。

package com.hgw.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;

/**
 * Data time:2022/3/14 21:17
 * StudentID:2019112118
 * Author:hgw
 * Description: 配置跨域,该类用来做过滤,允许所有的请求跨域。
 */
@Configuration
public class GulimallCorsConfiguration {

    @Bean		// 添加过滤器
    public CorsWebFilter corsWebFilter() {
        // 基于url跨域,选择reactive包下的
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

        // 配置跨域信息
        CorsConfiguration configuration = new CorsConfiguration();
        // 允许跨域的头 *:表示所有
        configuration.addAllowedHeader("*");
        // 允许跨域的请求方式
        configuration.addAllowedMethod("*");
        // 允许跨域的请求来源
        configuration.addAllowedOrigin("*");
        // 是否允许携带cookie跨域
        configuration.setAllowCredentials(true);

        // `/**` :任意url都要进行跨域配置
        source.registerCorsConfiguration("/**",configuration);
        return new CorsWebFilter(source);
    }
}

再次访问:http://localhost:8001/#/login

已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。

(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)

renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

出现了多个请求,并且也存在多个跨源请求。因为在renren-fast项目下有过滤器 .

为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。

第五步、Product 请求路径重写

之前解决了登陆验证码的问题, /api/请求重写成了/renren-fast, 但是vue项目中或者你自己写的数据库中有些是以/product为前缀的, 它要请求 product微服务, 这里也会让它请求renren-fast 显然是不合适的.

  • 解决办法是把请求在网关中以更小的范围先拦截一下,剩下的请求再交给renren-fast

在显示商品系统/分类信息的时候,出现了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异常。

  • 原本请求: http://localhost:88/api/product/category/list/tree
  • 映射请求: http://localhost:8001/renren-fast/product/category/list/tree
  • 真实请求: http://localhost:10000/product/category/list/tree
1.2.3.5.1、将 gulimall-product 加入到注册中心nacos
  1. 首先将 gulimall-product 加入到注册中心nacos

修改: 在product项目的application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://124.222.223.222:3306/gulimall_pms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-product

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      # 设置表主键自增
      id-type: auto

server:
  port: 10000

如果要使用nacos配置中心,可以这么做

  1. 在nacos中新建命名空间,用命名空间隔离项目,(可以在其中新建gulimall-product.yml)

  2. 在product项目中新建bootstrap.properties并配置

    spring.application.name=gulimall-product
    spring.cloud.nacos.config.server-addr=127.0.0.1:8848
    spring.cloud.nacos.config.namespace=502fa214-0e44-47d4-91c4-2d4589720c76
    

为了让product注册到主类上加上注解@EnableDiscoveryClient

1.2.3.5.2、定义路由规则, 进行路径重写
  1. 定义路由规则, 进行路径重写

修改 gulimall-gatewayapplication.yml 文件, 在后面加上以下路由规则

        - id: product_route
          uri: lb://gulimall-product  # 注册中心的服务
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?>.*),/$\{segment}	# 将/api/替换为空

此时 访问 localhost:88/api/product/category/list/tree invalid token,非法令牌,后台管理系统中没有登录,所以没有带令牌

谷粒商城-分布式基础【业务编写】_第4张图片

原因:先匹配的先路由,fast和product路由重叠,fast要求登录

修正:在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。

spring:
  cloud:
    gateway:
      routes:
        - id: product_route
          uri: lb://gulimall-product  # 注册中心的服务
          predicates:
            - Path=/api/product/**
          filters:
            - RewritePath=/api/(?>.*),/$\{segment}

        - id: admin_route
          uri: lb://renren-fast # 路由给renren-fast (lb)负载均衡
          predicates:  # 什么情况下路由给它
            - Path=/api/**  # 默认前端项目都带上api前缀,就是我们前面题的localhost:88/api
          filters:
            - RewritePath=/api/(?>.*),/renren-fast/$\{segment}  # 把/api/* 改变成 /renren-fast/*fast找

此时请求已可请求到数据!

补充: 跨域问题

跨域概括

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS

  • 跨域: 指的是浏览器不能执行其他网站的脚本. 它是由浏览器的同源策略造成的, 是浏览器对js施加的安全措施. (ajax可以)
  • 同源策略: 是指 协议、域名、端口 都要相同, 其中有一个不同都会产生跨域
URL 说明 是否允许通信
http://www.a.com/a.js
http://www.a.com/b.js
同一域名下 允许
http://www.a.com/lab/a.js
http://www.a.com/script/b.js
同一域名下不同文件夹 允许
http://www.a.com:8000/a.js
http://www.a.com/b.js
同一域名,不同端口 不允许
http://www.a.com/a.js
https://www.a.com/b.js
同一域名,不同协议 不允许
http://www.a.com/a.js
http://70.32.92.74/b.js
域名和域名对应ip 不允许
http://www.a.com/a.js
http://script.a.com/b.js
主域相同,子域不同 不允许
http://www.a.com/a.js
http://a.com/b.js
同一域名,不同二级域名(同上) 不允许(cookie这种情况下也不允许访问)
http://www.cnblogs.com/a.js
http://www.a.com/b.js
不同域名 不允许
跨域流程

跨域流程

这个跨域请求的实现是通过预检请求实现的, 发送一个OPSTIONS探路, 收到响应允许跨域后再发送真实请求

什么意思呢?

  • 跨域是要请求的、新的端口那个服务器限制的, 不是浏览器限制的

跨域请求流程: 非简单请求(PUT、DELETE)等,需要先发送预检请求

谷粒商城-分布式基础【业务编写】_第5张图片

跨域的解决方案

跨域的解决方案

  • 方法一: 使用Nginx部署为同一域
  • 方法二: 让服务器告诉预检请求能跨域
  1. 方法一: 使用Nginx部署为同一域
    设置Nginx包含admin 和 gateway. 都先请求nginx, 这样端口就统一了
    谷粒商城-分布式基础【业务编写】_第6张图片

  2. 方法二: 配置当次请求允许跨域
    在响应头中添加:参考:https://blog.csdn.net/qq_38128179/article/details/84956552

    • Access-Control-Allow-Origin : 支持哪些来源的请求跨域
    • Access-Control-Allow-Method : 支持那些方法跨域
    • 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 :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效

1.2.4、三级分类-查询-树形展示三级分类数据


接着修改前端category.vue,这里改的是点击分类维护后的右侧显示

  1. data解构,加上{},取出我们想要的数据
//方法集合
  methods: {
    handleNodeClick(data) {
      console.log(data);
    },
    getMenus() {
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log("成功获取到菜单数据", data.data);
        this.menus = data.data;
      });
    },
  },
  1. 此时有了3级结构,但是没有数据,在category.vue的模板中,数据是menus,而还有一个props。这是element-ui的规则,
<template>
  <el-tree
    :data="menus"
    :props="defaultProps"
    @node-click="handleNodeClick"
  ></el-tree>
</template>

而在data中
	defaultProps: {
        children: "children",
        label: "name"
      }

整个代码 :





1.3、三级分类 增删改操作


1.3.1、三级分类 [删除]


1.3.1.1、实现页面效果

这里采用ElementUI 的自定义节点内容 的 scoped slot 方式来实现 ElementUI组件

1.3.1.1.1、[效果一]: 实现增加、删除的效果, 点击节点的时候展开或者收缩节点


export default {
    append(data) {
      console.log("append", data);
    },

    remove(node, data) {
      console.log("remove", node, data);
    },
  },
}
参数 说明 类型 可选值 默认值
expand-on-click-node 是否在点击节点的时候展开或者收缩节点,
默认值为 true,
如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
boolean true
  • :expand-on-click-node="false" : 即设置为在点击节点的时候展开或者收缩节点
1.3.1.1.2、[效果二]: 实现在规定的地方显示 增删按钮
  • 没有子节点的时候才显示 Delete按钮
    • 解决: v-if="node.level <= 2"
  • 只有一级菜单和二级菜单才显示 Append按钮
    • 解决: v-if="node.childNodes.length == 0"
        Append
        Delete
1.3.1.1.3、[效果三]: 实现多选
<el-tree
    :data="menus"
    :props="defaultProps"
    :expand-on-click-node="false"
    show-checkbox
    node-key="catId"
  >
      
		//......
      
</el-tree>
参数 说明 类型 可选值 默认值
show-checkbox 节点是否可被选择 boolean false
node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 String
1.3.1.2、逻辑删除

这里使用MyBatis-Plus的逻辑删除 官网使用方法

逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。在表中应当编写一个字段标记是否被删除. 在进行删除的时候并不是执行delete命令, 而是执行update命令 , 如下 :

update user set deleted=1 where id = 1 and deleted=0
  • 1、配置全局的逻辑删除规则(可省略)

  • 2、配置逻辑删除的组件Bean(mybatis-plus3之后可省略)

  • 3、实体类字段上加上@TableLogic注解

第一步、配置 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)

第二步、给product.entity路径下的 CategoryEntity类的 showStatus属性加上注解

/**
	 * 是否显示[0-不显示,1显示]
	 */
	@TableLogic(value = "1",delval = "0")
	private Integer showStatus;

表中

  • 1 显示的是 删除
  • 0 显示的是 不删除

和全局配置是反的, 这里通过 @TableLogic(value = "1",delval = "0") 配置自己的规则 !

  • String value() default "" : 默认逻辑未删除值 (该值可无、会自动获取全局配置)
  • String delval() default "" : 默认逻辑删除值 (该值可无、会自动获取全局配置)

故前面配置 application.yml 全局的逻辑删除规则并没有做效, 而是 1(未删除), 0(删除)

第三步、修改Controller层

/**
     * 删除
     * @RequestBody: 获取请求体,必须发送POST请求
     * SpringMVC自动将请求体的数据(json),转为对应的对象
     */
    @RequestMapping("/delete")
    // @RequiresPermissions("product:category:delete")
    public R delete(@RequestBody Long[] catIds){
        // 1、检查当前删除的菜单,是否被别的地方引用
		// categoryService.removeByIds(Arrays.asList(catIds));

		categoryService.removeMenuByIds(Arrays.asList(catIds));
        return R.ok();
    }

第四步、修改Service层

接口 :

public interface CategoryService extends IService<CategoryEntity> {

    PageUtils queryPage(Map<String, Object> params);

    List<CategoryEntity> listWithTree();

  	// 加上删除方法
    void removeMenuByIds(List<Long> asList);
}

实现类 :

@Override
public void removeMenuByIds(List<Long> asList) {
    //TODO 1、检查当前删除的菜单,是否被别的地方引用

    // 逻辑删除
    baseMapper.deleteBatchIds(asList);
}

这里留下了一个待完成事项: 等以后业务来完成

  • //TODO 注释内容
    • todo默认不区分大小写,todo、Todo、ToDO、TODO都是可以的。也可以修改为区分。
    • todo后面必须要使用一个空格隔开注释内容。
  • 我们在某个地方加上了todo注释之后,我们可以通过任务列表快速定位到某个todo注释位置

谷粒商城-分布式基础【业务编写】_第7张图片

1.3.1.3、删除效果细化

效果一: 实现逻辑删除功能

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.$message({
              message: "菜单删除成功",
              type: "success",
            });
            // 刷新出新的菜单
            this.getMenus();
            // 设置需要默认展开的菜单
            this.expandedKey = [node.parent.data.catId];
          });
        })
        .catch(() => {
          this.$message("取消删除");
        });

      console.log("remove", node, data);
    },
  • 点击delete按钮弹出提示框
    • 确定: 向 /product/category/delete 发出post请求, 并带着请求体 data.catId(当前菜单的id)
      • then :
        • 则删除成功, 弹出提示框.
        • 并刷新出新的菜单.
        • :default-expanded-keys="expandedKey" 修改动态绑定expandedKey数组的值为当前删除菜单的母菜单id, 从而实现删除后默认展开删除的菜单
      • catch :
        • 取消删除, 弹出提示框
参数 说明 类型 可选值 默认值
default-expanded-keys 默认展开的节点的 key 的数组 array

全部代码附上:





1.3.2、三级分类[新增]


需求一: 点击append 按钮之后, 弹出一个对话框输入子分类的信息