谷粒商城基础篇笔记

1.微服务

​ 微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是HTTP API。这些服务围绕业务能力来构建。并通过完全自动化部署机制来独立部署,这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。简而言之:拒绝大型单体应用,基于业务边界进行服务微化拆分,各个服务独立部署运行。

2.集群&分布式&节点

​ 集群是个物理形态,分布式是个工作方式。

​ 只要是一堆机器,就可以叫集群,他们是不是一起协作着干活,这个谁也不知道:

​ <<分布式系统原理与泛型>>定义:

​ “分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”

​ 分布式系统是建立在网络之上的软件系统。

​ 分布式是指将不同的业务分布在不同的地方。

​ 集群指的是将几台服务器集中在一起,实现同一业务。

3.远程调用

​ 在分布式系统中,各个服务可能处于不同主机。但是服务之间不可避免的需要互相调用,我们称为远程调用。

​ SpringCloud中使用HTTP+JSON的方式完成远程调用。

4.负载均衡

​ 分布式系统中,A服务需要调用B服务,B服务在多台机器中都存在,A调用任意一个服务器均可完成功能。

​ 为了使每一个服务器都不要太忙或者太闲,我们可以负载均衡的调用每一个服务器,提升网站的健壮性。

​ 常见的负载均衡算法:

​ 轮询:为第一个请求选择健康池中的第一个后端服务器,然后按顺序往后一次选择,直到最后一个,然后循环。

​ 最小链接。优先选择连接数最少,也就是压力最小的后端服务器,在会话较长的情况下可以考虑采取这种方式。

​ 散列:略

5.服务注册/发现&注册中心

​ A服务调用B服务,A服务并不知道B服务当前在哪几台服务器有,哪些正常的,哪些服务已经下线。解决这个问题可以引入注册中心,如果某些服务器下线,可以被实时感知到。

6.配置中心

​ 每一个服务最终都有大量的配置,并且每个服务器都可能部署在多台机器上。我们经常需要变更配置,我们可以让每个服务在配置中心获取自己的配置。使用配置中心来集中管理微服务的配置信息。

7.服务熔断&服务降级

​ 在微服务架构中,微服务之间通过网络进行通信,存在互相依赖,当其中一个服务不可用时,有可能造成雪崩效应。要防止这样的情况,必须要有熔断机制来保护服务。

8.API网关

​ 在微服务架构中,API 网关座位整体架构的重要组件,它抽象了微服务中都需要的公共功能,同时提供了客户端客户端负载均衡,服务自动熔断,灰度发布,统一认证,限流流控,日志统计等丰富的功能,帮助我们解决很多API管理难题。

Docket

​ 虚拟化容器技术、Docket基于镜像,可以秒级启动各种容器。每一种容器都是一个完整的运行环境,容器之间互相隔离。
安装与启动docker:

删除历史存留:
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

安装所需的包,yum-utils提供了yum-config-manager效用
sudo yum install -y yum-utils

设置稳定存储库
$ sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

安装docker:
yum install docker

启动docker
sudo systemctl start docker


​设置阿里镜像(centos,可在阿里云官网找)
sudo mkdir -p /etc/docker

sudo tee /etc/docker/daemon.json <<-'EOF'
{
  "registry-mirrors": ["https://ywpuv5yo.mirror.aliyuncs.com"]
}
EOF

重新加载配置文件使之生效:
sudo systemctl daemon-reload

重启docker:
sudo systemctl restart docker

开机自动启动docker:
systemctl enable docker

查看docker镜像:
docker images

启动docker容器(容器之间是相互隔离的,每一个容器都是完整的运行环境):

docker run xxx

docker安装mysql:

​ 1.从镜像仓库拉取镜像:docker pull mysql:5.7

​ 2.通过mysql镜像启动mysql:

centos 7下:
docker run -p 3306:3306 --privileged=true  --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7

centos8下:
docker run -p 3306:3306   --name mysql \
-v /mydata/mysql/log:/var/log/mysql \
-v /mydata/mysql/data:/var/lib/mysql \
-v /mydata/mysql/conf:/etc/mysql \
-e MYSQL_ROOT_PASSWORD=root \
-d mysql:5.7


-v是目录挂载的意思: 可以将容器内部经常要使用的目录映射到linx中。
如/mydata/mysql/log映射了容器中的/var/log/mysql。
--privileged=true是mysql容器中/var/lib/mysql/没有权限时使用。

​ 3.安装好后可以通过docker ps查看启动的镜像

验证每个容器都是完整的环境

进入mysql容器的环境:docker exec -it mysql /bin/bash
-it是以交互环境的意思
之后就进入到了mysql的linux运行环境
whereis mysql可以看到mysql被安装到了哪个目录

exit退出容器 

​ 4.修改mysql配置

vi /mydata/mysql/conf/my.cnf

[client]
default-character-set=utf8


[mysql]
default-character-set=utf8


[mysqld]
init_connect='SET collation_connection = utf8_unicode_ci'
init_connect='SET NAMES utf8'
character-set-server=utf8
collation-server=utf8_unicode_ci
skip-character-set-client-handshake
skip-name-resolve

5.重启mysql容器

docker restart mysql

测试:
	docker exec -ti 2cbb0f246353 /bin/bash
登录mysql:
	mysql -u root -p
修改root 可以通过任何客户端连接:
	ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
刷新:
	flush privileges;

安装redis:

​ 1.拉取镜像(最新)

docker pull redis

​ 2.创建实例并启动

mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf

centos7:
docker run -p 6379:6379 --privileged=true  --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf

centos8:
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf

​ 3.测试

docker exec -it redis redis-cli

​ 4.编辑redis.conf,添加内容(采用aof的持久化方式):

appendonly yes

​ docker查看所有的容器:docker ps -a

​ docker自动启动redis容器:docker update redis --restart=always

​ docker自动启动mysql容器:docker update mysql --restart=always

环境搭建:

​ idea插件:Lombok、mybatisX

​ jdk设置环境变量:

​ 1.系统变量→新建 JAVA_HOME 变量 。变量值填写jdk的安装目录(本人是 D:\jdk).

​ 2.系统变量→寻找 Path 变量→编辑,在变量值最后输入 %JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;

​ 3.系统变量→新建 CLASSPATH 变量,变量值填写 .;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar(注意最前面有一点)

​ vscode插件:Auto Close Tag、Auto Rename Tag,简体中文包、HTML CSS Support、HTML Snippets、JavaScript (ES6) code snippets、Live Server、open in browser、Vetur

​ 安装git客户端

​ 配置git,进入git bash

​ 1.#配置用户名

​ git config --global user.name “username”

​ 2.#配置邮箱

​ git config --global user.email “xxx@qq.com” //注册时的邮箱

​ 3.配置ssh免密登录

​ 进入 git bash:使用 : ssh-keygen -t rsa -C "xxxx@xxxxx.com"命令。连续三次回车。

​ 一般用户目录下会有id_rsa/id_rsa.pub 或者cat ~/.ssh/id_rsa.pub

​ 登录进入gitee,在设置里面找到SSH KEY将.pub文件的内容黏贴进去

​ 使用ssh -T git@gitee.com测试是否成功即可

​ 码云创建仓库,语言选java、模板选maven、密钥选apache2.0

​ idea安装git插件(百度)

​ 从git里面把项目拉取下来

git

搭建项目

​ 创建各种服务,选必要的组件springWeb和openFegin:

​ 1.商品服务mall-produc

​ 2.仓储服务mall-ware

​ 3.订单服务mall-order

​ 4.优惠卷服务mall-coupon

​ 5.用户服务mall-member

​ 创建好后设置忽略提交内容:

**/mvnw
**/mvnw.cmd
**/.mvn
**/target/
.idea
**/.gitignore

​ 安装码云插件,调处控制台窗口:setting->Version Control->Commit->取消勾选Use non-modal commit interface

​ 克隆人人开源/renren-fast:git clone https://gitee.com/renrenio/renren-fast.git,删除项目中的git文件放到自己的项目中,

​ 导入后更改dev中的mysql配置。

​ 启动时报错:

​ 1.安装Lombok插件.

​ 2.修改mysql时区show variables like ‘%time_zone%’;

查看当前时区:show variables like ‘%time_zone%’;
设置全局时区:set global time_zone = ‘+8:00’;
设置东八区:set time_zone = ‘+8:00’;
刷新当前时区:flush privileges;
再次查看当前时区:show variables like ‘%time_zone%’;

​ 3.连接的url上加上useSSL=false

​ 4.项目启动成功,访问http://127.0.0.1:8080/renren-fast/

克隆人人开源/renren-fast-vue: git clone https://gitee.com/renrenio/renren-fast-vue.git,用vscode打开,需提前安装好node,执行npm install命令安装依赖的组件,需要组件在package.json里面配置,组件都会安装到node_modules里面。

​ npm install失败:

# 查看镜像地址是否为国内淘宝镜像
npm config get registry

# 设置为淘宝镜像
npm install -g cnpm --registry=https://registry.npm.taobao.org

#设置权限 (管理员打开cmd),输入set-ExecutionPolicy RemoteSigned 选择A

#用cnpm安装
cnpm install

#还是不行,因为电脑装了python3,环境要求python2,所以改为python2
npm install --global --production windows-build-tools
    或
npm install --python=python2.7     #先下载
npm config set python python2.7   #再设置环境

#再次尝试还是不行,这里直接说了node sass不支持当前环境,所以可以直接删掉原来不支持本机的node sass,再重新安装就行了
 删除:
    cnpm uninstall --save node-sass(管理员身份)
 安装:
    cnpm install --save node-sass(管理员身份)

​ 克隆人人开源/renren-generator: git clone https://gitee.com/renrenio/renren-generator.git,删除.git文件后放入项目里面。

​ 修改application.yml中的数据库名称、用户名和密码,和端口号。

​ 修改generator.properties,为每个子模块生成代码:

mainPath=com.doushabao
#包名
package=com.doushabao.mall
moduleName=product
#作者
author=jerry
#Email
email=1912614962@qq.com
#表前缀(类名不会包含表前缀)
tablePrefix=pms_

​ 运行RenrenApplication,访问网站生成代码,将生成的main目录覆盖项目子模块的main目录。

​ 创建公共子模块mall-common,来放一些项目的公共依赖等。其他的所有子服务都添加common模块的依赖。

​ common添加各个模块所需的依赖:

    <!--mybatis-plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>
        <!--R工具类需要的httpStatus-->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.12</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>${commons.lang.version}</version>
        </dependency>

​ 注释掉renren-generator项目中resource/template/Controller.java.vm中的所有 @RequiresPermissions(“ m o d u l e N a m e : {moduleName}: moduleName:{pathName}:info”)注解,这是shiro的注解,之后会使用springSecurity,

​ 删除import org.apache.shiro.authz.annotation.RequiresPermissions;

​ 重新生成代码,替换掉之前的controller相关。

​ 再common项目中的com.doushabao.common包下新建包utils,把Constant、PageUtils、Query、R放入其中。

​ 把renren-fast中的xss放入其中,renren-fast中的exception放入其中。

测试逆向工程生成的代码:

1.common里面导入

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>
 <dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>servlet-api</artifactId>
    <version>2.5</version>
    <scope>provided</scope>
</dependency>

2.在mall-product的resource目录下新建application.yml文件,在里面配置数据源相关信息

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.6.128:3306/mall_pms?useSSL=false
    driver-class-name: com.mysql.cj.jdbc.Driver

3.在product模块的主启动类上加上@MapperScan(“com.doushabao.mall.product.dao”)注解扫描mapper接口

4.告诉mybatis-plus,sql的映射文件在哪和自增主键,application.yml文件里面配置数据源相关信息

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto

之后就可以写测试接口进行测试了

之后依次为其他子模块生成代码,创建application.yml文件设置数据源和对应的配置并测试成功就行了。

微服务-注册中心、配置中心、网关

​ https://github.com/alibaba/spring-cloud-alibaba

​ https://github.com/alibaba/spring-cloud-alibaba/blob/2022.x/README-zh.md

​ 项目中的微服务组件都不适用springcloud提供的原生组件,而使用SpringCloudAlibaba的组件

​ SpringCloud的几大痛点

​ 部分组件停止维护和更新,给开发带来不便;

​ 部分环境搭建复杂,没有完善的可视化界面,我们需要大量的二次开发和定制

​ 配置复杂,难以上手,部分配置差别难以区分和合理应用

​ SpringCloud Alibaba的优势

​ 阿里使用过的组件经历了考验,性能强悍,设计合理,搭配简单,学习曲线低。

​ 项目最终的搭配方案:

​ SpringCloud Alibaba-Nacos:注册中心(服务发现/注册)

​ SpringCloud Alibaba-Nacos:配置中心(动态配置管理)

​ SpringCloud-Ribbon:负载均衡

​ SpringCloud-Fegin:声明式HTTP客户端(调用远程服务)

​ SpringCloud Alibaba-Sentinel:服务容错(限流、降级、熔断)

​ SpringCloud-Gateway:API网关(webflux编程模式)

​ SpringCloud-Sleuth:调用链监控

​ SpringCloud Alibaba-Seata:原Fescar,即分布式事务解决方案

在common项目的pom文件中设置依赖管理:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <!--SpringCloud Alibaba-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

使用注册中心Nacos:

​ 1.在common项目中导入依赖:

<dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>

​ 2.官网中下载注册中心nacos-server解压并启动注册中心:startup.cmd -m standalone

​ 3.将每个微服务注册到注册中心,还需为服务自身取个名字才能被注册到注册中心,每个服务下添加配置:

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: mall-coupon

​ 4.主启动类中加上注解:@EnableDiscoveryClient

使用Feign声明式远程调用

​ Feign是一个声明式的HTTP客户端,它的目的就是让远程调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。

​ Feign整合了Ribbon(负载均衡)和Hystrix(服务熔断),可以让我们不再需要显示地使用这两个组件。

​ SpringCloudFeign在NetflixFeign的基础上扩展了对SpringMVC注解的支持,在其实现下。我们只需创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定。简化了SpringCloudRibbon自行封装服务调用客户端的开发量。

​ 使用:

​ 1.引入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

​ 2.编写一个接口,告诉springcloud这个接口需要调用远程服务

​ 声明接口的每一个方法都是调用哪个远程服务的那个请求

@FeignClient("mall-coupon")
public interface CouponFeignService {

    @RequestMapping("/coupon/coupon/member/list")
    public R membercoupons();
}

​ 3.开启远程调用功能:

​ 在主启动类上加上@EnableFeignClients注解。

​ 使用时需要注意Spring、SpringCloud、SpringCloud Alibaba的版本对应关系。

​ 服务掉线的话是不能被调用的。

使用配置中心Nacos Config

​ 1.首先,修改common里面的 pom.xml 文件,引入 Nacos Config Starter。

 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
 </dependency>

​ 2.在应用的 /src/main/resources/bootstrap.properties(springboot里面规定的,会优先于application.yml加载) 配置文件中配置 Nacos Config 元数据

 spring.application.name=mall-coupon(依次填自服务的名字)
 spring.cloud.nacos.config.server-addr=127.0.0.1:8848(与注册中心一致)

​ 3.在配置中心新建配置:mall-coupon.properties进行配置

​ 4.测试,在主启动类上加上@RefreshScope注解

@Value("${coupons.user.name}")
private String name;
@Value("${coupons.user.age}")
private Integer age;

@RequestMapping("/test")
public R test() {
    return R.ok().put("name",name).put("age",age);
}


@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
public class MallCouponApplication {

    public static void main(String[] args) {
        SpringApplication.run(MallCouponApplication.class, args);
    }

}

​ 注:新版本的SpringCloud需要依赖:

<dependency>
 <groupId>org.springframework.cloud</groupId>
 <artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>

​ nacos配置中心概念:

​ 命名空间

​ 用于做配置隔离的。

​ 默认:public(保留空间);默认新增的所有配置都在public空间下。

​ 1.比如开发、测试、生产等不同环境可以建立不同的命名空间进行隔离。手动创建dev、test、pro三个不同环境的命名空间

​ 手动选择使用配置:项目的bootstrap.properties文件中添加配置:

​ spring.cloud.nacos.config.namespace=6a5286dc-a376-47c9-84bd-a5f7d71cc1c5

​ 6a5286dc-a376-47c9-84bd-a5f7d71cc1c5是配置环境的uuid

​ 2.每一个微服务之间互相隔离配置,每一个微服务都创建自己的命名空间,只加载自己命名空间下的所有配置,

​ 创建优惠系统的命名空间coupon、创建成员系统的命名空间member

​ 不同微服务下的bootstrap.properties文件中添加要使用的配置:

​ spring.cloud.nacos.config.namespace=f302e15f-13c2-4679-84c4-d452969c1f97

​ 配置集

​ 所有配置的集合

​ 配置集ID

​ Data ID,类似于配置文件名

​ 配置分组

​ 默认所有的配置集都属于:DEFAULT_GROUP;

​ 可以自己定制不同的配置组,创建配置的时候可以输入group,不同组下的配置集ID可以相同(配置文件名相同)

​ 指定使用的配置组:在bootstrap.properties文件中添加:spring.cloud.nacos.config.group=1111

​ 不指定配置组都是使用默认配置组中的配置文件

​ 正常开发配置流程:

​ 先为每一个服务创建不同的命名空间,然后通过分组来区分dev、test、prod等不同的环境。

​ 从配置中心中同时加载多个配置集:

​ 开发中不会把所有的配置都写到一个配置文件中,因为可能造成一个配置文件中的配置很庞大。

​ 一般通过业务把配置区分开来。

​ 数据源相关的配置:datasource.yml

​ mybatis相关的配置:mybatis.yml

​ 其他配置:other.yml

​ 在bootstrap.properties文件中添加:

spring.cloud.nacos.config.ext-config[0].data-id=datasource.yml
spring.cloud.nacos.config.ext-config[0].group=dev
spring.cloud.nacos.config.ext-config[0].refresh=true

spring.cloud.nacos.config.ext-config[1].data-id=mybatis.yml
spring.cloud.nacos.config.ext-config[1].group=dev
spring.cloud.nacos.config.ext-config[1].refresh=true

spring.cloud.nacos.config.ext-config[2].data-id=other.yml
spring.cloud.nacos.config.ext-config[2].group=dev
spring.cloud.nacos.config.ext-config[2].refresh=true

​ 注释application.yml的配置内容,测试配置中心的配置是否生效。可以看到项目启动的时候读取了哪些配置,还有一个不用配置也默认会读取的配置:服务名.properties。

网关

​ 网关作为流量的入口,常用功能包括路由转发、权限校验、限流控制等。而springcloud gateway作为SpringCloud官方推出的第二代网关框架,取代了Zuul网关。

​ 1.创建新的项目作为API网关,添加SpringGateway依赖。在主启动类中添加@EnableDiscoveryClient注解

​ 2.nacos新建命名空间gateway并新建配置文件mall-gateway.yml,指定组为dev

​ 3.新建bootstrap.properties文件,指定项目的配置中心:

spring.application.name=mall-gateway
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=dbad32f6-25e5-4ced-b511-d7e7e4c904f1
spring.cloud.nacos.config.group=dev

​ 4.application.properties添加配置:

spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=gateway
server.port=88

​ 5.新建application.yml文件用于配置测试,后面可以改到配置中心中来使用配置。

​ 6.测试:http://127.0.0.1:88/?url=qq

ES6新特性

1.let和var

​ var声明的变量会越域,let声明的变量有严格的作用域。

​ var可以多次声明变量,let只能一次声明变量。

​ var会提升变量,先使用后定义使用的是undifined类型的变量,let不会提升变量,先使用后定义会报错:变量没有定义。

2.const声明只读变量(常量)

3.解构表达式

​ 数组结构:

let arr = [1,2,3]
let [a,b,c] = arr;        意思是把数组里的三个值赋值给a,b,c三个变量。

​ 对象结构:

const person = {
    name : 'jack',
    age : 21,
    language: ['java','js','css']
}
const {name,age,language} = person;            //把person.name赋值给name......

4.字符串扩展

​ 新增了 startsWith(),endWith(),includes() 几个方法

​ 声明一段很长的字符串可以使用``包裹字符串而不用拼串

​ 字符串插入变量和表达式。变量名写在 中 , {}中, {}中可以放入JavaScript表达式:我是${name},今年${age}岁了

5.1函数默认参数:

function add(a,b = 1) {
    return a + b;
}

5.2函数使用不定参数

function fun(...values) {
    console.log(values.length)
}

5.3箭头函数

​ 可以和结构表达式一起使用

6.对象优化

​ 6.1新增方法:

​ Object.keys(person):获取对象中的所有的key

​ Object.values(person):获取对象中的所有的value

​ Object.entries(person):获取对象的key-value

​ Object.assign(target,source1,source2);将source1、source2、…等对象的值拷贝给target对象

​ 6.2声明对象简写:

const age = 23;
const name = "张三";
const person = {name: name,age: age};        //传统的写法
const person2 = {name,age};                     //es6的写法,变量名和值通同名时可以简写
let person3 = {
    name: "jack",
    //以前
    eat: function(food) {
        console.log(this.name + "在吃" + food);
    },
    //es6箭头函数
    eat2:(food) => console.log(this.name + "在吃" + food),        //这里this.name获取不到
    //es6简写
    eat3(food) {
        console.log(this.name + "在吃" + food);
    }
}

​ 6.3对象拓展运算符:

​ 拓展运算符(…)用于取出参数对象所有可遍历属性任何拷贝到当前对象。

1.拷贝对象(深拷贝)
let person  = {name: "Amy",age: 15};
let someone = {...person1}
console.log(someone);

2.合并对象
let age = {age: 15}
let name = {name: "Amy"}
let person2 = {name: "zhangsan"}
person2 = {...age, ...name}    //如果两个对象得字段名重复,后面对象字段值会覆盖前面对象的字段值
console.log(person2);

7.数组新增

​ 新增map和reduce方法

map方法:接受一个函数,将原数组中所有元素用这个函数处理后放入新数组返回
let arr = ['1','20','-5','3'];
arr = arr.map((item) => {
    return item * 2;
});
console.log(arr);

reduce方法,reduce的回调有四个参数
1.previousValue:上一次调用回调返回的值,或者时提供的初始值
2.currentValue:数组中当前被处理的元素
3.index:当前元素在数组中的索引
4.array:调用reduce的数组
let result = arr.reduce((a,b) => {
    console.log("上一次处理后:" + a);
    console.log("当前正在处理:" + b);
    return a + b;
},100);
console.log(result);

8.Promise

​ 在JavaScript的世界中,所有代码都是单线程执行的。由于这个"缺陷",导致JavaScript的所有网络操作,浏览器时间,都必须时异步执行。异步执行可以用回调函数实现。一旦有一连串的ajax请求a,b,c,d… 后面的请求依赖前面的请求结果,就需要层层嵌套。这种缩进和层层嵌套的方式,非常容易造成上下文代码混乱,我们不得不非常小心翼翼处理内层函数与外层函数的数据,一旦内层函数使用了上层函数的变量,这种混乱程度就会加剧…总之,这种层叠上下文的层层嵌套方式,着实增加了神经的紧张程度。

        //promise封装异步操作
        let p = new Promise((resolve, reject) => {
            $.ajax({
                url: 'mock/user.json',
                success: function (data) {
                    console.log("查询用户成功:", data);
                    resolve(data);
                },
                error(err) {
                    reject(err);
                }
            });
        })
        p.then((obj) => {
            return new Promise((resolve, reject) => {
                $.ajax({
                    url: `mock/user_corse_${obj.id}`,
                    success: function (data) {
                        console.log("查询用户课程成功:", data);
                        resolve(data);
                    },
                    error(err) {
                        reject(err);
                    }
                });
            })
        }).then((data) => {
            console.log("上一步结果", data);
            $.ajax({
                url: `mock/corse_score_${data.id}`,
                success: function (data) {
                    console.log("查询课程得分成功:", data);
                    resolve(data);
                },
                error(err) {
                    reject(err);
                }
            });
        })


        改造后:

         function get(url, data) {
            return new Promise((resolve, reject) => {
                $.ajax({
                    url: url,
                    data: data,
                    success: function (data) {
                        resolve(data);
                    },
                    error: function (err) {
                        reject(err);
                    }
                })
            })
        }

        get("mock/user.json")
            .then((data) => {
                console.log("查询用户成功:", data);
                return get(`mock/user_corse_${data.id}.json`);
            })
            .then((data) => {
                console.log("课程查询成功:", data);
                return get(`mock/corse_score${data.id}.json`);
            })
            .then((data) => {
                console.log("课程成绩查询成功:", data);
            })
            .catch((err) => {
                console.log("出现异常", err);
            })

9.模块化

​ 1)模块化就是把代码进行拆分,方便重复利用。类似java中的导包:要使用一个包,必须先导包。而js中没有包的概念,换来的是模块。

​ 模块功能主要由两个命令构成: exportimport

export命令用于规定模块的对外接口。

import命令用于导入其他模块提供的功能。

export default导出默认

vue模块化开发

​ 模块化开发都使用npm进行包管理

​ cmd窗口右击->属性->取消快速编辑模式

​ 查看vue脚手架版本号:vue -V,查看vue版本号项目的package.json中查看

​ 1.全局安装webpack:npm install webpack -g

​ 2.vue-cli 2.x+ 安装及使用

​ (1)卸载vue3.x+ (如果安装过)

​ npm uninstall -g @vue/cli

​ (2) 安装vue2.x+,安装成功可以使用vue -V进行检查是否安装成功

​ npm install vue-cli -g

​ 基于2.x+版本的脚手架创建vue项目

​ vue init webpack vue_demo,

​ cd vue_demo

​ npm install

​ npm run dev

​ 3.vue-cli3.x+ 安装及使用

​ (1)卸载旧版本vue2.x+(如果安装过)

​ npm uninstall vue-cli -g

​ (2)安装vue3.x+,安装成功可以使用vue -V进行检查是否安装成功

​ npm install -g @vue/cli

​ 基于3.x+ 版本的脚手架创建vue项目

​ ①使用vue ui图形界面创建vue3.x+ 项目

​ vue ui

​ ②使用命令行创建vue3.x+ 项目

​ vue create //文件名 不支持驼峰

​ ③基于2.x的旧模板,创建旧版vue项目

​ npm install -g @vue/cli-init //第一次使用,全局安装一个桥接工具

​ vue init webpack my_project

​ 注:创建vue3项目需要vue-cli版本大于4.5.4

使用elementUI

​ 安装:npm install(简写i) element-ui

​ 使用:在main.js中写入以下内容:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});

商品服务-三级分类

查询-递归树形结构数据获取

    /**
     * 列表
     */
    @RequestMapping("/list/tree")
    //@RequiresPermissions("product:category:list")
    public R list(){
        List<CategoryEntity> entities = categoryService.listWithTree();

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

    @Override
    public List<CategoryEntity> listWithTree() {
        //1.查出所有分类
        List<CategoryEntity> entities = baseMapper.selectList(null);
        //2.组装成父子的树形结构
        //2.1找到所有的一级分类
        List<CategoryEntity> level1Menus = entities.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0)
                .peek((menu) -> menu.setChildren(getChildrens(menu,entities)))
                .sorted(Comparator.comparingInt(menu -> (null == menu.getSort() ? 0 : menu.getSort())))
                .collect(Collectors.toList());

        return level1Menus;
    }

    private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all) {
        return all.stream()
                .filter((categoryEntity) -> categoryEntity.getParentCid().equals(root.getCatId())) //1.找到所有的子菜单
                .peek((menu) -> menu.setChildren(getChildrens(menu,all)))   //2.找到子菜单的所有子菜单
                .sorted(Comparator.comparingInt(menu -> (null == menu.getSort() ? 0 : menu.getSort())))   //3.对所有子菜单进行排序,注意节点的getSort获取到的值可能为null
                .collect(Collectors.toList());
    }

配置网关,路由与路径重写

​ 1.renren-fast服务模块的pom.xml中添加common的依赖。

​ 2.把项目交由注册中心管理:

spring:
  application:
    name: renren-fast
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

主启动类中添加注册发现的注解:        
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {

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

}

​ 3.网关服务模块mall-gateway的application.yml中添加配置:

spring:
  cloud:
    gateway:
      routes:
        - id: admin_route
          uri: lb://renren-fast
          predicates:
            - Path=/api/**
          filters:
            - RewritePath=/api/?(?<segment>.*), /renren-fast/$\{segment}    //把访问网关的路径进行重写并转发
## 前端项目的请求路径都加上: /api前缀

​ 4.renren-fast-vue后台管理系统中添加商品分类管理菜单,修改配置文件的api请求地址:

 // api接口请求地址

 window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';

​ 5.进行项目开发,之后的请求都会通过网关转发给各模块。

解决跨域

​ 跨域:指的是浏览器不能执行其他网站的脚本。它是右路由器的同源策略造成的,是浏览器对javascript施加的安全限制。

​ 同源策略:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域;

​ 跨域流程:

​ 非简单请求(PUT、DELETE)等,需要先发送预定检请求

​ 1.客户端发送预检请求,OPTIONS

​ 2.服务器响应允许跨域

​ 3.客户端发送真实请求

​ 4.服务器响应数据

​ get和post虽然属于简单请求,但如果发送的内容类型(Content-type)不属于test/plain、multipart/form-data

​ application/x-www-form-urlencoded之一时也需要跨域。

​ 跨域操作都需要发送一个预检请求,"需预检请求"要求必须首先使用OPTIONS方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。"预检请求"的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

​ 跨域的官方文档:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

​ 解决跨域-(一)使用nginx部署为同一域

​ 解决跨域-(二)服务器端配置当次请求允许跨域(给预检请求一个响应)

​ 1.添加响应头

​ • 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在响应服务器之前添加这些允许跨域的(Content-type)

​ 1.mall-gateway项目的com.daoshabao.mall.gateway下新建一个config包,在下面新建一个配置类

package com.doushabao.mall.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
public class MallCorsConfiguration {

    @Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();    //获取一个跨域的配置信息,注意是reactive包下的

        CorsConfiguration configuration = new CorsConfiguration();

        //配置跨域
        configuration.addAllowedHeader("*");        //允许哪些头跨域
        configuration.addAllowedMethod("*");        //允许哪些请求方式进行跨域
        configuration.addAllowedOrigin("*");        //允许哪些请求来源进行跨域
        configuration.setAllowCredentials(true);    //是否允许携带cookies进行跨域
        source.registerCorsConfiguration("/**",configuration);

        return new CorsWebFilter(source);
    }

}

​ 2.注释掉原来renren-fast后台的跨域配置:config下的CorsConfig的Configuration注释即可

请求三级分类数据

​ 之前配置了把经过网关的请求都转给了renren-fast,但请求三级分类等项目数据需要到对应的服务中去,所以网关下添加配置:

- id: product_route
  uri: lb://mall-product
  predicates:
    - Path=/api/product/**
  filters:
    - RewritePath=/api/(?<segment>.*), /$\{segment}

​ 添加配置中心和注册中心的配置(暂未把配置都移到配置中心)

​ 1.配置中心中新建命名空间

​ 2.创建bootstrap.properties,添加如下配置:

spring.application.name=mall-product

spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=87dc4aee-e444-49c5-963e-b8b982ed91d0

​ 3.给项目下的application.yml添加注册中心的配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

​ 4.主启动类中添加注解:@EnableDiscoveryClient

​ 注:服务的注册都是注册到public命名空间下的,其他命名空间只是用来做配置隔离的。

​ 5.以上操作之后依然无法访问成功,因为网关的路由配置没有生效,与renren-fast的路由配置冲突了,renren-fast的配置提前生效了。转到renren-fast里面了,需要调整路由顺序,与renren-fast的路由顺序对换(把精确的路径放到上面(高优先级))

- id: admin_route
  uri: lb://renren-fast
  predicates:
    - Path=/api/**
  filters:
    - RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}

- id: product_route
  uri: lb://mall-product
  predicates:
    - Path=/api/product/**
  filters:
    - RewritePath=/api/(?<segment>.*), /$\{segment}


换成

- id: product_route
  uri: lb://mall-product
  predicates:
    - Path=/api/product/**
  filters:
    - RewritePath=/api/(?<segment>.*), /$\{segment}

- id: admin_route
  uri: lb://renren-fast
  predicates:
    - Path=/api/**
  filters:
    - RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}

​ 6前端成功访问到后端数据,之后就是前端显示的问题了。

删除菜单

​ 使用mybatis plus的逻辑删除:

​ • 步骤一,例: application.yml

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
      logic-delete-value: 1 # 逻辑已删除值(默认为 1)
      logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)

​ • 步骤二,实体字段上加上@TableLogic注解

​ @TableLogic(value = “1”,delval = “0”)

​ private Integer showStatus;

​ 之后该服务的所有删除操作都会变成逻辑删除

​ 因为前端的get请求和post请求都是用的同一套模板,所以定义模板代码:

    "vue": {
        "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 scoped>",
            "//@import url($3); 引入公共css类",
            "$4",
            "</style>"
        ],
        "description": "生成vue模板"
    },
    "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请求"
    }

添加菜单

​ 添加页面使用elementui的对话框组件el-dialog和表单组件el-form。

编辑菜单

​ 页面中添加编辑菜单按钮。

​ 参数需要绑定菜单id。

​ 服用添加菜单的对话框,但注意区分确定按钮点击事件发送不同的请求,使用 dialogType 变量来区分。

​ 对话框里回显的内容应该从后端读取才是最新的数据。

​ 可以被修改的字段:name、productUnit、icon,不应该被修改的字段:catLevel、showStatus、sort。

拖拽树形控件节点改变节点的父子关系

​ 通过设置draggable属性可让节点变为可拖拽。

​ 拖拽后层级可能大于等于4层,应通过allow-drop属性来判断目标节点能否放置在指定位置。

​ 节点拖拽成功之后会触发node-drop事件,用于向后端发送节点信息改变的请求。

​ 节点拖拽后可能改变的信息:节点的层级(catLevel)、节点的父节点id(parentCid)、节点的排序(sort)、节点的子节点的层级,统一把需要更新的节点信息封装到一个数组里面(updateNodes)。

​ 之后遍历updateNodes里面的所有节点进行数据更新。

​ 后端写一个批量修改的方法。

开关控制是否开启拖拽功能和统一保存拖拽

​ el-switch、el-button

​ 注:之后应继续优化拖拽功能

批量删除菜单

​ 使用el-button进行批量删除功能

​ 获取当前被选中的所有节点,为菜单树绑定ref属性:ref =“menuTree”,通过getCheckedNodes获取被选中的节点

商品服务-品牌管理

页面显示和品牌显示状态

​ 1.商品系统中新增菜单-品牌管理

​ 2.将商品服务项目中生成的brand.vue和brand-add-update.vue文件放到renren-fast-vue项目的modules/product下。

​ 3.修改utils/index.js中的isAuth(key)方法,统一返回有权限。

​ 4.修改显示界面:显示状态使用开关el-switch、品牌logo地址使用上传组件el-upload

​ 5.为el-switch绑定@change事件,当开关切换时向后端发送更新品牌的显示状态updateBrandStatus()

​ 6.el-switch设置 :active-value=“1” , :inactive-value=“0”

上传品牌logo

​ 单体应用的上传:只需把文件上传给服务器,服务器把文件存储起来即可

​ 而分布式应用的上传:需要一个文件统一的文件系统供所有服务访问。

​ 自建文件存储服务器:FastDFS、vsftpd,搭建复杂,维护成本高,前期费用高。

​ 云存储文件服务器:阿里云对象存储、七牛云存储,即开即用,无需维护,按量收费。

​ 项目中采用阿里云对象存储(OSS),需先开通并创建bucket,往bucket里存文件。

​ 上传方式:

​ 一、阿里云对象存储-普通上传方式:用户 -> 应用服务器 -> OSS。弊端:文件先经过自己的服务器,浪费性能和带宽,

​ 影响服务器其他功能。

​ 一.五、用户 -> OSS 会泄露阿里云账号密码等相关信息。

​ 二、阿里云对象存储-服务端签名后直传(项目中使用此方式):

​ 1.用户向应用服务器请求上传Policy

​ 2.应用服务器返回上传Policy

​ 3.用户直接上传数据到OSS

​ 项目中使用OSS:

​ 1.商品服务(mall-product)中添加sdk依赖

        <dependency>
            <groupId>com.aliyun.oss</groupId>
            <artifactId>aliyun-sdk-oss</artifactId>
            <version>3.10.2</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>jaxb-api</artifactId>
            <version>2.3.1</version>
        </dependency>
        <dependency>
            <groupId>javax.activation</groupId>
            <artifactId>activation</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- no more than 2.3.3-->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.3</version>
        </dependency>

​ 2.参考上传代码

        // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
        String endpoint = "oss-cn-guangzhou.aliyuncs.com";
        // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
        String accessKeyId = "myAccessKeyId";
        String accessKeySecret = "myAccessKeySecret";
        // 填写Bucket名称,例如examplebucket。
        String bucketName = "doushabao-mall";
        // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
        String objectName = "c.jpg";
        // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
        // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
        String filePath= "D:\\IOOperate\\c.jpg";

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObjectRequest对象。
            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new File(filePath));
            // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
            // ObjectMetadata metadata = new ObjectMetadata();
            // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
            // metadata.setObjectAcl(CannedAccessControlList.Private);
            // putObjectRequest.setMetadata(metadata);

            // 上传文件。
            ossClient.putObject(putObjectRequest);
            System.out.println("上传成功咯");
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

​ 3.为阿里云账户创建子账户,访问方式选择Open API调用访问,项目中用这个子用户访问阿里云。

​ 为这个子用户添加AliyunOSSFullAccess权限

​ 这个子用户的accessKeyId和accessKeySecret运用打项目中

​ 4.使用SpringCloud Alibaba的文件上传功能,而不用原生的上传sdk,注释上面的原生sdk依赖,

​ 在common中引入相关依赖,之后使用即可。

​ 创建一个微服务(mall-third-party)统一管理需要调用第三方平台的功能,选中web和openFeign依赖,

​ 之后依赖common和把common中的OSS依赖转移过来。更改springboot和springcloud等的依赖版本。

​ 为该服务创建配置文件(bootstrap.properties等),配置注册中心和配置中心等相关配置。

​ 因为common中添加了mybatis相关的依赖,但mall-third-party服务中暂时用不上,所以可以过滤掉,在依赖中添加过滤:

<exclusions>
    <exclusion>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
    </exclusion>
</exclusions>

​ 主启动类中加上@EnableDiscoveryClient注解。

​ 把之前写在product服务中的对象存储相关的测试迁移到第三方服务中。

第三方服务中使用OSS对象存储

​ 项目中新建包controller

​ 新建OssController,对应获取签名的代码:

@Autowired
OSS ossClient;

@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;

@Value("${spring.cloud.alicloud.access-key}")
private String accessId;

//获取签名相关信息
@RequestMapping("/oss/policy")
public  Map<String, String> policy() {
    String bucket = this.bucket; // 请填写您的 bucketname 。
    String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
    // callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
    //String callbackUrl = "http://88.88.88.88:8888";
    String now = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
    String dir = now + "/"; // 用户上传文件时指定的前缀。
    Map<String, String> respMap = null;
    try {
        long expireTime = 30;
        long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
        Date expiration = new Date(expireEndTime);
        // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
        PolicyConditions policyConds = new PolicyConditions();
        policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
        policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

        String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
        byte[] binaryData = postPolicy.getBytes("utf-8");
        String encodedPolicy = BinaryUtil.toBase64String(binaryData);
        String postSignature = ossClient.calculatePostSignature(postPolicy);

        respMap = new LinkedHashMap<String, String>();
        respMap.put("accessid", accessId);
        respMap.put("policy", encodedPolicy);
        respMap.put("signature", postSignature);
        respMap.put("dir", dir);
        respMap.put("host", host);  //告诉浏览器访问地址
        respMap.put("expire", String.valueOf(expireEndTime / 1000));
        // respMap.put("expire", formatISO8601Date(expiration));
    } catch (Exception e) {
        // Assert.fail(e.getMessage());
        System.out.println(e.getMessage());
    } finally {
        ossClient.shutdown();
    }
    return respMap;
}

​ 使用网关统一管理第三方服务:

​ 在mall-gateway服务的application.yml中添加配置

- id: third_party_route
  uri: lb://mall-third-party
  predicates:
    - Path=/api/thirdparty/**
  filters:
    - RewritePath=/api/thirdparty/(?<segment>.*), /$\{segment}

​ 之后可以请求网关进行第三方服务的请求:http://127.0.0.1:88/api/thirdparty/oss/policy

联调前端完成文件上传功能

​ 使用el-upload组件实现文件上传功能。

​ 复制资源中的upload包到项目的components包下,里面封装单文件上传和多文件上传的组件。

​ 在brand-add-or-update.vue中导入并在品牌logo地址中使用单文件上传组件,修改组件中的action属性为bucket的域名

​ 点击上传文件时会报跨域请求错误,需要在oss里面把bucket设置成允许跨域访问:

​ 概览 -> 基础设置 -> 跨域访问设置 -> 创建规则 -> 设置跨域规则。

​ 再次上传即可成功。更改表格中logo的显示为图片,使用el-image组件进行图片显示,

但原来的项目中并没有导入elemen-ui的el-image组件,需自己手动导入

前端上传数据前的表单校验

​ Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。校验规则参见 async-validator

​ 为首字母和排序规则使用自定义校验规则

后端对上传的数据进行基本校验(JSR303,在javax.validation.constraints包下)

​ 高版本的springboot可能需要加入依赖:spring-boot-starter-validation

​ 1)、给Bean添加校验注解(如@NotBlank)

​ 2)、告诉springmvc哪个bean是需要校验的(bean作为参数的时候在前面叫上@Valid)

​ 3)校验错误之后会有默认的响应

​ 4)也可以通过BindingResult 类型的参数获取校验结果,该参数必须紧跟在@Valid标注的参数之后,然后自定义封装。

后端对上传的异常数据进行集中处理

​ 使用springmvc提供的advice功能

​ 校验异常如果不用BindingResult 进行接受的话,springmvc就会把异常抛出去,所以可以写一个公共的异常处理类

​ 1。创建exception.MallExceptionControllerAdvice,上面添加注解

​ @Slf4j和@RestControllerAdvice(basePackages = “com.doushabao.mall.product.controller”)

​ 2.编写统一的校验异常处理方法:

 @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public R handleValidException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        Map<String,String> errorMap = new HashMap<>();
        bindingResult.getFieldErrors().forEach((fieldError) -> {
            errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
        });
        log.error("数据校验出现问题{},异常类型: {}",e.getMessage(),e.getClass());
        return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
    }

​ 3.编写统一的异常处理方法

@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
    return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}

​ 4.状态码和响应信息都封装到一个枚举类中:BizCodeEnume

JSR303分组校验

​ 比如品牌的新增和修改所需要的字段是不一样的,新增不需要传id,而修改需要传id,所以需对他们校验的字段进行分组。

​ 在common下新建valid包,添加AddGroup.class和Update.class,空接口即可。

​ 1.在校验对应的注解上加上group属性进行分组。

​ 2.把controller中的@Valid注解换成@Validated注解(springframework包中的),在注解上指定要使用的具体分组。

​ 3.@Validated注解必须指定校验分组,否则不会进行数据校验。

​ 4.对应的校验注解在@Validated指定了分组的情况下会失效,必须自己也指定分组校验才会生效。

JSR303自定义校验逻辑

​ 1.自己编写一个自定义的校验注解

@Documented
@Constraint( validatedBy = {ListValueConstraintValidator.class} )
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {

    String message() default "{com.doushabao.common.valid.ListValue.message}";

    Class<?>[] groups() default {};  //支持分组校验

    Class<? extends Payload>[] payload() default {};       //支持一些负载信息

    int[] vals() default {};
}

​ 2.编写一个自定义的校验器

public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {

    private Set<Integer> set = new HashSet<>();

    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * @Description:   判断是否校验成功
     * @Param          value 提交的值
    */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(value);
    }
}

​ 3.字段关联自定义的校验器和自定义的校验注解

/**
 * 显示状态[0-不显示;1-显示]
 */
@ListValue(vals = {0,1},groups = {AddGroup.class})
private Integer showStatus;

​ 4.若之后希望能把这个校验注解用于校验更多的数据类型,如Double等,则需要在校验注解上指定更多的校验器。

​ 即一个校验注解可以有多个校验器,它会自动识别自己被标注在了什么类型的属性上。

SPU与SKU

​ SPU:Standard Product Unit(标准化产品单元)是商品信息聚合的最小单位。是一组可复用、易检索的标准化信息的集合,

​ 该集合描述了一个产品的特性。

​ SKU:Stock Keeping Unit(库存量单位)即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。

​ SKU这是对于大型连锁超时DC(配置中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,

​ 每种产品均对应有唯一的SKU号。

​ 基本属性【规格参数】与销售属性

​ • 每个分类下的商品共享规格参数,与销售属性。只是有些商品不一定要用这个分类下全部的属性;

​ • 属性是以三级分类组织起来的

​ • 规格参数中有些是可以提供检索的

​ • 规格参数也是基本属性,他们具有自己的分组

​ • 属性的分组也是以三级分类组织起来的

​ • 属性名确定的,但是值是每一个商品不同来决定的

商品属性分组

分组查询功能

​ 后台管理中新建菜单,直接在mall-admin数据库中运行创建菜单的sql语句,需注意库名一致。

​ 接口描述ip地址:https://easydoc.net/s/78237135/ZUqEdvA4/hKJTcbfd

​ 前端:

​ 把三级分类抽取成一个组件category.vue放到modules/common下,

​ 在modules/product下新建属性分组页面attrgroup.vue里面使用栅格布局把页面分成两部分:

​ 左:分类组件,右:表格。点击左边的三级分类动态的把属性从后端请求放入右边的表格。

​ 涉及到父子组件之间的通信:点击子组件中的三级菜单category.vue,

​ 父组件attrgroup.vue需要能够感知到并将数据渲染的表格

​ 子组件给父组件传递数据,事件机制:

​ 子组件给父组件发送一个事件(this.$emit),携带上数据。

​ 父组件进行事件监听从而感知到事件的触发获取数据,有点类似冒泡。

分组新增功能

​ 新增界面的所属分类id应使用级联选择器来供选择。

​ v-model 需要绑定一个数组

分组修改功能(分类回显)

​ 打开新增/修改对话框时顺便查询到分类的全路径catelogPath

mybatis-plus分页插件

​ 商品服务下新建config包,添加MybatisConfig,配置mybatis-plus分页插件

@Configuration
@EnableTransactionManagement    //开启事务
@MapperScan("com.doushabao.mall.product.dao")
public class MybatisConfig {

    //引入分页插件
    @Bean
    public PaginationInnerInterceptor paginationInnerInterceptor() {
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
        paginationInnerInterceptor.setOverflow(true);   //如果页码大于最后一页,重新从第一页开始
        paginationInnerInterceptor.setMaxLimit(1000L);  //每页限制最大为1000条,-1为不限制
        return paginationInnerInterceptor;
    }
}

​ 品牌管理界面需使用分页插件和条件查询

品牌关联分类

​ 一个品牌可能会有多个分类,如小米品牌下会有手机、电视、电脑等

​ 一个分类也会对应多个品牌,如手机会有华为、小米等

​ 品牌和分类是多对多关系

为品牌添加关联的分类,前端传入的参数没有分类名和品牌名,需自己在后端保存前去对应的表中查询

因为品牌名和分类名存储在了中间表中,所以当对应的表中的字段进行了修改时需要进行数据同步,保持数据一致,之后业务中可能会存储很多冗余字段,都需要进行同步

为对应的service方法加上事务注解 @Transactional

Object划分
1、PO(persistent object) 持久对象
PO就是对应数据库中某个表的一条记录,多个记录可以用PO的集合,PO中应该不包含任何对数据库的操作。
2、DO(Domain Object)领域对象
就是从显示世界中抽象出来的有形或无形的业务实体。
3、TO(Transfer Object),数据传输对象
不同的应用程序之间传输的对象,如不同微服务之间通过TO来封装传输数据,放common中比较合适。
4、DTO(Data Transfer Object)数据传输对象
跟TO一样
5、VO(value/view object)值对象
通常用于业务层之间的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。用new关键字创建,由GC回收的。
接受页面传递的数据,封装为对象;将业务处理完的对象,封装成页面要用的数据
6、BO(business object)业务对象
从业务模型的角度看,见UML元件领域模型中的领域对象。封装业务逻辑的java对象,通过调用DAO方法,结合PO,VO进行业务操作。business object:业务对象 主要作用是把业务逻辑封装为一个对象。这个对象可以包含一个或多个其他的对象。比如一个简历、工作经历、社会关系等等。我们可以把教育经历对应一个PO,工作经历对应一个PO,社会关系对应一个PO。建立一个对应建立的BO对象处理简历,每个BO包含这些PO。这样处理业务逻辑时,我们就可以针对BO取处理。
7、POJO(plain ordinary java object)简单无规则java对象
传统意义的java对象。就是说在一些object/Relation Mapping工具中,能够做到维护数据库表记录的persisent object完全是一个符合Java Bean规范的纯Java对象,没有增加别的属性和方法。我的理解就是最基本的Java Bean,只有属性字段及setter和getter方法!。
POJO是DO/DTO/BO/VO的统称。
8、DAO(data access object)数据访问对象
用来访问数据库的对象

规格参数增删改查与关联分组、关联所属分类
规格参数(属性)需要有对应的属性分组,应该是多对一的关系,却存在着中间表attr_attrgroup_relation。
规格参数(属性)需要有对应的分类,如入网型号这个属性可能会对应着手机、平板等,他们之间应该是多对多的关系,项目中没有设计中间表,不知道他们之间的关系。
注意:规格参数添加和更新时,关联的分组不是必填参数,可能会接收到null,如果为空则不进行添加和更新。

销售属性增删改查、关联所属分类
销售属性与规格参数都是属性,属于同一张表中的数据,但他们是通过一个字段attr_type来区分的,0代表销售属性,1代表规格参数,2既是规格参数又是销售属性(项目中不使用)。
原来的查询规格参数的方法中,新加一个路径参数attrType,来区分不同类型的属性。
新增销售属性的时候会在关联表中设置分组字段为null,查询时可能会报空指针异常,需注意如果是销售属性的新增或修改都不需要进行分组关联操作。

分组与属性关联功能
查询关联:通过分组与属性的关联表获取所有的关联实体(当前分组下的),通关关联实体的属性id查询到所有的属性实体并返回。
删除关联:通过传入的attrId和attrGroupId删除关联表中的数据即可,注意前端传入的是一个数组,所以要写一个批量删除的方法,使用mybatis的foreeach标签遍历数组中的每一项进行批量删除。
添加关联:需先查询出当前分类下的没有被关联(包括当前分组和其他分组)的所有属性,这些属性即是可以被当前分组关联的属性。

发布商品
需要依次录入基本信息->规格参数->销售属性->SKU信息,才能完成商品的录入。
 1. 调试会员等级相关接口(商品基本信息中使用)
  导入会员等级所需的页面,增加网关过滤会员相关的服务接口。
 2. 通过分类id获取所有的品牌(商品基本信息中使用)
  选中三级分类后,查询该分类下的所有品牌,供选择品牌下拉框选择。
 3. 获取分类下所有分组以及关联属性(规格参数中使用)
  通过三级分类id,获取到关联的所有分组,查询这些分组下的所有属性
 依次填写好待发布的商品信息后,向后端发送保存的请求,后端使用SpuSaveVo接收传来的所有数据
 4. 保存商品基本流程
  4.1保存spu基本信息:sms_spu_info
  4.2保存spu的描述信息:pms_spu_info_desc
  4.3保存spu的图片集:pms_spu_images
  4.4保存spu的规格参数 pms_product_attr_value
  4.5.保存spu的积分信息–跨库需远程调用(feign)–mall_sms–sms_spu_bounds
  4.6.保存spu的所有sku
   4.6.1. sku的基本信息–pms_sku_info
   4.6.2. sku的图片信息–pms_sku_images
   4.6.3. sku的销售属性值–pms_sku_sale_attr_value
   4.6.4. sku的优惠满减等信息–跨库需远程调用(feign)–mall_sms
 5. 测试
  idea批量重启:Edit Configurations -> + ->Compound->将服务添加进去

SPU管理(SPU检索)
  请求地址:/product/spuinfo/list

商品管理(SKU检索)
  请求地址:/product/skuinfo/list

仓库管理
  仓库管理是仓储服务(mall-ware)下的的功能,需为服务配置注册中心、开启事务、开启mybatis扫描等功能。
  给网关添加上该服务的管理。
  之后即可访问逆向工程生成的仓库管理。

商品库存管理
  查询库存列表接口:/ware/waresku/list

采购系统(只是简单模拟,有很多需要优化和完善的地方)
  商品的库存一般不直接通过人为进行修改,提供一个查看商品库存的接口就好,商品库存的操作一般通过采购单管理,采购好商品后自动入库,商品销售出去之后自动减少库存。
  采购需求:1、人为在后台创建了采购需求;2、系统自动检测库存,对低库存商品发布采购需求。
  采购单就类似于采购需求,采购人员通过采购单进行商品的采购。
  合并采购需求:对同一仓库和同一商品的采购单可以进行合并。合并采购单需要参数:purchaseId(哪个采购单id,如果没有选择采购单则不传)和items(哪些采购需求id)
领取采购单:/ware/purchase/received,主要是对采购单状态进行更新
完成采购:/ware/purchase/done,完成采购后商品入库,更新采购单的转台
加了个商品管理的:spu属性更新:/product/attr/update/{spuId}

其他:

​ 查看端口使用:netstat -ano

​ 查看指定端口的使用:netstat -aon|findstr “指定的端口”

​ 结束进程:taskkill /f /t /im tor.exe

​ idea所有的import都报错了而且重复刷新依赖也没用,可以试试:File->Invalidate Caches/Restart清除一下缓存

​ postman账号:qq号

​ idea构建maven项目:help -> Find Action -> 搜索maven -> Add Maven Projects -> 选择项目的pom文件。

​ idea中项目没有启动图标:右击项目 -> Mark Directory as -> Sources Root

​ SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供

​ 安全及数据完整性的一种安全协议。TLS与SSL在传输层与应用层之间对网络连接进行加密。

​ idea改变方法签名ctrl + f6

启动项目:

​ 1.启动nacos

​ 2.本人电脑需要结束10000端口的进程:yundetectservice.exe

​ 3.启动对应的服务

​ 4.前端项目报eslint错时, 在build/webpack.base.conf.js文件中,

​ 注释…(config.dev.useEslint ? [createLintingRule()] : []),这行配置

​ 或注释createLintingRule的方法体内容

idea安装mybatisx插件,可以与xml文件互眺

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值