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中没有包的概念,换来的是模块。
模块功能主要由两个命令构成: export
和 import
。
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文件互眺