分布式基础篇

1)简介

项目介绍

该项目分为三篇进行开发:

分布式基础篇(全栈开发篇)

谷粒商城的后台管理系统,后端技术栈:SpringBoot,带上Mybatis那一套,SpringCloud,docker,作为后端的基础环境,结合前端vue+ElementUI,使用逆向工程的方式快速开发出一个后台管理系统,通过分布式基础篇,能够快速打通全栈开发能力。

分布式高级篇(微服务架构篇)

通过实现一整套的商城业务逻辑,比如商品服务、购物车、订单、结算、库存、秒杀…通过实现这么一套业务,来打通整个在微服务架构开发期间的技术栈以及一些重点难点,使用SpringBoot和SpringCloud作为最基础的配套,以及搭配SpringCloudAlibaba的Nacos注册中心配置中心,Sentinel流量保护,Seata分布式事务…在架构篇不仅可以学到分布式的高级技术,以及微服务的周边所有配套方案都能一站式打通。

涉及到的重点难点:

网关,远程调用,链路追踪,缓存,Session同步,全文检索,异步编排,线程池,压力测试,性能调优,redis分布式锁…

高可用集群篇(架构师提升篇)

开发完此篇,将会进一步提升架构师能力。搭建k8s集群,一主两从的方式,会使用kubershere这个一站式的平台,快速将整个商城应用部署到k8s集群里面,打通整个DevOps技术栈,包括完成的可视化的CICD(持续集成,持续部署)基于企业真实流水线编写的一个具有参数化构建,手工确认模式的一个流水线。最终实现整个应该的持续集成持续部署。包括一些集群技术都会涉及,比如mysql的集群,redis的classter,rabbitMQ的镜像队列…

项目背景

1)、电商模式

市场上有5种常见的电商模式B2B、B2C、C2B、C2C、O2O;

1、B2B模式

指的是商家与商家建立的商业关系,如阿里巴巴

2、B2C模式

就是我们经常看到的供应商直接把商品卖给用户,即“商对客”模式,也就是通常说的商业零售,直接面向消费者销售产品和服务,如:苏宁易购、京东、天猫、小米商城

3、C2B模式

就是消费者对企业,先由消费者需求产生后有企业生产,即先有消费者提出需求,后有生产企业按照需求组织生产

4、C2C模式

客户直接把东西放到网上去卖,比如:淘宝,咸鱼,拼多多

5、O2O模式

即Online To Offline,线上消费,线下服务,也就是将线下商务的机会与互联网结合在了一起,让互联网称为线下交易的前台,线上快速支付,线下优质服务。如:饿了么。美团,淘票票,京东到家

2)、谷粒商城

是一个B2C模式的电商平台,模拟销售自营商品给客户。

项目技术&特色

  • 前后端分离开发,并开发基于vue的后台管理系统
  • SpringCloud全新解决方案
  • 应用监控、限流、网关、熔断降级等分布式方案全方位涉及
  • 渗透讲解分布式事务、分布式锁等分布式系统的难点
  • 分析高并发场景的编码方式,线程池,异步编排等使用
  • 压力测试与性能优化
  • 各种集群技术的区别以及使用
  • CI/CD使用

项目前置要求

  • 学习项目的前置知识
  • 熟悉SpringBoot以及常见的整合方案
  • 了解SpringCloud
  • 熟悉git,maven
  • 熟悉linux,redis,docker基本操作
  • 了解html,css,js,vue
  • 熟练使用idea开发项目

项目整体效果演示

https://www.bilibili.com/video/BV1np4y1C7Yf?p=2

分布式微服务基础概念


什么是微服务

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

微服务:拒绝大型单体应用,基础业务边界进行服务微化和拆分,各个服务器独立部署运行

概念理解(集群&分布式&节点

分布式中的每一个节点,都可以做集群,而集群并不一定就是分布式

  1. 集群:是物理形态,一堆机器合起来,就叫集群
  2. 分布式:是工作方式,分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统。
  3. 节点:集群中的一个服务器

远程调用

分布式系统中,各个服务可能处于不同主机,但是服务器之间有不可避免的调用,我们也称为远程调用

负载均衡

(不要让任何一台服务器太忙,也一会让他太闲)可以负载均衡调用每一个服务器,提升网站的健壮性

常见的负裁均衡算法:

  1. **轮询:**为第一个请求选择健康池中的第一个后端服务器,然后按顺序往后依次选择,直到最后一个,然后循环。
  2. **最小连接:**优先选择连接数最少,也就是压力最小的后端服务器,在会话较长的情况下可以考虑采取这种方式。
  3. **散列:**根据请求源的IP 的散列(hash) 来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器,可以采取这种方式。

服务的注册/发现&注册中心

为了解诀不知道所需服务当前在哪几台服务器有,哪些正常的,哪些服务已经下线,可以引入注册中心;

如果某些服务下线,为了避免调用不可用的已经下线。解诀这个问题可以引入注册中心;

配置中心

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

服务熔断&服务降级

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

服各熔断

设置服务的超时,当被调用的服务经常失败到达某个阈值,我们可以开启断路保护机制,后来的请求不再去调用这个服务。本地直接返回默认的数据

服务降级

在运维期间,当系统处于高峰期,系统资源紧张,我们可以让非核心业务降级运行。降级:某些服务不处理,或者简单处理[抛异常、返回NULL、调用Mock数据、调用Fallback处理逻辑]。

API网关

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

微服务架构图

分布式基础篇_第1张图片

微服务划分图

分布式基础篇_第2张图片

2)环境

使用vagrant快速创建Linux虚拟机

安装vagrant

https://www.vagrantup.com/downloads

安装Centos7

$ vagrant init centos/7
A `Vagrantfile` has been placed in this directory. You are now
ready to `vagrant up` your first virtual environment! Please read
the comments in the Vagrantfile as well as documentation on
`vagrantup.com` for more information on using Vagrant.

执行完上面的命令后,会在用户的家目录下生成Vagrantfile文件。

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Box 'centos/7' could not be found. Attempting to find and install...
    default: Box Provider: virtualbox
    default: Box Version: >= 0
==> default: Loading metadata for box 'centos/7'
    default: URL: https://vagrantcloud.com/centos/7
==> default: Adding box 'centos/7' (v1905.1) for provider: virtualbox
    default: Downloading: https://vagrantcloud.com/centos/boxes/7/versions/1905.1/providers/virtualbox.box
    default: Download redirected to host: cloud.centos.org
    default: Progress: 0% (Rate: 6717/s, Estimated time remaining: 7:33:42)

下载镜像过程比较漫长,也可以采用先用下载工具下载到本地后,然后使用“ vagrant box add ”添加,再“vagrant up”即可

#将下载的镜像添加到virtualBox中
$ vagrant box add centos/7 E:\迅雷下载\CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.box
==> box: Box file was not detected as metadata. Adding it directly...
==> box: Adding box 'centos/7' (v0) for provider:
    box: Unpacking necessary files from: file:///E:/%D1%B8%C0%D7%CF%C2%D4%D8/CentOS-7-x86_64-Vagrant-1905_01.VirtualBox.box
    box:
==> box: Successfully added box 'centos/7' (v0) for 'virtualbox'!

#启动
$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
==> default: Importing base box 'centos/7'...
==> default: Matching MAC address for NAT networking...
==> default: Setting the name of the VM: Administrator_default_1588497928070_24634
==> default: Clearing any previously set network interfaces...
==> default: Preparing network interfaces based on configuration...
    default: Adapter 1: nat
    default: Adapter 2: hostonly
==> default: Forwarding ports...
    default: 22 (guest) => 2222 (host) (adapter 1)
==> default: Booting VM...
==> default: Waiting for machine to boot. This may take a few minutes...
    default: SSH address: 127.0.0.1:2222
    default: SSH username: vagrant
    default: SSH auth method: private key
    default:
    default: Vagrant insecure key detected. Vagrant will automatically replace
    default: this with a newly generated keypair for better security.
    default:
    default: Inserting generated public key within guest...
    default: Removing insecure key from the guest if it's present...
    default: Key inserted! Disconnecting and reconnecting using new SSH key...
==> default: Machine booted and ready!
==> default: Checking for guest additions in VM...
    default: No guest additions were detected on the base box for this VM! Guest
    default: additions are required for forwarded ports, shared folders, host only
    default: networking, and more. If SSH fails on this machine, please install
    default: the guest additions and repackage the box to continue.
    default:
    default: This is not an error message; everything may continue to work properly,
    default: in which case you may ignore this message.
==> default: Configuring and enabling network interfaces...
==> default: Rsyncing folder: /cygdrive/c/Users/Administrator/ => /vagrant

vagrant ssh 开启SSH,并登陆到centos7

$ vagrant ssh
[vagrant@localhost ~]$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 52:54:00:8a:fe:e6 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global noprefixroute dynamic eth0
       valid_lft 86091sec preferred_lft 86091sec
    inet6 fe80::5054:ff:fe8a:fee6/64 scope link
       valid_lft forever preferred_lft forever
3: eth1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 08:00:27:d1:76:f6 brd ff:ff:ff:ff:ff:ff
    inet 192.168.56.102/24 brd 192.168.56.255 scope global noprefixroute dynamic eth1
       valid_lft 892sec preferred_lft 892sec
    inet6 fe80::8c94:1942:ba09:2458/64 scope link noprefixroute
       valid_lft forever preferred_lft forever
[vagrant@localhost ~]$
C:\Users\Administrator>ipconfig

Windows IP 配置

以太网适配器 VirtualBox Host-Only Network:

   连接特定的 DNS 后缀 . . . . . . . :
   本地链接 IPv6 地址. . . . . . . . : fe80::a00c:1ffa:a39a:c8c2%16
   IPv4 地址 . . . . . . . . . . . . : 192.168.56.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . :

虚拟机网络设置

配置网络信息,打开"Vagrantfile"文件:

config.vm.network "private_network", ip: "自己的ip(需要和母机在同一网段下,互相能ping通)"

修改完成后,重启启动vagrant

vagrant reload

检查宿主机和virtualBox之间的通信是否正常

[vagrant@localhost ~]$ ping 192.168.43.43                                                                                                         PING 192.168.43.43 (192.168.43.43) 56(84) bytes of data.
64 bytes from 192.168.43.43: icmp_seq=1 ttl=127 time=0.533 ms
64 bytes from 192.168.43.43: icmp_seq=2 ttl=127 time=0.659 ms

--- 192.168.43.43 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 999ms
rtt min/avg/max/mdev = 0.533/0.596/0.659/0.063 ms
[vagrant@localhost ~]$
[vagrant@localhost ~]$
[vagrant@localhost ~]$ ping www.baidu.com
PING www.a.shifen.com (112.80.248.76) 56(84) bytes of data.
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=1 ttl=53 time=56.1 ms
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=2 ttl=53 time=58.5 ms
64 bytes from 112.80.248.76 (112.80.248.76): icmp_seq=3 ttl=53 time=53.4 ms

开启远程登陆,修改“/etc/ssh/sshd_config”

PermitRootLogin yes 
PasswordAuthentication yes

然后重启SSHD

systemctl restart sshd

使用Xshell或SecureCRT进行远程连接。

image-20200503174735162

linux安装docker

官方安装文档

https://docs.docker.com/engine/install/centos/

#卸载系统之前的docker 
sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine
                  
                  
sudo yum install -y yum-utils

# 配置镜像
sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo
    
sudo yum install docker-ce docker-ce-cli containerd.io

sudo systemctl start docker
# 设置开机自启动
sudo systemctl enable docker

配置docker阿里云镜像加速

由于docker默认拉取镜像是访问国外dockerHub网站,拉镜像下来特别特别慢,所以我们要给docker配置一个阿里云的镜像加速器,这样通过阿里云访问dockerHub就非常快了。

首先进入我们自己的阿里云这个页面:

分布式基础篇_第3张图片

依次执行命令即可。

docker安装mysql

用docker安装上mysql,去docker仓库里搜索mysql

sudo docker pull mysql:5.7

# --name指定容器名字 -v目录挂载 -p指定端口映射  -e设置mysql参数 -d后台运行
sudo 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

su root 密码为vagrant,这样就可以不写sudo了

[root@localhost vagrant]# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                               NAMES
6a685a33103f        mysql:5.7           "docker-entrypoint.s…"   32 seconds ago      Up 30 seconds       0.0.0.0:3306->3306/tcp, 33060/tcp   mysql

#配置mysql字符编码
vi /mydata/mysql/conf/my.conf 

[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

docker restart mysql

docker安装redis

如果直接挂载的话docker会以为挂载的是一个目录,所以我们先创建一个文件然后再挂载。

# 在虚拟机中
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf

docker pull redis

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

# 直接进去redis客户端。
docker exec -it redis redis-cli

默认是不持久化的。在配置文件中输入appendonly yes,就可以aof持久化了。修改完docker restart redis,docker -it redis redis-cli

vim /mydata/redis/conf/redis.conf
# 插入下面内容
appendonly yes
保存

docker restart redis

开发工具&环境安装

配置maven阿里云加速

        <mirror>
            <id>alimavenid>
            <name>aliyun mavenname>
            <url>http://maven.aliyun.com/nexus/content/groups/public/url>
            <mirrorOf>centralmirrorOf>
        mirror>

配置使用jdk1.8编译项目

<profile>   
    <id>jdk1.8id>    
    <activation>   
        <activeByDefault>trueactiveByDefault>    
        <jdk>1.8jdk>   
    activation>    
    <properties>   
        <maven.compiler.source>1.8maven.compiler.source>    
        <maven.compiler.target>1.8maven.compiler.target>    
        <maven.compiler.compilerVersion>1.8maven.compiler.compilerVersion>   
    properties>   
profile>

使用idea整合我们刚配置好的maven。

分布式基础篇_第4张图片

给idea安装插件

lombak、mybatisx

下载vscode

code.visualstudio.com

安装vdcode插件

分布式基础篇_第5张图片

配置git-ssh

下载git客户端:

https://git-scm.com/

右键桌面Git GUI/bash Here。

# 配置用户名
git config --global user.name "username"  //(名字,随意写)

# 配置邮箱
git config --global user.email "[email protected]" // 注册账号时使用的邮箱

# 配置ssh免密登录
ssh-keygen -t rsa -C "[email protected]"
三次回车后生成了密钥
cat ~/.ssh/id_rsa.pub

也可以查看密钥
浏览器登录码云后,个人头像上点设置、然后点ssh公钥、随便填个标题,然后赋值
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC6MWhGXSKdRxr1mGPZysDrcwABMTrxc8Va2IWZyIMMRHH9Qn/wy3PN2I9144UUqg65W0CDE/thxbOdn78MygFFsIG4j0wdT9sdjmSfzQikLHFsJ02yr58V6J2zwXcW9AhIlaGr+XIlGKDUy5mXb4OF+6UMXM6HKF7rY9FYh9wL6bun9f1jV4Ydlxftb/xtV8oQXXNJbI6OoqkogPKBYcNdWzMbjJdmbq2bSQugGaPVnHEqAD74Qgkw1G7SIDTXnY55gBlFPVzjLWUu74OWFCx4pFHH6LRZOCLlMaJ9haTwT2DB/sFzOG/Js+cEExx/arJ2rvvdmTMwlv/T+6xhrMS3 [email protected]

# 测试
ssh -T [email protected]
测试成功,这样以后代码提交到码云就不用再输入账号密码了。

项目结构创建&提交到码云

在码云新建仓库,仓库名gulimall,选择语言java,在.gitignore选中maven,许可证选Apache-2.0,开发模型选生成/开发模型,开发时在dev分支,发布时在master分支,创建。

在IDEA中New–Project from version control–git–复制刚才项目的地址,如https://gitee.com/hanferm/gulimall.git

然后New Module–Spring Initializer–com.atguigu.gulimall , Artifact填 gulimall-product。Next—选择web,springcloud routin里选中openFeign。

依次创建出以下服务

  • 商品服务product
  • 存储服务ware
  • 订单服务order
  • 优惠券服务coupon
  • 用户服务member

共同点:

  • 导入web和openFeign
  • group:com.atguigu.gulimall
  • Artifact:gulimall-XXX
  • 每一个服务,包名com.atguigu.gulimall.XXX{product/order/ware/coupon/member}
  • 模块名:gulimall-XXX

然后右下角显示了springboot的service选项,选择他

从某个项目粘贴个pom.xml粘贴到项目目录,修改他


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0modelVersion>

	<groupId>com.atguigu.gulimallgroupId>
	<artifactId>gulimallartifactId>
	<version>0.0.1-SNAPSHOTversion>
	<name>gulimallname>
	<description>聚合服务description>

	<packaging>pompackaging>

	<modules>
		<module>gulimall-couponmodule>
		<module>gulimall-membermodule>
		<module>gulimall-ordermodule>
		<module>gulimall-productmodule>
		<module>gulimall-waremodule>

	modules>


project>

在maven窗口刷新,并点击+号,找到刚才的pom.xml添加进来,发现多了个root。这样比如运行root的clean命令,其他项目也一起clean了。

修改总项目的.gitignore,把小项目里的垃圾文件在提交的时候忽略掉,比如HTLP.md。。。

在version control/local Changes,点击刷新看Unversioned Files,可以看到变化。

全选最后剩下21个文件,选择右键、Add to VCS。

在IDEA中安装插件:gitee,重启IDEA。

在D额fault changelist右键点击commit,去掉右面的勾选Perform code analysis、CHECK TODO,然后点击COMMIT,有个下拉列表,点击commit and push才会提交到云端。此时就可以在浏览器中看到了。

数据库初始化

因为已经有人贡献sql文件了,所以我们不理会下面引用部分的内容了

安装powerDesigner软件。http://forspeed.onlinedown.net/down/powerdesigner1029.zip

其他软件:
https://www.lanzous.com/b015ag33e

密码:2wre

所有的数据库数据再复杂也不建立外键,因为在电商系统里,数据量大,做外键关联很耗性能。

name是给我们看的,code才是数据库里真正的信息。

选择primary和identity作为主键。然后点preview就可以看到生成这张表的语句。

点击菜单栏database–generate database—点击确定

找到评论区的sql文件,打开sqlyog软件,链接192.168.56.10,账号密码root。

注意重启虚拟机和docker后里面的容器就关了。

sudo docker ps
sudo docker ps -a
# 这两个命令的差别就是后者会显示  【已创建但没有启动的容器】

# 我们接下来设置我们要用的容器每次都是自动启动
sudo docker update redis --restart=always
sudo docker update mysql --restart=always
# 如果不配置上面的内容的话,我们也可以选择手动启动
sudo docker start mysql
sudo docker start redis
# 如果要进入已启动的容器
sudo docker exec -it mysql /bin/bash

然后接着去sqlyog直接我们的操作,在左侧root上右键建立数据库:字符集选utf8mb4,他能兼容utf8且能解决一些乱码的问题。分别建立了下面数据库

gulimall-oms
gulimall-pms
gulimall-sms
gulimall-ums
gulimall-wms

然后打开对应的sql在对应的数据库中执行。依次执行。(注意sql文件里没有建库语句)

3)快速开发

人人开源搭建后台管理系统

分布式基础篇_第6张图片

克隆到本地:

git clone https://gitee.com/renrenio/renren-fast-vue.git

git clone https://gitee.com/renrenio/renren-fast.git

将拷贝下来的“renren-fast”删除“.git”后,拷贝到“gulimall”工程根目录下,然后将它作为gulimall的一个module

创建“gulimall_admin”的数据库,然后执行“renren-fast/db/mysql.sql”中的SQl脚本

修改“application-dev.yml”文件,默认为dev环境,修改连接mysql的url和用户名密码

spring:
    datasource:
        type: com.alibaba.druid.pool.DruidDataSource
        druid:
            driver-class-name: com.mysql.cj.jdbc.Driver
            url: jdbc:mysql://192.168.137.14:3306/gulimall_admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
            username: root
            password: root

启动“gulimall_admin”,然后访问“http://localhost:8080/renren-fast/”

1587616296253

安装node.js,并且安装仓库

npm config set registry http://registry.npm.taobao.org/
PS D:\tmp\renren-fast-vue> npm config set registry http://registry.npm.taobao.org/
PS D:\tmp\renren-fast-vue> npm install
npm WARN [email protected] requires a peer of ajv@>=4.10.0 but none is installed. You must install peer dependencies yourself.
npm WARN [email protected] requires a peer of node-sass@^4.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: [email protected] (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for [email protected]: wanted {
     "os":"darwin","arch":"any"} (current: {
     "os":"win32","arch":"x64"})

up to date in 17.227s
PS D:\tmp\renren-fast-vue> 
PS D:\tmp\renren-fast-vue> npm run dev

> [email protected] dev D:\tmp\renren-fast-vue
> webpack-dev-server --inline --progress --config build/webpack.dev.conf.js

 10% building modules 5/10 modules 5 active ...-0!D:\tmp\renren-fast-vue\src\main.js(node:19864) Warning: Accessing non-existent property 'cat' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:19864) Warning: Accessing non-existent property 'cd' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'chmod' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'cp' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'dirs' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'pushd' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'popd' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'echo' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'tempdir' of module exports inside circular dependency
(node:19864) Warning: Accessing non-existent property 'pwd' of module exports inside circular dependency

常见问题1:“Module build failed: Error: Cannot find module 'node-sass”

运行过程中,出现“Module build failed: Error: Cannot find module 'node-sass’报错问题”,解决方法

用npm install -g cnpm --registry=https://registry.npm.taobao.org ,从淘宝镜像那下载,然后cnpm下载成功。

最后输入cnpm install node-sass --save。npm run dev终于能跑起来了!!!
————————————————
版权声明:本文为CSDN博主「夕阳下美了剪影」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38401285/article/details/86483278

常见问题2:cnpm - 解决 " cnpm : 无法加载文件 C:\Users\93457\AppData\Roaming\npm\cnpm.ps1,因为在此系统上禁止运行脚本。有关详细信息 。。。 "

https://www.cnblogs.com/500m/p/11634969.html

所有问题的根源都在“node_modules”,npm install之前,应该将这个文件夹删除,然后再进行安装和运行。

再次运行npm run dev恢复正常:

1587637858665

逆向工程搭建&使用

clone

https://gitee.com/renrenio/renren-generator.git

然后将该项目放置到“gulimall”的跟路径下,然后添加该Module,并且提交到github上

修改配置

renren-generator/src/main/resources/generator.properties

#代码生成器,配置信息

mainPath=com.bigdata
#包名
package=com.bigdata.gulimall
moduleName=product
#作者
author=cosmoswong
#Email
[email protected]
#表前缀(类名不会包含表前缀)
tablePrefix=pms_

运行“renren-generator”

访问:http://localhost:80/

1587638853416

点击“renren-fast”,能够看到它将“renren-fast”的所有表都列举了出来:

1587638968519

选择所有的表,然后点击“生成代码”,将下载的“renren.zip”,解压后取出main文件夹,放置到“gulimall-product”项目的main目录中。

配置&测试微服务基本CRUD

https://www.bilibili.com/video/BV1np4y1C7Yf?p=18

逆向生成所有微服务基本CRUD代码

下面的几个module,也采用同样的方式来操作。

但是针对于“undo_log”,存在一个问题

1587657745923

它的数据类型是“longblob”类型,逆向工程后,对应的数据类型未知:

1587657812283

这个问题该要怎么解决?

4)分布式组件

SpringCloud Alibaba简介

概述

2018年10月31日,SpringCloudAlibaba 正式入驻 SpringCloud 官方孵化器,并在 Maven 中央库发布第一个版本。
Spring Cloud for Alibaba 0.2.0 released

The Spring Cloud Alibaba project, consisting of Alibaba’s open-source components and several Alibaba Cloud products, aims to implement and expose well known Spring Framework patterns and abstractions to bring the benefits of Spring Boot and Spring Cloud to Java developers using Alibaba products.

Spring Cloud for Alibaba,它是由一些阿里巴巴的开源组件和云产品组成的。这个项目的目的是为了让大家所熟知的 Spring 框架,其优秀的设计模式和抽象理念,以给使用阿里巴巴产品的 Java 开发者带来使用 Spring Boot 和 Spring Cloud 的更多便利。

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。该项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

主要功能

  • 服务限流降级:默认支持 Servlet、Feign、RestTemplate、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 SpringCloud 服务注册与发现标准,默认集成了 Ribbon的支持。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 SpringCloudStream 为微服务应用构建消息驱动能力。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。

组件

  • Sentinel:面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助您保护服务的稳定性。
  • Nacos:阿里巴巴推出来的一个新开源项目,这是一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。
  • RocketMQ:分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
  • Alibaba Cloud ACM:一款在分布式架构环境中对应用配置进行集中管理和推送的应用配置中心产品。
  • Alibaba Cloud OSS: 阿里云对象存储服务(Object Storage Service,简称 OSS),是阿里云提供的海量、安全、低成本、高可靠的云存储服务。您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • Alibaba Cloud SchedulerX: 阿里中间件团队开发的一款分布式任务调度产品,提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。

SpringCloud Alibaba-Nacos注册中心

要注意nacos集群所在的server,一定要关闭防火墙,否则容易出现各种问题。

搭建nacos集群,然后分别启动各个微服务,将它们注册到Nacos中。

  application:
    name: gulimall-coupon
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.137.14

查看注册情况:

1587694451601

SpringCloud-OpenFeign测试远程调用

1)、引入open-feign

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-openfeignartifactId>
        dependency>

2)、编写一个接口,告诉SpringCLoud这个接口需要调用远程服务

修改“com.bigdata.gulimall.coupon.controller.CouponController”,添加以下controller方法:

    @RequestMapping("/member/list")
    public R memberCoupons(){
     
        CouponEntity couponEntity = new CouponEntity();
        couponEntity.setCouponName("discount 20%");
        return R.ok().put("coupons",Arrays.asList(couponEntity));
    }

新建“com.bigdata.gulimall.member.feign.CouponFeignService”接口

@FeignClient("gulimall_coupon")
public interface CouponFeignService {
     
    @RequestMapping("/coupon/coupon/member/list")
    public R memberCoupons();
}

修改“com.bigdata.gulimall.member.GulimallMemberApplication”类,添加上"@EnableFeignClients":

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.bigdata.gulimall.member.feign")
public class GulimallMemberApplication {
     

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

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

3)、开启远程调用功能

com.bigdata.gulimall.member.controller.MemberController

    @RequestMapping("/coupons")
    public R test(){
     
        MemberEntity memberEntity=new MemberEntity();
        memberEntity.setNickname("zhangsan");
        R memberCoupons = couponFeignService.memberCoupons();

        return memberCoupons.put("member",memberEntity).put("coupons",memberCoupons.get("coupons"));
    }

(4)、访问http://localhost:8000/member/member/coupons

1587701348764

停止“gulimall-coupon”服务,能够看到注册中心显示该服务的健康值为0:

1587701521184

再次访问:http://localhost:8000/member/member/coupons

1587701587456

启动“gulimall-coupon”服务,再次访问,又恢复了正常。

SpringCloud Alibaba-nacos配置中心-简单示例

1)修改“gulimall-coupon”模块

添加pom依赖:

<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>

创建bootstrap.properties文件,该配置文件会优先于“application.yml”加载。

spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=192.168.137.14:8848

2)传统方式

为了详细说明config的使用方法,先来看原始的方式

创建“application.properties”配置文件,添加如下配置内容:

coupon.user.name="zhangsan"
coupon.user.age=30

修改“com.bigdata.gulimall.coupon.controller.CouponController”文件,添加如下内容:

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

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

启动“gulimall-coupon”服务:

访问:http://localhost:7000/coupon/coupon/test>

1587716583668

这样做存在的一个问题,如果频繁的修改application.properties,在需要频繁重新打包部署。下面我们将采用Nacos的配置中心来解决这个问题。

3)nacos config

1、在Nacos注册中心中,点击“配置列表”,添加配置规则:

1587716911435

DataID:gulimall-coupon

配置格式:properties

文件的命名规则为:s p r i n g . a p p l i c a t i o n . n a m e − {spring.application.name}-sprin**g.application.nam**e−{spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

${spring.application.name}:为微服务名

${spring.profiles.active}:指明是哪种环境下的配置,如dev、test或info

${spring.cloud.nacos.config.file-extension}:配置文件的扩展名,可以为properties、yml等

2、查看配置:

1587717125580

3、修改“com.bigdata.gulimall.coupon.controller.CouponController”类,添加“@RefreshScope”注解

@RestController
@RequestMapping("coupon/coupon")
@RefreshScope
public class CouponController {
     

这样都会动态的从配置中心读取配置.

4、访问:http://localhost:7000/coupon/coupon/test

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sVYEnJMB-1595834441066)(https://fermhan.oss-cn-qingdao.aliyuncs.com/guli/1587717485283.png)]

能够看到读取到了nacos 中的最新的配置信息,并且在指明了相同的配置信息时,配置中心中设置的值优先于本地配置。

SpringCloud Alibaba-nacos配置中心-命名空间与配置分组

Namespace方案

通过命名空间实现环境区分

下面是配置实例:

1、创建命名空间:

“命名空间”—>“创建命名空间”:

1587718802109

创建三个命名空间,分别为dev,test和prop

2、回到配置列表中,能够看到所创建的三个命名空间

1587718889316

下面我们需要在dev命名空间下,创建“gulimall-coupon.properties”配置规则:

1587719108947

3、访问:http://localhost:7000/coupon/coupon/test

1587721184218

并没有使用我们在dev命名空间下所配置的规则,而是使用的是public命名空间下所配置的规则,这是怎么回事呢?

查看“gulimall-coupon”服务的启动日志:

2020-04-24 16:37:24.158  WARN 32792 --- [           main] c.a.c.n.c.NacosPropertySourceBuilder     : Ignore the empty nacos configuration and get it based on dataId[gulimall-coupon] & group[DEFAULT_GROUP]
2020-04-24 16:37:24.163  INFO 32792 --- [           main] c.a.nacos.client.config.utils.JVMUtil    : isMultiInstance:false
2020-04-24 16:37:24.169  INFO 32792 --- [           main] b.c.PropertySourceBootstrapConfiguration : Located property source: [BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon.properties,DEFAULT_GROUP'}, BootstrapPropertySource {name='bootstrapProperties-gulimall-coupon,DEFAULT_GROUP'}]

"gulimall-coupon.properties",默认就是public命名空间中的内容中所配置的规则。

4、指定命名空间

如果想要使得我们自定义的命名空间生效,需要在“bootstrap.properties”文件中,指定使用哪个命名空间:

spring.cloud.nacos.config.namespace=a2c83f0b-e0a8-40fb-9b26-1e9d61be7d6d

这个命名空间ID来源于我们在第一步所创建的命名空间

1587718802109

5、重启“gulimall-coupon”,再次访问:http://localhost:7000/coupon/coupon/test

1587720311349

但是这种命名空间的粒度还是不够细化,对此我们可以为项目的每个微服务module创建一个命名空间。

6、为所有微服务创建命名空间

1587720714101

7、回到配置列表选项卡,克隆pulic的配置规则到coupon命名空间下

1587720883244

切换到coupon命名空间下,查看所克隆的规则:

1587720963699

8、修改“gulimall-coupon”下的bootstrap.properties文件,添加如下配置信息

spring.cloud.nacos.config.namespace=7905c915-64ad-4066-8ea9-ef63918e5f79

这里指明的是,读取时使用coupon命名空间下的配置。

9、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/coupon/test

1587721184218

DataID方案

通过指定spring.profile.active和配置文件的DataID,来使不同环境下读取不同的配置,读取配置时,使用的是默认命名空间public,默认分组(default_group)下的DataID。

默认情况,Namespace=public,Group=DEFAULT GROUP,默认Cluster是DEFAULT

Group方案

通过Group实现环境区分

实例:通过使用不同的组,来读取不同的配置,还是以上面的gulimall-coupon微服务为例

1、新建“gulimall-coupon.properties”,将它置于“tmp”组下

1587721616021

2、修改“bootstrap.properties”配置,添加如下的配置

spring.cloud.nacos.config.group=tmp

3、重启“gulimall-coupon”,访问:http://localhost:7000/coupon/coupon/test

1587721844449

SpringCloud Alibaba-nacos配置中心-加载多配置集

当微服务数量很庞大时,将所有配置都书写到一个配置文件中,显然不是太合适。对此我们可以将配置按照功能的不同,拆分为不同的配置文件。

如下面的配置文件:

server:
  port: 7000

spring:
  datasource:
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.137.14:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root

  application:
    name: gulimall-coupon
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.137.14:8848



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

我们可以将,

数据源有关的配置写到一个配置文件中:

spring:
  datasource:
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.137.14:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root

和框架有关的写到另外一个配置文件中:

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

也可以将上面的这些配置交给nacos来进行管理。

实例:将“gulimall-coupon”的“application.yml”文件拆分为多个配置,并放置到nacos配置中心

1、创建“datasource.yml”,用于存储和数据源有关的配置

spring:
  datasource:
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.137.14:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root

在coupon命名空间中,创建“datasource.yml”配置

1587722798375

2、将和mybatis相关的配置,放置到“mybatis.yml”中

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

1587722710432

3、创建“other.yml”配置,保存其他的配置信息

server:
  port: 7000

spring:
  application:
    name: gulimall-coupon
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.137.14:8848

1587722998265

现在“mybatis.yml”、“datasource.yml”和“other.yml”共同构成了微服务的配置。

4、修改“gulimall-coupon”的“bootstrap.properties”文件,加载“mybatis.yml”、“datasource.yml”和“other.yml”配置

spring.cloud.nacos.config.extension-configs[0].data-id=mybatis.yml
spring.cloud.nacos.config.extension-configs[0].group=dev
spring.cloud.nacos.config.extension-configs[0].refresh=true

spring.cloud.nacos.config.extension-configs[1].data-id=datasource.yml
spring.cloud.nacos.config.extension-configs[1].group=dev
spring.cloud.nacos.config.extension-configs[1].refresh=true


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

"spring.cloud.nacos.config.ext-config"已经被废弃,建议使用“spring.cloud.nacos.config.extension-configs”

5、注释“application.yml”文件中的所有配置

6、重启“gulimall-coupon”服务,然后访问:http://localhost:7000/coupon/coupon/test

1587724212905

7、访问:http://localhost:7000/coupon/coupon/list,查看是否能够正常的访问数据库

1587724350548

小结:

1)、微服务任何配置信息,任何配置文件都可以放在配置中心;

2)、只需要在bootstrap.properties中,说明加载配置中心的哪些配置文件即可;

3)、@Value, @ConfigurationProperties。都可以用来获取配置中心中所配置的信息;

4)、配置中心有的优先使用配置中心中的,没有则使用本地的配置。

SpringCloud-Gateway核心概念&原理

https://www.bilibili.com/video/BV1np4y1C7Yf?p=26

SpringCloud-Gateway-创建&测试API网关

1、注册“gulimall-gateway”到Nacos

1)创建“gulimall-gateway”

SpringCloud gateway

2)添加“gulimall-common”依赖和“spring-cloud-starter-gateway”依赖

        <dependency>
            <groupId>com.bigdata.gulimallgroupId>
            <artifactId>gulimall-commonartifactId>
            <version>1.0-SNAPSHOTversion>
        dependency>
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>

3)“com.bigdata.gulimall.gulimallgateway.GulimallGatewayApplication”类上加上“@EnableDiscoveryClient”注解

4)在Nacos中创建“gateway”命名空间,同时在该命名空间中创建“gulimall-gateway.yml”

1587729576178

5)创建“bootstrap.properties”文件,添加如下配置,指明配置中心地址和所属命名空间

spring.application.name=gulimall-gateway
spring.cloud.nacos.config.server-addr=192.168.137.14:8848
spring.cloud.nacos.config.namespace=1c82552e-1af0-4ced-9a48-26f19c2d315f

6)创建“application.properties”文件,指定服务名和注册中心地址

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

7)启动“gulimall-gateway”

启动报错:

Description:

Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.

Reason: Failed to determine a suitable driver class

解决方法:在“com.bigdata.gulimall.gulimallgateway.GulimallGatewayApplication”中排除和数据源相关的配置

@SpringBootApplication(exclude = {
     DataSourceAutoConfiguration.class})

重新启动

访问:http://192.168.137.14:8848/nacos/#,查看到该服务已经注册到了Nacos中

1587730035866

2、案例

现在想要实现针对于“http://localhost:88/hello?url=baidu”,转发到“https://www.baidu.com”,针对于“http://localhost:88/hello?url=qq”的请求,转发到“https://www.qq.com/”

1)创建“application.yml”

spring:
  cloud:
    gateway:
      routes:
        - id: baidu_route
          uri: https://www.baidu.com
          predicates:
            - Query=url, baidu
        - id: qq_route
          uri: https://www.qq.com/
          predicates:
            - Query=url, qq

2)启动“gulimall-gateway”

3)测试

访问:http://localhost:88/hello?url=baidu

访问:http://localhost:88/hello?url=qq

5)前端基础

技术栈简介

1)VsCode使用

微软旗下的一款轻量级高性能的前段开发工具。

2)ES6

什么是es6?

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。

目标:是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

ECMAScript 和 JavaScript 的关系是:

前者是后者的规格,后者是前者的一种实现

ES6 与 ECMAScript 2015 的关系

2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。

ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。

3)Node.js

前端开发,少不了 node.js,Node.js是一个基于ChromeV8引擎的JavaScript运行环境

http://nodejs.cn/api/

我们关注与node.js的npm功能就行;

NPM 是随同 NodeJS—起安装的包管理工具,JavaScript-NPM,Java-Maven;

  1. 、官网下载安装node.js,幷使用node-v检査版本

  2. 、配置npm使用淘宝镜像

npm config set registry http://registry.npm.taobao.org/

4)Vue

Vue.js是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,并且非常容易学习,非常容易与其它库或已有项目整合。另一方面,Vue 完全有能力驱动采用单文件组件和Vue生态系统支持的库开发的复杂单页应用。

Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件 。

Vue.js 自身不是一个全能框架——它只聚焦于视图层。因此它非常容易学习,非常容易与其它库或已有项目整合。另一方面,在与相关工具和支持库一起使用时 ,Vue.js 也能完美地驱动复杂的单页应用。

优点

在有HTML CSS JavaScript的基础上,快速上手。

简单小巧的核心,渐进式技术栈,足以应付任何规模的应用。

20kb min+gzip 运行大小、超快虚拟 DOM 、最省心的优化。

目录/文件 说明
build 项目构建(webpack)相关代码
config 配置目录,包括端口号等。我们初学可以使用默认的。
node_modules npm 加载的项目依赖模块
src 包含了几个目录及文件:assets: 放置一些图片,如logo等。components: 目录里面放了一个组件文件,可以不用。App.vue: 项目入口文件,我们也可以直接将组件写这里,而不使用 components 目录。main.js: 项目的核心文件。
static 静态资源目录,如图片、字体等。
test 初始测试目录,可删除
.xxxx文件 这些是一些配置文件,包括语法配置,git配置等。
index.html 首页入口文件,你可以添加一些 meta 信息或统计代码啥的。
package.json 项目配置文件。
README.md 项目的说明文档,markdown 格式

5)Babel

babel是一个javaScript编译器,我们可以使用es的最新语法编程,而不用担心浏览器兼容问题,它会自动转换为浏览器兼容的代码。

6)Webpack

自动化项目构建工具

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xuRsntqW-1603345710001)(C:%5CUsers%5CRuic%5CDesktop%5C%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE%202020-10-13%20101751.jpg)]

ES6-let&const

示例


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>
    

    <script>
       // var 声明的变量往往会越域
       // let 声明的变量有严格局部作用域
//         {
      
//             var a = 1;
//             let b = 2;
//         }
//         console.log(a);  // 1
//         console.log(b);  // ReferenceError: b is not defined

    // var 可以声明多次
            // let 只能声明一次
//         var m = 1
//         var m = 2
//         let n = 3
// //         let n = 4
//         console.log(m)  // 2
//         console.log(n)  // Identifier 'n' has already been declared

        // var 会变量提升
        // let 不存在变量提升
//         console.log(x);  // undefined
//         var x = 10;
//         console.log(y);   //ReferenceError: y is not defined
//         let y = 20;

        // let
        // 1. const声明之后不允许改变
                // 2. 一但声明必须初始化,否则会报错
        const a = 1;
        a = 3; //Uncaught TypeError: Assignment to constant variable.
    
    script>

body>
html>

小结

  • var在{}之外也起作用
  • let在{}不起作用
  • var多次声明同一变量不会报错,let多次声明会报错,只能声明一次。
  • var 会变量提升(打印和定义可以顺序反)。let 不存在变量提升(顺序不能反)
  • let的const声明之后不允许改变

ES6-解构&字符串

示例

解构表达式


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>

    <script>
        //数组解构
        // let arr = [1,2,3];
        // // let a = arr[0];
        // // let b = arr[1];
        // // let c = arr[2];

        // let [a,b,c] = arr;
        // console.log(a,b,c)

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

        //对象解构
        const {
       name: abc, age, language } = person;
        console.log(abc, age, language)

        //4、字符串扩展
        let str = "hello.vue";
        console.log(str.startsWith("hello"));//true
        console.log(str.endsWith(".vue"));//true
        console.log(str.includes("e"));//true
        console.log(str.includes("hello"));//true

        //字符串模板
        let ss = `
hello world
`
; console.log(ss); // // 2、字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。 function fun() { return "这是一个函数" } let info = `我是${ abc},今年${ age + 10}了, 我想说: ${ fun()}`; console.log(info);
script> body> html>

小结

  • 支持let arr = [1,2,3]; let [a,b,c] = arr;这种语法
  • 支持对象解析:const { name: abc, age, language } = person; 冒号代表改名
  • 字符串函数
  • 支持一个字符串为多行
  • 占位符功能 ${}

ES6-箭头函数

Document

<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>

    <script>
        //在ES6以前,我们无法给一个函数参数设置默认值,只能采用变通写法:
        function add(a, b) {
      
            // 判断b是否为空,为空就给默认值1
            b = b || 1;
            return a + b;
        }
        // 传一个参数
        console.log(add(10));


        //现在可以这么写:直接给参数写上默认值,没传就会自动使用默认值
        function add2(a, b = 1) {
      
            return a + b;
        }
        console.log(add2(20));


        //2)、不定参数
        function fun(...values) {
      
            console.log(values.length)
        }
        fun(1, 2)      //2
        fun(1, 2, 3, 4)  //4

        //3)、箭头函数
        //以前声明一个方法
        // var print = function (obj) {
      
        //     console.log(obj);
        // }
        var print = obj => console.log(obj);
        print("hello");

        var sum = function (a, b) {
      
            c = a + b;
            return a + c;
        }

        var sum2 = (a, b) => a + b;
        console.log(sum2(11, 12));

        var sum3 = (a, b) => {
      
            c = a + b;
            return a + c;
        }
        console.log(sum3(10, 20))


        const person = {
      
            name: "jack",
            age: 21,
            language: ['java', 'js', 'css']
        }

        function hello(person) {
      
            console.log("hello," + person.name)
        }

        //箭头函数+解构
        var hello2 = ({
      name}) => console.log("hello," +name);
        hello2(person);

    script>
body>
html>

小结

  • 原来想要函数默认值得这么写b = b || 1; 现在可以直接写了function add2(a, b = 1) {
  • 函数不定参数function fun(...values) {
  • 支持箭头函数(lambda表达式),还支持使用{}结构传入对象的成员

ES6-对象优化


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>
    <script>
        const person = {
      
            name: "jack",
            age: 21,
            language: ['java', 'js', 'css']
        }

        console.log(Object.keys(person));//["name", "age", "language"]
        console.log(Object.values(person));//["jack", 21, Array(3)]
        console.log(Object.entries(person));//[Array(2), Array(2), Array(2)]

        const target = {
       a: 1 };
        const source1 = {
       b: 2 };
        const source2 = {
       c: 3 };

        //{a:1,b:2,c:3}
        Object.assign(target, source1, source2);

        console.log(target);//["name", "age", "language"]

        //2)、声明对象简写
        const age = 23
        const name = "张三"
        const person1 = {
       age: age, name: name }

        const person2 = {
       age, name }//声明对象简写
        console.log(person2);

        //3)、对象的函数属性简写
        let person3 = {
      
            name: "jack",
            // 以前:
            eat: function (food) {
      
                console.log(this.name + "在吃" + food);
            },
            //箭头函数this不能使用,对象.属性
            eat2: food => console.log(person3.name + "在吃" + food),
            eat3(food) {
      
                console.log(this.name + "在吃" + food);
            }
        }

        person3.eat("香蕉");
        person3.eat2("苹果")
        person3.eat3("橘子");

        //4)、对象拓展运算符

        // 1、拷贝对象(深拷贝)
        let p1 = {
       name: "Amy", age: 15 }
        let someone = {
       ...p1 }
        console.log(someone)  //{name: "Amy", age: 15}

        // 2、合并对象
        let age1 = {
       age: 15 }
        let name1 = {
       name: "Amy" }
        let p2 = {
      name:"zhangsan"}
        p2 = {
       ...age1, ...name1 } 
        console.log(p2)
    script>
body>

html>

小结

  • 可以获取map的键值对等Object.keys()、values、entries
  • Object.assgn(target,source1,source2) 合并
  • const person2 = { age, name }//声明对象简写
  • …代表取出该对象所有属性拷贝到当前对象。let someone = { …p1 }

E6-map和reduce


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
head>
<body>
    
    <script>
        //数组中新增了map和reduce方法。
        //map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
         let arr = ['1', '20', '-5', '3'];
         
        //  arr = arr.map((item)=>{
      
        //     return item*2
        //  });
         arr = arr.map(item=> item*2);

        

         console.log(arr);
        //reduce() 为数组中的每一个元素依次执行回调函数,不包括数组中被删除或从未被赋值的元素,
        //[2, 40, -10, 6]
        //arr.reduce(callback,[initialValue])
        /**
         1、previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
    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)

    
    script>
body>
html>

小结

  • map处理,arr = arr.map(item=> item*2);

  • reduce。arr.reduce((原来的值,处理后的值即return的值)=>{

ES6-promise异步编排


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Documenttitle>
    <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js">script>
head>
<body>
    <script>
        //1、查出当前用户信息
        //2、按照当前用户的id查出他的课程
        //3、按照当前课程id查出分数
        // $.ajax({
      
        //     url: "mock/user.json",
        //     success(data) {
      
        //         console.log("查询用户:", data);
        //         $.ajax({
      
        //             url: `mock/user_corse_${data.id}.json`,
        //             success(data) {
      
        //                 console.log("查询到课程:", data);
        //                 $.ajax({
      
        //                     url: `mock/corse_score_${data.id}.json`,
        //                     success(data) {
      
        //                         console.log("查询到分数:", data);
        //                     },
        //                     error(error) {
      
        //                         console.log("出现异常了:" + error);
        //                     }
        //                 });
        //             },
        //             error(error) {
      
        //                 console.log("出现异常了:" + error);
        //             }
        //         });
        //     },
        //     error(error) {
      
        //         console.log("出现异常了:" + error);
        //     }
        // });


        //1、Promise可以封装异步操作
        // let p = new Promise((resolve, reject) => { //传入成功解析,失败拒绝
        //     //1、异步操作
        //     $.ajax({
      
        //         url: "mock/user.json",
        //         success: function (data) {
      
        //             console.log("查询用户成功:", data)
        //             resolve(data);
        //         },
        //         error: function (err) {
      
        //             reject(err);
        //         }
        //     });
        // });

        // p.then((obj) => { //成功以后做什么
        //     return new Promise((resolve, reject) => {
      
        //         $.ajax({
      
        //             url: `mock/user_corse_${obj.id}.json`,
        //             success: function (data) {
      
        //                 console.log("查询用户课程成功:", data)
        //                 resolve(data);
        //             },
        //             error: function (err) {
      
        //                 reject(err)
        //             }
        //         });
        //     })
        // }).then((data) => { //成功以后干什么
        //     console.log("上一步的结果", data)
        //     $.ajax({
      
        //         url: `mock/corse_score_${data.id}.json`,
        //         success: function (data) {
      
        //             console.log("查询课程得分成功:", data)
        //         },
        //         error: function (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)=>{
       //失败的话catch
                console.log("出现异常",err)
            });

    script>
body>

html>

corse_score_10.json 得分

{
     
    "id": 100,
    "score": 90
}

user.json 用户

{
     
    "id": 1,
    "name": "zhangsan",
    "password": "123456"
}

user_corse_1.json 课程

{
     
    "id": 10,
    "name": "chinese"
}

小结

以前嵌套ajax的时候很繁琐。

  • 把Ajax封装到Promise中,赋值给let p
  • 在Ajax中成功使用resolve(data),失败使用reject(err)
  • p.then().catch()

ES6-模块化

模块化就是把代码进行拆分,方便重复利用。类似于java中的导包,而JS换了个概念,是导模块。

模块功能主要有两个命令构成 export 和import

  • export用于规定模块的对外接口
  • import用于导入其他模块提供的功能

user.js

var name = "jack"
var age = 21
function add(a,b){
     
    return a + b;
}

export {
     name,age,add}

hello.js

// export const util = {
     
//     sum(a, b) {
     
//         return a + b;
//     }
// }

export default {
     
    sum(a, b) {
     
        return a + b;
    }
}
// export {util}

//`export`不仅可以导出对象,一切JS变量都可以导出。比如:基本类型变量、函数、数组、对象。

main.js

import abc from "./hello.js"
import {
     name,add} from "./user.js"

abc.sum(1,2);
console.log(name);
add(1,3);

vue-介绍&helloword

MVVM思想

  • M:module 包括数据和一些基本操作
  • V:view 视图,页面渲染结果
  • VM:View-module,模型与视图间的双向操作(无需开发人员干涉)

视图和数据通过VM绑定起来,模型里有变化会自动地通过Directives填写到视图中,视图表单中添加了内容也会自动地通过DOM Listeners保存到模型中。

教程:https://cn.vuejs.org/v2/guide/

安装:

  • 直接下载并用

    vue-使用Vue脚手架进行模块化开发

    1)npm install webpack -g

    全局安装Webpack。

    2)npm install -g @vue/cli-init

    全局安装vue脚手架。

    3)vue init webpack 项目名

    初始化vue企业化项目。

    **4)npm start & npm run dev **

    项目的package.json中有scripts,代表我们能运行的命令;

    npm start & npm run dev 启动项目。

    vue-整合ElementUi快速开发

    官网: https://element.eleme.cn/#/zh-CN/component/installation

    安装

    npm i element-ui -S
    

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

    import ElementUI  from 'element-ui'
    import 'element-ui/lib/theme-chalk/index.css';
    
    Vue.use(ElementUI);
    

    6)商品服务

    API-三级分类-查询-递归树形结构数据获取

    在注册中心中“product”命名空间中,创建“gulimall-product.yml”配置文件:

    image-20200425153735737

    将“application.yml”内容拷贝到该配置文件中

    server:
      port: 10000
    
    spring:
      datasource:
        #MySQL配置
        driverClassName: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://192.168.137.14:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: root
      application:
        name: gulimall-product
      cloud:
        nacos:
          discovery:
            server-addr: 192.168.137.14:8848
    
    
    mybatis-plus:
      global-config:
        db-config:
          id-type: auto
      mapper-locations: classpath:/mapper/**/*.xml
    

    在本地创建“bootstrap.properties”文件,指明配置中心的位置和使用到的配置文件:

    spring.application.name=gulimall-product
    spring.cloud.nacos.config.server-addr=192.168.137.14:8848
    spring.cloud.nacos.config.namespace=3c50ffaa-010b-4b59-9372-902e35059232
    spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-product.yml
    spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
    spring.cloud.nacos.config.extension-configs[0].refresh=true
    

    然后启动gulimall-product,查看到该服务已经出现在了nacos的注册中心中了

    修改“com.bigdata.gulimall.product.service.CategoryService”类,添加如下代码:

        /**
         * 列表
         */
        @RequestMapping("/list/tree")
        public List<CategoryEntity> list(){
         
            List<CategoryEntity> categoryEntities = categoryService.listWithTree();
    
            return categoryEntities;
        }
    

    测试:http://localhost:10000/product/category/list/tree

    image-20200425154348716

    如何区别是哪种分类级别?

    答:可以通过分类的parent_cid来进行判断,如果是一级分类,其值为0.

         /**
         * 列表
         */
        @RequestMapping("/list/tree")
        public List<CategoryEntity> list(){
         
            List<CategoryEntity> categoryEntities = categoryService.listWithTree();
            //找到所有的一级分类
            List<CategoryEntity> level1Menus = categoryEntities.stream()
                    .filter(item -> item.getParentCid() == 0)
                    .map(menu->{
         
                        menu.setChildCategoryEntity(getChildrens(menu,categoryEntities));
                        return menu;
                    })
                    .sorted((menu1, menu2) -> {
         
    
                      return (menu1.getSort() ==null ? 0:menu1.getSort())- (menu2.getSort()==null?0:menu2.getSort());
    
                    })
                    .collect(Collectors.toList());
    
    
    
            return level1Menus;
        }
    
        public List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
         
    
            List<CategoryEntity> childrens = all.stream().filter(item -> {
         
                return item.getParentCid() == root.getCatId();
            }).map(item -> {
         
                item.setChildCategoryEntity(getChildrens(item, all));
                return item;
            }).sorted((menu1, menu2) -> {
         
                return (menu1.getSort() ==null ? 0:menu1.getSort())- (menu2.getSort()==null?0:menu2.getSort());
            }).collect(Collectors.toList());
    
            return childrens;
        }
    

    下面是得到的部分JSON数据

    [
      {
         
        "catId": 1,
        "name": "图书、音像、电子书刊",
        "parentCid": 0,
        "catLevel": 1,
        "showStatus": 1,
        "sort": 0,
        "icon": null,
        "productUnit": null,
        "productCount": 0,
        "childCategoryEntity": [
          {
         
            "catId": 22,
            "name": "电子书刊",
            "parentCid": 1,
            "catLevel": 2,
            "showStatus": 1,
            "sort": 0,
            "icon": null,
            "productUnit": null,
            "productCount": 0,
            "childCategoryEntity": [
              {
         
                "catId": 165,
                "name": "电子书",
                "parentCid": 22,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              },
              {
         
                "catId": 166,
                "name": "网络原创",
                "parentCid": 22,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              },
              {
         
                "catId": 167,
                "name": "数字杂志",
                "parentCid": 22,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              },
              {
         
                "catId": 168,
                "name": "多媒体图书",
                "parentCid": 22,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              }
            ]
          },
          {
         
            "catId": 23,
            "name": "音像",
            "parentCid": 1,
            "catLevel": 2,
            "showStatus": 1,
            "sort": 0,
            "icon": null,
            "productUnit": null,
            "productCount": 0,
            "childCategoryEntity": [
              {
         
                "catId": 169,
                "name": "音乐",
                "parentCid": 23,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              },
              {
         
                "catId": 170,
                "name": "影视",
                "parentCid": 23,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              },
              {
         
                "catId": 171,
                "name": "教育音像",
                "parentCid": 23,
                "catLevel": 3,
                "showStatus": 1,
                "sort": 0,
                "icon": null,
                "productUnit": null,
                "productCount": 0,
                "childCategoryEntity": []
              }
            ]
          },
    

    API-三级分类-配置网关路由和路径重写

    启动后端项目renren-fast

    启动前端项目renren-fast-vue:

    npm run dev
    

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

    创建一级菜单:

    image-20200425164019287

    创建完成后,在后台的管理系统中会创建一条记录:

    image-20200425164201813

    然后创建子菜单:

    image-20200425164509143

    创建renren-fast-vue\src\views\modules\product目录,子所以是这样来创建,是因为product/category,对应于product-category

    在该目录下,新建“category.vue”文件:

    刷新页面出现404异常,查看请求发现,请求的是“http://localhost:8080/renren-fast/product/category/list/tree”

    image-20200425173615149

    这个请求是不正确的,正确的请求是:http://localhost:10000/product/category/list/tree,

    修正这个问题:

    替换“static\config\index.js”文件中的“window.SITE_CONFIG[‘baseUrl’]”

    替换前:

    window.SITE_CONFIG['baseUrl'] = 'http://localhost:8080/renren-fast';
    

    替换后:

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

    http://localhost:88,这个地址是我们网关微服务的接口。

    这里我们需要通过网关来完成路径的映射,因此将renren-fast注册到nacos注册中心中,并添加配置中心

    application:
        name: renren-fast
      cloud:
        nacos:
          discovery:
            server-addr: 192.168.137.14:8848
    
          config:
            name: renren-fast
            server-addr: 192.168.137.8848
            namespace: ee409c3f-3206-4a3b-ba65-7376922a886d
    

    配置网关路由,前台的所有请求都是经由“http://localhost:88/api”来转发的,在“gulimall-gateway”中添加路由规则:

            - id: admin_route
              uri: lb://renren-fast
              predicates:
                - Path=/api/**
    

    但是这样做也引入了另外的一个问题,再次访问:http://localhost:8001/#/login,发现验证码不再显示:

    分析原因:

    1. 现在的验证码请求路径为,http://localhost:88/api/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
    2. 原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

    在admin_route的路由规则下,在访问路径中包含了“api”,因此它会将它转发到renren-fast,网关在转发的时候,会使用网关的前缀信息,为了能够正常的取得验证码,我们需要对请求路径进行重写

    关于请求路径重写:

    6.16. The RewritePath GatewayFilter Factory

    The RewritePath GatewayFilter factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path. The following listing configures a RewritePath``GatewayFilter:

    Example 41. application.yml

    spring:
      cloud:
        gateway:
          routes:
          - id: rewritepath_route
            uri: https://example.org
            predicates:
            - Path=/foo/**
            filters:
            - RewritePath=/red(?>/?.*), $\{
         segment}
    

    For a request path of /red/blue, this sets the path to /blue before making the downstream request. Note that the $should be replaced with $\ because of the YAML specification.

    修改“admin_route”路由规则:

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

    再次访问:http://localhost:8001/#/login,验证码能够正常的加载了。

    但是很不幸新的问题又产生了,访问被拒绝了

    image-20200425192722821

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

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

    image-20200425192902637

    跨域流程:

    image-20200425193136641

    image-20200425193523849

    image-20200425193614185

    API-三级分类-网关统一配置跨域

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

    @Configuration
    public class GulimallCorsConfiguration {
         
    
        @Bean
        public CorsWebFilter corsWebFilter(){
         
            UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.addAllowedHeader("*");
            corsConfiguration.addAllowedMethod("*");
            corsConfiguration.addAllowedOrigin("*");
            corsConfiguration.setAllowCredentials(true);
            
            source.registerCorsConfiguration("/**",corsConfiguration);
            return new CorsWebFilter(source);
        }
    }
    

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

    image-20200425195437299

    http://localhost:8001/renre已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)n-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6

    出现了多个请求,并且也存在多个跨源请求。

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

    在显示分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在

    image-20200425213240724

    这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree

    但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。

    解决方法就是定义一个product路由规则,进行路径重写:

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

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

    API-三级分类-树形展示三级分类数据

    //页面初始化的时候就会调用该方法,将获取到的菜单数据赋值给本地变量,然后通过树形控件进行显示。  
    getMenus() {
         
          this.$http({
         
            url: this.$http.adornUrl("/product/category/list/tree"),
            method: "get",
          }).then(({
          data }) => {
         
            console.log("成功获取到菜单数据...", data.data);
            this.menus = data.data;
          });
        }
    
    //该属性标明了哪个属性是树形控件的子菜单,和要显示的值是哪个属性  
    defaultProps: {
         
            children: "children",
            label: "name",
          },
    
      
          
            {
        { node.label }}
            
              Append
              edit
              Delete
            
          
        
    

    整个el-tree在接收到我们提供的数据后展示的效果是这个样子的:

    分布式基础篇_第7张图片

    也没啥问题。

    API-三级分类-删除-页面效果

    //添加delete和append标识,并且增加复选框(前面已经加过)
    
     
        <span class="custom-tree-node" slot-scope="{ node, data }">
          <span>{
        { node.label }}span>
          <span>
              //只有当前节点的层级小于等于2才可以添加新节点,因为节点最高3级
            <el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">Appendel-button>
            //只有当前节点没有子节点的时候才可以删除该节点
              <el-button
              v-if="node.childNodes.length == 0"
              type="text"
              size="mini"
              @click="() => remove(node, data)"
            >Deleteel-button>
          span>
        span>
      el-tree>
    

    页面效果:

    分布式基础篇_第8张图片

    API-三级分类-删除-逻辑删除

    测试删除数据,打开postman输入“ http://localhost:88/api/product/category/delete ”,请求方式设置为POST,为了比对效果,可以在删除之前查询数据库的pms_category表:

    image-20200426112814069

    由于delete请求接收的是一个数组,所以这里使用JSON方式,传入了一个数组:

    image-20200426113003531

    再次查询数据库能够看到cat_id为1000的数据已经被删除了。

    修改“com.bigdata.gulimall.product.controller.CategoryController”类,添加如下代码:

     @RequestMapping("/delete")
        public R delete(@RequestBody Long[] catIds){
         
            //删除之前需要判断待删除的菜单那是否被别的地方所引用。
    //		categoryService.removeByIds(Arrays.asList(catIds));
    
            categoryService.removeMenuByIds(Arrays.asList(catIds));
            return R.ok();
        }
    

    com.bigdata.gulimall.product.service.impl.CategoryServiceImpl

        @Override
        public void removeMenuByIds(List<Long> asList) {
         
            //TODO 检查当前的菜单是否被别的地方所引用
            categoryDao.deleteBatchIds(asList);
        }
    

    然而多数时候,我们并不希望删除数据,而是标记它被删除了,这就是逻辑删除;

    可以设置show_status为0,标记它已经被删除。

    image-20200426115332899

    mybatis-plus的逻辑删除:

    image-20200426115420393

    配置全局的逻辑删除规则,在“src/main/resources/application.yml”文件中添加如下内容:

    mybatis-plus:
      global-config:
        db-config:
          id-type: auto
          logic-delete-value: 1
          logic-not-delete-value: 0
    

    mybatisPlus默认的逻辑删除规则是1不显示,0显示,和我们需求正好相反,我们可以给逻辑删除字段添加上@TableLogic自定义删除规则。

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

    然后在POSTMan中测试一下是否能够满足需要。另外在“src/main/resources/application.yml”文件中,设置日志级别,打印出SQL语句:

    logging:
      level:
        com.bigdata.gulimall.product: debug
    

    打印的日志:

     ==>  Preparing: UPDATE pms_category SET show_status=0 WHERE cat_id IN ( ? ) AND show_status=1 
     ==> Parameters: 1431(Long)
     <==    Updates: 1
     get changedGroupKeys:[]
    //发现逻辑删除只是把该数据改为了0即是不显示,并没有真正的删除数据。
    

    API-三级分类-删除-删除效果细化

    前面使用PostMan测试了逻辑删除,也没啥问题,现在就完成通过我们的页面点击Delete删除菜单的功能,点击Delete会调用这么一个方法(传入当前节点信息和当前节点数据信息):

    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(() => {
         });
    
          console.log("remove", node, data);
        },
      },
    

    也没啥问题。

    API-三级分类-新增-新增效果完成

    点击菜单上的Append按钮会触发这个方法,传入当前被点击节点数据信息。给准备新增的节点设置一些默认值,然后打开新增文本框。

         <el-button
                v-if="node.level <= 2"
                type="text"
                size="mini"
                @click="() => append(data)"
                >Appendel-button
              >
    
    append(data) {
         
          console.log("append", data); //打印当前被点击节点信息
          this.dialogType = "add";     //设置文本框类型为add
          this.title = "添加分类";     //设置文本框标题为添加分类
          this.dialogVisible = true;  //打开添加文本框
          this.category.parentCid = data.catId;  //设置新增节点的父节点信息
          this.category.catLevel = data.catLevel * 1 + 1; //设置新增节点的层级信息
          this.category.catId = null;        //...
          this.category.name = "";
          this.category.icon = "";
          this.category.productUnit = "";
          this.category.sort = 0;
          this.category.showStatus = 1;
        },
    
        //用户点击确定,会调用submitData方法,该方法会收集所有新节点的数据,一并提交给后台。
        <el-dialog
          :title="title"
          :visible.sync="dialogVisible"
          width="30%"
          :close-on-click-modal="false"
        >
          <el-form :model="category">
            <el-form-item label="分类名称">
              <el-input v-model="category.name" autocomplete="off"></el-input>
            </el-form-item>
            <el-form-item label="图标">
              <el-input v-model="category.icon" autocomplete="off"></el-input>
            </el-form-item>
            <el-form-item label="计量单位">
              <el-input
                v-model="category.productUnit"
                autocomplete="off"
              ></el-input>
            </el-form-item>
          </el-form>
          <span slot="footer" class="dialog-footer">
            <el-button @click="dialogVisible = false">取 消</el-button>
            <el-button type="primary" @click="submitData">确 定</el-button>
          </span>
        </el-dialog>
    
     submitData() {
                    //判断状态
          if (this.dialogType == "add") {
         
            this.addCategory();
          }
          if (this.dialogType == "edit") {
         
            this.editCategory();
          }
        },
    
      addCategory() {
         
          console.log("提交的三级分类数据", this.category);
          this.$http({
         
            url: this.$http.adornUrl("/product/category/save"),
            method: "post",
            data: this.$http.adornData(this.category, false),
          }).then(({
          data }) => {
         
            this.$message({
         
              message: "菜单保存成功",
              type: "success",
            });
            //关闭对话框
            this.dialogVisible = false;
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [this.category.parentCid];
          });
        },
    

    也没啥问题。

    API-三级分类-修改-基本修改效果完成

    用户点击edit,触发edit方法传入当前被点击节点信息,设置文本框类型标题后打开修改文本框,发送请求获取该节点最新数据用于修改文本框回显,供用户修改。

    <el-button type="text" size="mini" @click="edit(data)"
                >editel-button
              >
    
     edit(data) {
         
          console.log("要修改的数据", data);
          this.dialogType = "edit";
          this.title = "修改分类";
          this.dialogVisible = true;
    
          //发送请求获取当前节点最新的数据
          this.$http({
         
            url: this.$http.adornUrl(`/product/category/info/${
           data.catId}`),
            method: "get",
          }).then(({
          data }) => {
         
            //请求成功
            console.log("要回显的数据", data);
            this.category.name = data.data.name;
            this.category.catId = data.data.catId;
            this.category.icon = data.data.icon;
            this.category.productUnit = data.data.productUnit;
            this.category.parentCid = data.data.parentCid;
            this.category.catLevel = data.data.catLevel;
            this.category.sort = data.data.sort;
            this.category.showStatus = data.data.showStatus;
            /**
             *         parentCid: 0,
            catLevel: 0,
            showStatus: 1,
            sort: 0,
             */
          });
        },
    
    
        submitData() {
           //判断文本框类型
          if (this.dialogType == "add") {
         
            this.addCategory();
          }
          if (this.dialogType == "edit") {
         
            this.editCategory();
          }
        },
    
      //来到这个方法:修改三级分类数据
        editCategory() {
         
          var {
          catId, name, icon, productUnit } = this.category;
          this.$http({
         
            url: this.$http.adornUrl("/product/category/update"),
            method: "post",
            data: this.$http.adornData({
          catId, name, icon, productUnit }, false),
          }).then(({
          data }) => {
         
            this.$message({
         
              message: "菜单修改成功",
              type: "success",
            });
            //关闭对话框
            this.dialogVisible = false;
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = [this.category.parentCid];
          });
        },
    

    也没啥问题。

    API-三级分类-修改-拖拽效果

    
    

    这样菜单就可以拖拽了,但是哪个菜单能被拖拽哪个菜单不能是有依据的,得按照业务需求来,不能随便拖拽,

    ​ 因此有一个限制拖拽的回调方法:

    在这里插入图片描述

    根据我们的需求而言,三级分类只能有三层,那么一个节点能够被拖拽需要满足:当前正在拖动的节点+父节点所在的层级深度不大于3即可拖拽。

     allowDrop(draggingNode, dropNode, type) {
         
          //1、被拖动的当前节点以及所在的父节点总层数不能大于3
    
          //1)、被拖动的当前节点总层数
          console.log("allowDrop:", draggingNode, dropNode, type);
          //
          this.countNodeLevel(draggingNode);
          //当前正在拖动的节点+父节点所在的深度不大于3即可
          let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
          console.log("深度:", deep);
    
          //   this.maxLevel
          if (type == "inner") {
         
            // console.log(
            //   `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`
            // );
            return deep + dropNode.level <= 3;
          } else {
         
            return deep + dropNode.parent.level <= 3;
          }
        },
    

    这样就限制了节点的拖拽,也没啥问题。

    API-三级分类-修改-拖拽数据收集

    前面拖拽基本效果算是完成了,但是拖拽成功后,并没有经过后台,一刷新拖拽后的效果就没了,我们希望拖拽节点成功后发送请求

    给后台,修改被拖拽节点的信息,该怎么做呢?在我们拖拽成功后还有一个回调方法,我们可以利用它,当一拖拽成功后,就利用它

    给后台发送请求,修改最新的节点数据。但是我们又说如果拖拽成功一次就给后台发一次请求,会不会太麻烦了?我们可以这样,每

    拖拽成功一次,就把这次拖拽成功的数据给收集到数组里面,每拖拽一次就收集一次,然后搞一个批量操作的按钮,只有当用户点击按钮

    之后,才会拿着收集好的数据真正的去后台批量的修改分类。

    在这里插入图片描述

     handleDrop(draggingNode, dropNode, dropType, ev) {
         
          console.log("handleDrop: ", draggingNode, dropNode, dropType);
          //1、当前节点最新的父节点id
          let pCid = 0;
          let siblings = null;
          if (dropType == "before" || dropType == "after") {
         
            pCid =
              dropNode.parent.data.catId == undefined
                ? 0	
                : dropNode.parent.data.catId;
            siblings = dropNode.parent.childNodes;
          } else {
         
            pCid = dropNode.data.catId;
            siblings = dropNode.childNodes;
          }
          this.pCid.push(pCid);                //收集父节点
    
          //2、当前拖拽节点的最新顺序,
          for (let i = 0; i < siblings.length; i++) {
         
            if (siblings[i].data.catId == draggingNode.data.catId) {
         
              //如果遍历的是当前正在拖拽的节点
              let catLevel = draggingNode.level;
              if (siblings[i].level != draggingNode.level) {
         
                //当前节点的层级发生变化
                catLevel = siblings[i].level;
                //修改他子节点的层级
                this.updateChildNodeLevel(siblings[i]);
              }
              this.updateNodes.push({
         
                catId: siblings[i].data.catId,
                sort: i,
                parentCid: pCid,
                catLevel: catLevel,
              });
            } else {
         
              this.updateNodes.push({
          catId: siblings[i].data.catId, sort: i });
            }
            if (this.updateNodes.length > 0) {
         
              this.flag = false;
            }
          }
    
          //3、当前拖拽节点的最新层级
          console.log("updateNodes", this.updateNodes);  //该属性保存了所有需要修改的拖拽节点数据。
        },
        updateChildNodeLevel(node) {
         
          if (node.childNodes.length > 0) {
         
            for (let i = 0; i < node.childNodes.length; i++) {
         
              var cNode = node.childNodes[i].data;
              this.updateNodes.push({
         
                catId: cNode.catId,
                catLevel: node.childNodes[i].level,
              });
              this.updateChildNodeLevel(node.childNodes[i]);
            }
          }
        },
    

    API-三级分类-修改-拖拽功能完成&批量拖拽效果

    接着前面的逻辑,用户每拖拽成功一次,被拖拽节点的最新信息就会被保存到updateNodes这个数组里面,

    只有用户点击了批量保存按钮,才会发送请求给后台来保存这次拖拽操作。(如果没有任何拖拽操作,批量保存按钮不能按,判断数组里是否有值就行了,有值就说明有拖拽操作,批量保存按钮可用。)

      <el-button v-if="draggable" @click="batchSave" :disabled="flag"
          >保存拖拽el-button
        >
    
    //用户点击批量保存会带上所有因为拖拽需要被修改的分类数据来到后台的批量修改方法。  
    batchSave() {
         
          this.$http({
         
            url: this.$http.adornUrl("/product/category/update/sort"),
            method: "post",
            data: this.$http.adornData(this.updateNodes, false),
          }).then(({
          data }) => {
         
            this.$message({
         
              message: "菜单顺序等修改成功",
              type: "success",
            });
            //刷新出新的菜单
            this.getMenus();
            //设置需要默认展开的菜单
            this.expandedKey = this.pCid;
            this.updateNodes = [];
            this.maxLevel = 0;
            // this.pCid = 0;
            if (this.updateNodes.length == 0) {
         
              this.flag = true;
            }
          });
        },
    

    也没啥问题。

    还有就是,为了用户体验,拖拽功能还需要根据用户需求,用户想用的时候用,不想用的时候就不用

    我们可以加上一个开关按钮供用户选择是开启还是不开启。

        <el-switch
          v-model="draggable"
          active-text="开启拖拽"
          inactive-text="关闭拖拽"
        ></el-switch>
    

    我们都知道,拖拽功能的开关是由’draggable’属性决定的

    我们给这个开关绑上这个属性,让这个开关控制这个属性的值,就相当于控制了拖拽功能的开关(当开关开时,draggable属性变为true,拖拽就可以用了,当开关关时,draggable就变为了false,拖拽也就关了),到此拖拽功能就算是做完了。

    API-三级分类-删除-批量删除&小结

    用户通过选中复选框,来批量选择要删除的分类数据,当点击批量删除按钮时,需要获取到所有被选中的分类数据id,一并传到后台,完成批量删除效果,如下图所示(当没有选中节点时,批量删除不可用):

    分布式基础篇_第9张图片

    //点击批量删除会触发batchDelete这个方法
    <el-button :disabled="flg" type="danger" @click="batchDelete"
          >批量删除el-button
        >
    
     batchDelete() {
         
          let catIds = [];
          let checkedNodes = this.$refs.menuTree.getCheckedNodes();
          console.log("被选中的元素", checkedNodes);
          for (let i = 0; i < checkedNodes.length; i++) {
         
            catIds.push(checkedNodes[i].catId);
          }
          this.$confirm(`是否批量删除【${
           catIds}】菜单?`, "提示", {
         
            confirmButtonText: "确定",
            cancelButtonText: "取消",
            type: "warning",
          })
            .then(() => {
         
              this.$http({
         
                url: this.$http.adornUrl("/product/category/delete"),
                method: "post",
                data: this.$http.adornData(catIds, false),
              }).then(({
          data }) => {
         
                this.$message({
         
                  message: "菜单批量删除成功",
                  type: "success",
                });
                this.getMenus();
                this.flg = true;
              });
            })
            .catch(() => {
         });
        },
    

    点击批量删除,获取到所有被选中节点的id,依次放入数组,传到后台批量删除即可。

    也没啥问题。

    API-品牌管理-使用逆向工程的前后端代码

    1)创建路由菜单

    image-20200428164054517

    2)将“”逆向工程得到的resources\src\views\modules\product文件拷贝到gulimall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件

    brand.vue brand-add-or-update.vue

    但是显示的页面没有新增和删除功能,这是因为权限控制的原因,

    image-20200428170325515

    新增
    批量删除
    

    查看“isAuth”的定义位置:

    image-20200428170437592

    它是在“index.js”中定义,现在将它设置为返回值为true,即可显示添加和删除功能。

    再次刷新页面能够看到,按钮已经出现了:

    image-20200428170644511

    测试一下基本的增删改查,也都没啥问题,所以,一句话,以后一些基本的增删改查操作都可以使用逆向工程,逆向工程非常强大,

    连页面代码都会生成,极大的提高了我们的开发效率,一些比较复杂的业务代码,我们可以自己编写,基本的增删改查代码使用逆向

    工程即可。

    API-品牌管理-效果优化与快速显示开关

    显示状态逆向工程默认是根据数据库的字段描述定义的,0不显示,1显示,用户体验不好,所以我们可以使用一个开关按钮来控制它的显隐。

    
    

    效果:

    分布式基础篇_第10张图片

    brand-add-or-update也加上

     
        
     
    
    //更新开关的状态,当用户按了开关就会发送请求给后台修改品牌状态。
        updateBrandStatus(data) {
         
          console.log("最新状态", data);
          let {
         brandId,showStatus} = data;
          this.$http({
         
            url: this.$http.adornUrl("/product/brand/update"),
            method: "post",
            data: this.$http.adornData({
         brandId,showStatus}, false)
          }).then(({
          data }) => {
         
    
            this.$message({
         
              message: "状态更新成功",
              type: "success"
            });
    
          });
        },
    

    也没啥问题。

    API-品牌管理-云存储开通与使用

    在新增品牌信息或者修改品牌信息的时候,涉及到了文件上传的功能,以前我们编写单体应用的时候,可以把上传的文件保存在本地机器A上,但是由于分布式架构的来临,如果上传的文件还保存在本地机器,那么你在本地机器是可以访问到图片的,因为图片本来就在你的机器上,但是如果其他服务也需要获取这个图片,但是这个服务是在另外一台机器上,这台机器是没有A机器上的图片的,这就是痛点所在。

    这里我们选择将数据上传到分布式文件服务器上,这样所有服务器都去访问公共的云存储服务器,就不存在图片访问不到的问题了。

    这里我们选择将图片放置到阿里云上,使用对象存储。

    阿里云上使使用对象存储方式:

    image-20200428182755992

    创建Bucket

    image-20200428183041570

    上传文件:

    image-20200428183213694

    上传成功后,取得图片的URL

    image-20200428183644020

    这种方式是手动上传图片,没啥问题,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。

    上传模型:(用户通过web界面点击上传图片的时候,会先给我们后台发送一个请求,要到一个服务端签名核验身份,要到了签名之后就可以直接上传图片到云存储服务器了。)

    image-20200428184029655

    查看阿里云关于文件上传的帮助: https://help.aliyun.com/document_detail/32009.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ

    API-品牌管理-OSS整合测试

    前面我们测试了云存储的使用,下面我们就将上传图片达到云存储的操作整合进我们的代码里进行一个实现。

    1)添加依赖包

    在Maven项目中加入依赖项(推荐方式)

    在 Maven 工程中使用 OSS Java SDK,只需在 pom.xml 中加入相应依赖即可。以 3.8.0 版本为例,在 内加入如下内容:

    <dependency>
        <groupId>com.aliyun.ossgroupId>
        <artifactId>aliyun-sdk-ossartifactId>
        <version>3.8.0version>
    dependency>
    

    2)上传文件流

    以下代码用于上传文件流:

    // Endpoint以杭州为例,其它Region请按实际情况填写。
    String endpoint = "http://oss-cn-hangzhou.aliyuncs.com";
    // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
    String accessKeyId = "";
    String accessKeySecret = "";
    
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
    // 上传文件流。
    InputStream inputStream = new FileInputStream("");
    ossClient.putObject("", "", inputStream);
    
    // 关闭OSSClient。
    ossClient.shutdown();
    

    endpoint的取值:

    image-20200428190553350

    accessKeyId和accessKeySecret需要创建一个RAM账号:

    image-20200428190532924

    创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。

    另外还需要添加访问控制权限:

    image-20200428191518591

    @Test
        public void testUpload() throws FileNotFoundException {
         
            // Endpoint以杭州为例,其它Region请按实际情况填写。
            String endpoint = "oss-cn-shanghai.aliyuncs.com";
            // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
            String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi";
            String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw";
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
            // 上传文件流。
            InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\timg.jpg");
            ossClient.putObject("gulimall-images", "time.jpg", inputStream);
    
            // 关闭OSSClient。
            ossClient.shutdown();
            System.out.println("上传成功.");
        }
    

    更为简单的使用方式,是使用SpringCloud Alibaba

    image-20200428195507730

    详细使用方法,见: https://help.aliyun.com/knowledge_detail/108650.html

    (1)添加依赖

            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alicloud-ossartifactId>
                <version>2.2.0.RELEASEversion>
            dependency>
    

    (2)创建“AccessKey ID”和“AccessKeySecret”

    (3)配置key,secret和endpoint相关信息

          access-key: LTAI4G4W1RA4JXz2QhoDwHhi
          secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw
          oss:
            endpoint: oss-cn-shanghai.aliyuncs.com
    

    (4)注入OSSClient并进行文件上传下载等操作

    image-20200428224840535

    但是这样来做还是比较麻烦,如果以后的上传任务都交给gulimall-product来完成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。

    其他方式

    1)新建gulimall-third-party

    2)添加依赖,将原来gulimall-common中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中

            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-starter-alicloud-ossartifactId>
                <version>2.2.0.RELEASEversion>
            dependency>
    
            <dependency>
                <groupId>com.bigdata.gulimallgroupId>
                <artifactId>gulimall-commonartifactId>
                <version>1.0-SNAPSHOTversion>
                <exclusions>
                    <exclusion>
                        <groupId>com.baomidougroupId>
                        <artifactId>mybatis-plus-boot-starterartifactId>
                    exclusion>
                exclusions>
            dependency>
    

    另外也需要在“pom.xml”文件中,添加如下的依赖管理

    <dependencyManagement>
    
            <dependencies>
                <dependency>
                    <groupId>org.springframework.cloudgroupId>
                    <artifactId>spring-cloud-dependenciesartifactId>
                    <version>${spring-cloud.version}version>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
                <dependency>
                    <groupId>com.alibaba.cloudgroupId>
                    <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                    <version>2.2.1.RELEASEversion>
                    <type>pomtype>
                    <scope>importscope>
                dependency>
            dependencies>
        dependencyManagement>
    

    3)在主启动类中开启服务的注册和发现

    @EnableDiscoveryClient
    

    4)在nacos中注册

    (1)创建命名空间“ gulimall-third-party ”

    image-20200429075831984

    (2)在“ gulimall-third-party”命名空间中,创建“ gulimall-third-party.yml”文件

    spring:
      cloud:
        alicloud:
          access-key: LTAI4G4W1RA4JXz2QhoDwHhi
          secret-key: R99lmDOJumF2x43ZBKT259Qpe70Oxw
          oss:
            endpoint: oss-cn-shanghai.aliyuncs.com
    

    5)编写配置文件

    application.yml

    server:
      port: 30000
    
    spring:
      application:
        name: gulimall-third-party
      cloud:
        nacos:
          discovery:
            server-addr: 192.168.137.14:8848
    
    logging:
      level:
        com.bigdata.gulimall.product: debug
    

    bootstrap.properties

    spring.cloud.nacos.config.name=gulimall-third-party
    spring.cloud.nacos.config.server-addr=192.168.137.14:8848
    spring.cloud.nacos.config.namespace=f995d8ee-c53a-4d29-8316-a1ef54775e00
    spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-third-party.yml
    spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
    spring.cloud.nacos.config.extension-configs[0].refresh=true
    

    6) 编写测试类

    package com.bigdata.gulimall.thirdparty;
    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.OSSClient;
    import com.aliyun.oss.OSSClientBuilder;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.InputStream;
    
    @SpringBootTest
    class GulimallThirdPartyApplicationTests {
         
    
    
        @Autowired
        OSSClient ossClient;
    
        @Test
        public void testUpload() throws FileNotFoundException {
         
            // Endpoint以杭州为例,其它Region请按实际情况填写。
            String endpoint = "oss-cn-shanghai.aliyuncs.com";
            // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。
            String accessKeyId = "LTAI4G4W1RA4JXz2QhoDwHhi";
            String accessKeySecret = "R99lmDOJumF2x43ZBKT259Qpe70Oxw";
    
            // 创建OSSClient实例。
            OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
             //上传文件流。
            InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\timg.jpg");
            ossClient.putObject("gulimall-images", "time3.jpg", inputStream);
    
            // 关闭OSSClient。
            ossClient.shutdown();
            System.out.println("上传成功.");
        }
    
    }
    

    https://help.aliyun.com/document_detail/31926.html?spm=a2c4g.11186623.6.1527.228d74b8V6IZuT

    API-品牌管理-OSS获取服务端签名

    背景

    采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。因此,OSS提供了服务端签名后直传的方案。

    原理介绍

    img

    服务端签名后直传的原理如下:

    1. 用户发送上传Policy请求到应用服务器。
    2. 应用服务器返回上传Policy和签名给用户。
    3. 用户直接上传数据到OSS。

    编写“com.bigdata.gulimall.thirdparty.controller.OssController”类:

    package com.bigdata.gulimall.thirdparty.controller;
    
    import com.aliyun.oss.OSS;
    import com.aliyun.oss.common.utils.BinaryUtil;
    import com.aliyun.oss.model.MatchMode;
    import com.aliyun.oss.model.PolicyConditions;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.text.SimpleDateFormat;
    import java.util.Date;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    @RestController
    public class OssController {
         
    
        @Autowired
        OSS ossClient;
        @Value ("${spring.cloud.alicloud.oss.endpoint}")
        String endpoint ;
    
        @Value("${spring.cloud.alicloud.oss.bucket}")
        String bucket ;
    
        @Value("${spring.cloud.alicloud.access-key}")
        String accessId ;
        @Value("${spring.cloud.alicloud.secret-key}")
        String accessKey ;
        @RequestMapping("/oss/policy")
        public Map<String, String> policy(){
         
    
            String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
    
            String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
            String dir = format; // 用户上传文件时指定的前缀。
    
            Map<String, String> respMap=null;
            try {
         
                long expireTime = 30;
                long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
                Date expiration = new Date(expireEndTime);
                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));
    
            } catch (Exception e) {
         
                // Assert.fail(e.getMessage());
                System.out.println(e.getMessage());
            } finally {
         
                ossClient.shutdown();
            }
            return respMap;
        }
    }
    

    测试: http://localhost:30000/oss/policy

    {"accessid":"LTAI4G4W1RA4JXz2QhoDwHhi","policy":"eyJleHBpcmF0aW9uIjoiMjAyMC0wNC0yOVQwMjo1ODowNy41NzhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIwLTA0LTI5LyJdXX0=","signature":"s42iRxtxGFmHyG40StM3d9vOfFk=","dir":"2020-04-29/","host":"https://gulimall-images.oss-cn-shanghai.aliyuncs.com","expire":"1588129087"}
    

    以后在上传文件时的访问路径为“ http://localhost:88/api/thirdparty/oss/policy”,

    在“gulimall-gateway”中配置路由规则:

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

    测试是否能够正常跳转: http://localhost:88/api/thirdparty/oss/policy(可以获取到签名数据)

    image-20200429111408164

    API-品牌管理-前后联调测试上传

    放置项目提供的upload文件夹到components目录下,一个是单文件上传,另外一个是多文件上传

    PS D:\Project\gulimall\renren-fast-vue\src\components\upload> ls
    
    
        目录: D:\Project\gulimall\renren-fast-vue\src\components\upload
    
    
    Mode                LastWriteTime         Length Name
    ----                -------------         ------ ----
    -a----  2020/4/29 星期三     12:0           3122 multiUpload.vue
                                    2
    -a----  2019/11/11 星期一     21:            343 policy.js
                                   20
    -a----  2020/4/29 星期三     12:0           3053 singleUpload.vue
                                    1
    
    
    PS D:\Project\gulimall\renren-fast-vue\src\components\upload>
    

    修改这两个文件的配置后

    开始执行上传,但是在上传过程中,出现了如下的问题:

    image-20200429124629150

    Access to XMLHttpRequest at 'http://gulimall-images.oss-cn-shanghai.aliyuncs.com/' 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.
    

    这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:

    image-20200429124940091

    再次执行文件上传。

    API-品牌管理-表单校验&自定义校验器

    在新增品牌信息的时候,为了用户输入信息的合法性,我们需要对用户数据进行规则校验来确保数据的合法性:

    分布式基础篇_第11张图片

    //给每一个表单项都添加上自己的校验规则,在鼠标blur时触发 
    dataRule: {
         
            name: [{
          required: true, message: "品牌名不能为空", trigger: "blur" }],
            logo: [
              {
          required: true, message: "品牌logo地址不能为空", trigger: "blur" }
            ],
            descript: [
              {
          required: true, message: "介绍不能为空", trigger: "blur" }
            ],
            showStatus: [
              {
         
                required: true,
                message: "显示状态[0-不显示;1-显示]不能为空",
                trigger: "blur"
              }
            ],
                //自定义校验器
            firstLetter: [
              {
         
                validator: (rule, value, callback) => {
         
                  if (value == "") {
         
                    callback(new Error("首字母必须填写"));
                  } else if (!/^[a-zA-Z]$/.test(value)) {
         
                    callback(new Error("首字母必须a-z或者A-Z之间"));
                  } else {
         
                    callback();
                  }
                },
                trigger: "blur"
              }
            ],
            //自定义校验器
            sort: [
              {
         
                validator: (rule, value, callback) => {
         
                  if (value == "") {
         
                    callback(new Error("排序字段必须填写"));
                  } else if (!Number.isInteger(value) || value<0) {
         
                    callback(new Error("排序必须是一个大于等于0的整数"));
                  } else {
         
                    callback();
                  }
                },
                trigger: "blur"
              }
            ]
          }
        };
      },
    

    API-品牌管理-JSR303数据校验

    除了前端校验外,后台数据处理时,同样也需要校验,为什么呢?如果后台不加以校验,不法分子通过发送请求的工具给我们的后台发送请求,这些发送请求的工具可是没有校验规则的,假如发送一些不合法的数据给后台,那就没办法了,所以仅仅只有前端校验是不行的,传入后台的数据我们也需要校验数据合法性,这里推荐使用JSR303数据校验框架:

    1)使用java提供的可标注在实体类字段上达到校验注解:

    在Java中提供了一系列的校验方式,它这些校验方式在“javax.validation.constraints”包中,提供了如@Email,@NotNull等注解。

    在非空处理方式上提供了@NotNull,@Blank和@

    (1)@NotNull

    The annotated element must not be null. Accepts any type.
    注解元素禁止为null,能够接收任何类型

    (2)@NotEmpty

    the annotated element must not be null nor empty.

    该注解修饰的字段不能为null或""

    Supported types are:

    支持以下几种类型

    CharSequence (length of character sequence is evaluated)

    字符序列(字符序列长度的计算)

    Collection (collection size is evaluated)
    集合长度的计算

    Map (map size is evaluated)
    map长度的计算

    Array (array length is evaluated)
    数组长度的计算

    (3)@NotBlank

    The annotated element must not be null and must contain at least one non-whitespace character. Accepts CharSequence.
    该注解不能为null,并且至少包含一个非空白字符。接收字符序列。

    2)在请求方法种,使用校验注解@Valid,开启校验

        @RequestMapping("/save")
        public R save(@Valid @RequestBody BrandEntity brand){
         
    		brandService.save(brand);
    
            return R.ok();
        }
    

    测试: http://localhost:88/api/product/brand/save

    在postman种发送上面的请求

    {
         
        "timestamp": "2020-04-29T09:20:46.383+0000",
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
         
                "codes": [
                    "NotBlank.brandEntity.name",
                    "NotBlank.name",
                    "NotBlank.java.lang.String",
                    "NotBlank"
                ],
                "arguments": [
                    {
         
                        "codes": [
                            "brandEntity.name",
                            "name"
                        ],
                        "arguments": null,
                        "defaultMessage": "name",
                        "code": "name"
                    }
                ],
                "defaultMessage": "不能为空",
                "objectName": "brandEntity",
                "field": "name",
                "rejectedValue": "",
                "bindingFailure": false,
                "code": "NotBlank"
            }
        ],
        "message": "Validation failed for object='brandEntity'. Error count: 1",
        "path": "/product/brand/save"
    }
    

    能够看到"defaultMessage": “不能为空”,这些错误消息定义在“hibernate-validator”的“\org\hibernate\validator\ValidationMessages_zh_CN.properties”文件中。在该文件中定义了很多的错误规则:

    javax.validation.constraints.AssertFalse.message     = 只能为false
    javax.validation.constraints.AssertTrue.message      = 只能为true
    javax.validation.constraints.DecimalMax.message      = 必须小于或等于{value}
    javax.validation.constraints.DecimalMin.message      = 必须大于或等于{value}
    javax.validation.constraints.Digits.message          = 数字的值超出了允许范围(只允许在{integer}位整数和{fraction}位小数范围内)
    javax.validation.constraints.Email.message           = 不是一个合法的电子邮件地址
    javax.validation.constraints.Future.message          = 需要是一个将来的时间
    javax.validation.constraints.FutureOrPresent.message = 需要是一个将来或现在的时间
    javax.validation.constraints.Max.message             = 最大不能超过{value}
    javax.validation.constraints.Min.message             = 最小不能小于{value}
    javax.validation.constraints.Negative.message        = 必须是负数
    javax.validation.constraints.NegativeOrZero.message  = 必须是负数或零
    javax.validation.constraints.NotBlank.message        = 不能为空
    javax.validation.constraints.NotEmpty.message        = 不能为空
    javax.validation.constraints.NotNull.message         = 不能为null
    javax.validation.constraints.Null.message            = 必须为null
    javax.validation.constraints.Past.message            = 需要是一个过去的时间
    javax.validation.constraints.PastOrPresent.message   = 需要是一个过去或现在的时间
    javax.validation.constraints.Pattern.message         = 需要匹配正则表达式"{regexp}"
    javax.validation.constraints.Positive.message        = 必须是正数
    javax.validation.constraints.PositiveOrZero.message  = 必须是正数或零
    javax.validation.constraints.Size.message            = 个数必须在{min}和{max}之间
    
    org.hibernate.validator.constraints.CreditCardNumber.message        = 不合法的信用卡号码
    org.hibernate.validator.constraints.Currency.message                = 不合法的货币 (必须是{value}其中之一)
    org.hibernate.validator.constraints.EAN.message                     = 不合法的{type}条形码
    org.hibernate.validator.constraints.Email.message                   = 不是一个合法的电子邮件地址
    org.hibernate.validator.constraints.Length.message                  = 长度需要在{min}和{max}之间
    org.hibernate.validator.constraints.CodePointLength.message         = 长度需要在{min}和{max}之间
    org.hibernate.validator.constraints.LuhnCheck.message               = ${validatedValue}的校验码不合法, Luhn模10校验和不匹配
    org.hibernate.validator.constraints.Mod10Check.message              = ${validatedValue}的校验码不合法, 模10校验和不匹配
    org.hibernate.validator.constraints.Mod11Check.message              = ${validatedValue}的校验码不合法, 模11校验和不匹配
    org.hibernate.validator.constraints.ModCheck.message                = ${validatedValue}的校验码不合法, ${modType}校验和不匹配
    org.hibernate.validator.constraints.NotBlank.message                = 不能为空
    org.hibernate.validator.constraints.NotEmpty.message                = 不能为空
    org.hibernate.validator.constraints.ParametersScriptAssert.message  = 执行脚本表达式"{script}"没有返回期望结果
    org.hibernate.validator.constraints.Range.message                   = 需要在{min}和{max}之间
    org.hibernate.validator.constraints.SafeHtml.message                = 可能有不安全的HTML内容
    org.hibernate.validator.constraints.ScriptAssert.message            = 执行脚本表达式"{script}"没有返回期望结果
    org.hibernate.validator.constraints.URL.message                     = 需要是一个合法的URL
    
    org.hibernate.validator.constraints.time.DurationMax.message        = 必须小于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
    org.hibernate.validator.constraints.time.DurationMin.message        = 必须大于${inclusive == true ? '或等于' : ''}${days == 0 ? '' : days += '天'}${hours == 0 ? '' : hours += '小时'}${minutes == 0 ? '' : minutes += '分钟'}${seconds == 0 ? '' : seconds += '秒'}${millis == 0 ? '' : millis += '毫秒'}${nanos == 0 ? '' : nanos += '纳秒'}
    

    想要自定义错误消息,可以覆盖默认的错误提示信息,如@NotBlank的默认message是

    public @interface NotBlank {
         
    
    	String message() default "{javax.validation.constraints.NotBlank.message}";
    

    可以在添加注解的时候,修改message:

    	@NotBlank(message = "品牌名必须非空")
    	private String name;
    

    当再次发送请求时,得到的错误提示信息:

    {
         
        "timestamp": "2020-04-29T09:36:04.125+0000",
        "status": 400,
        "error": "Bad Request",
        "errors": [
            {
         
                "codes": [
                    "NotBlank.brandEntity.name",
                    "NotBlank.name",
                    "NotBlank.java.lang.String",
                    "NotBlank"
                ],
                "arguments": [
                    {
         
                        "codes": [
                            "brandEntity.name",
                            "name"
                        ],
                        "arguments": null,
                        "defaultMessage": "name",
                        "code": "name"
                    }
                ],
                "defaultMessage": "品牌名必须非空",
                "objectName": "brandEntity",
                "field": "name",
                "rejectedValue": "",
                "bindingFailure": false,
                "code": "NotBlank"
            }
        ],
        "message": "Validation failed for object='brandEntity'. Error count: 1",
        "path": "/product/brand/save"
    }
    

    但是这种返回的错误结果并不符合我们的业务需要。

    3)给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。

     @RequestMapping("/save")
        public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
         
            if( result.hasErrors()){
         
                Map<String,String> map=new HashMap<>();
                //1.获取错误的校验结果
                result.getFieldErrors().forEach((item)->{
         
                    //获取发生错误时的message
                    String message = item.getDefaultMessage();
                    //获取发生错误的字段
                    String field = item.getField();
                    map.put(field,message);
                });
                return R.error(400,"提交的数据不合法").put("data",map);
            }else {
         
    
            }
    		brandService.save(brand);
    
            return R.ok();
        }
    

    这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。

    API-品牌管理-统一异常处理

    可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。

    (1)抽取一个异常处理类

    package com.bigdata.gulimall.product.exception;
    
    import com.bigdata.common.utils.R;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 集中处理所有异常
     */
    @Slf4j
    @RestControllerAdvice(basePackages = "com.bigdata.gulimall.product.controller")
    public class GulimallExceptionAdvice {
         
    
    
        @ExceptionHandler(value = Exception.class)
        public R handleValidException(MethodArgumentNotValidException exception){
         
            Map<String,String> map=new HashMap<>();
            BindingResult bindingResult = exception.getBindingResult();
            bindingResult.getFieldErrors().forEach(fieldError -> {
         
                String message = fieldError.getDefaultMessage();
                String field = fieldError.getField();
                map.put(field,message);
            });
    
            log.error("数据校验出现问题{},异常类型{}",exception.getMessage(),exception.getClass());
            return R.error(400,"数据校验出现问题").put("data",map);
        }
    
    }
    

    (2)测试: http://localhost:88/api/product/brand/save

    image-20200429183334783

    (3)默认异常处理

       @ExceptionHandler(value = Throwable.class)
        public R handleException(Throwable throwable){
         
            log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());
            return R.error(BizCodeEnum.UNKNOW_EXEPTION.getCode(),BizCodeEnum.UNKNOW_EXEPTION.getMsg());
        }
    

    (4)错误状态码

    上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

    image-20200429183748249

    为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

    package com.bigdata.common.exception;
    
    /***
     * 错误码和错误信息定义类
     * 1. 错误码定义规则为5为数字
     * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
     * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
     * 错误码列表:
     *  10: 通用
     *      001:参数格式校验
     *  11: 商品
     *  12: 订单
     *  13: 购物车
     *  14: 物流
     */
    public enum BizCodeEnum {
         
    
        UNKNOW_EXEPTION(10000,"系统未知异常"),
    
        VALID_EXCEPTION( 10001,"参数格式校验失败");
    
        private int code;
        private String msg;
    
        BizCodeEnum(int code, String msg) {
         
            this.code = code;
            this.msg = msg;
        }
    
        public int getCode() {
         
            return code;
        }
    
        public String getMsg() {
         
            return msg;
        }
    }
    

    (5)测试: http://localhost:88/api/product/brand/save

    分布式基础篇_第12张图片

    API-品牌管理-JSR303分组校验

    1、给校验注解,标注上groups,指定什么情况下才需要进行校验

    如:指定在更新和添加的时候,都需要进行校验

    	@NotEmpty
    	@NotBlank(message = "品牌名必须非空",groups = {
         UpdateGroup.class,AddGroup.class})
    	private String name;
    

    在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

    2、业务方法参数上使用@Validated注解

    @Validated的value方法:

    Specify one or more validation groups to apply to the validation step kicked off by this annotation.
    指定一个或多个验证组以应用于此注释启动的验证步骤。

    JSR-303 defines validation groups as custom annotations which an application declares for the sole purpose of using
    them as type-safe group arguments, as implemented in SpringValidatorAdapter.

    JSR-303 将验证组定义为自定义注释,应用程序声明的唯一目的是将它们用作类型安全组参数,如 SpringValidatorAdapter 中实现的那样。

    Other SmartValidator implementations may support class arguments in other ways as well.

    其他SmartValidator 实现也可以以其他方式支持类参数。

    默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

    API-品牌管理-JSR303自定义校验注解

    1、编写一个自定义的校验注解

    @Documented
    @Constraint(validatedBy = {
          ListValueConstraintValidator.class})
    @Target({
          METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
    @Retention(RUNTIME)
    public @interface ListValue {
         
        String message() default "{com.bigdata.common.valid.ListValue.message}";
    
        Class<?>[] groups() default {
          };
    
        Class<? extends Payload>[] payload() default {
          };
    
        int[] value() default {
         };
    }
    

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

    public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
         
        private Set<Integer> set=new HashSet<>();
        @Override
        public void initialize(ListValue constraintAnnotation) {
         
            int[] value = constraintAnnotation.value();
            for (int i : value) {
         
                set.add(i);
            }
    
        }
    
        @Override
        public boolean isValid(Integer value, ConstraintValidatorContext context) {
         
    
    
            return  set.contains(value);
        }
    }
    

    3、关联自定义的校验器和自定义的校验注解

    @Constraint(validatedBy = {
          ListValueConstraintValidator.class})
    

    4、使用实例

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

    概念-SPU&SKU&规格参数&销售属性

    http://www.gulixueyuan.com/course/369/task/13352/show#

    重新执行“sys_menus.sql”

    API-属性分组-前端组件抽取&父子组件交互

    现在想要实现点击菜单的左边,能够实现在右边展示数据

    image-20200430215649355

    父子组件传递数据:

    1)子组件给父组件传递数据,事件机制;

    在category中绑定node-click事件,

      
    

    2)子组件给父组件发送一个事件,携带上数据;

        nodeClick(data,Node,component){
         
           console.log("子组件",data,Node,component);
           this.$emit("tree-node-click",data,Node,component);
        }, 
    

    this.$emit(事件名,“携带的数据”);

    3)父组件中的获取发送的事件

    
        //获取发送的事件数据
        treeNodeClick(data,Node,component){
         console.log("attgroup感知到的category的节点被点击",data,Node,component);
         console.log("刚才被点击的菜单ID",data.catId);
        },
    

    API-属性分组-获取分类属性分组

    当点击了一个分类的时候,右边会列出与他相关的所有属性分组

    分析:分类子组件触发点击效果后会将当前被点击的分类id传给父分类,父分类感应到子组件被点击并且接收到了分类id,就可以发送请求要到该分类下的所有属性分组在父组件中展示。

    前面,父子组件交互已经没啥问题,当子组件被点击父组件也能感应的到并且能收到子组件传入的参数,这个时候,父组件就可以拿着这个分类id去请求这个接口获取到属性分组数据:

       /**
         * 列表
         */
        @RequestMapping("/list/{catelogId}")
        //@RequiresPermissions("product:attrgroup:list")
        public R list(@RequestParam Map<String, Object> params,
                      @PathVariable("catelogId") Long catelogId){
         
    //        PageUtils page = attrGroupService.queryPage(params);
    
            PageUtils page = attrGroupService.queryPage(params,catelogId);
    
            return R.ok().put("page", page);
        }
    

    service细节(前端除了传入分类id外,还有可能传入搜索关键字,如果没传分类id默认查所有):

     @Override
        public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
         
            String key = (String) params.get("key");
            //select * from pms_attr_group where catelog_id=? and (attr_group_id=key or attr_group_name like %key%)
            QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<AttrGroupEntity>();
            if(!StringUtils.isEmpty(key)){
         
                wrapper.and((obj)->{
         
                    obj.eq("attr_group_id",key).or().like("attr_group_name",key);
                });
            }
    
            if(catelogId == 0){
         
                IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
                        wrapper);
                return new PageUtils(page);
            }else {
         
                wrapper.eq("catelog_id",catelogId);
                IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),
                        wrapper);
                return new PageUtils(page);
            }
    
        }
    

    就实现了点击左边分类右边显示该分类所有属性分组,或者根据关键字搜索属性分组的功能。

    也没啥问题。

    API-属性分组-分组新增&级联选择器

    在新增属性分组的时候,会涉及到该分组的所属分类id是多少,我们希望该字段应当获取到所有分类数据放在一个级联选择器粒由用户自行选择:如下所示:

    分布式基础篇_第13张图片

    实现细节:

       
    

    options这个属性绑定的是所有分类数据,props这个属性指定了一些数据显示规则,规则如下:

    //指定分类数据中的catId为级联排序字段,name为显示字段, children为子集。
    setting: {
         
            value: "catId",
            label: "name",
            children: "children"
          },
    

    最终效果:

    分布式基础篇_第14张图片

    因为后台提供的分类子集可能为空,级联选择器认为他可能也是一个值,就把空也展示上去了,我们可以在后台设置这个字段,如果为空就不要组装起来了。(在bean属性上添加注解)

    	@JsonInclude(JsonInclude.Include.NON_EMPTY)
    	@TableField(exist=false)
    	private List<CategoryEntity> children;
    

    整个级联显示就没啥问题了。

    重新梳理一边思路,首先用户点击新增属性分组,会调用该方法(因为是新增不会传入分组id):

     // 新增 / 修改
        addOrUpdateHandle(id) {
         
          this.addOrUpdateVisible = true;
          this.$nextTick(() => {
         
            this.$refs.addOrUpdate.init(id);
          });
        },
    

    这个方法会打开输入框,然后调用引用addOrUpdate组件的init方法:

        init(id) {
         
          this.dataForm.attrGroupId = id || 0;
          this.visible = true;
          this.$nextTick(() => {
         
            this.$refs["dataForm"].resetFields();
            if (this.dataForm.attrGroupId) {
         
              this.$http({
         
                url: this.$http.adornUrl(
                  `/product/attrgroup/info/${
           this.dataForm.attrGroupId}`
                ),
                method: "get",
                params: this.$http.adornParams()
              }).then(({
          data }) => {
         
                if (data && data.code === 0) {
         
                  this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
                  this.dataForm.sort = data.attrGroup.sort;
                  this.dataForm.descript = data.attrGroup.descript;
                  this.dataForm.icon = data.attrGroup.icon;
                  this.dataForm.catelogId = data.attrGroup.catelogId;
                  //查出catelogId的完整路径
                  this.catelogPath =  data.attrGroup.catelogPath;
                }
              });
            }
    

    因为本次操作是新增,该Init方法只会打开对话框。

    打开对话框,用户输入自己的属性分组数据后,如果点击提交,会触发这个方法:

     // 表单提交
            dataFormSubmit() {
         
                alert(this.catelogPath[this.catelogPath.length - 1]);
                this.$refs["dataForm"].validate((valid) => {
         
                    if (valid) {
         
                        this.$http({
         
                            url: this.$http.adornUrl(
                                `/product/attrgroup/${
           
                    !this.dataForm.attrGroupId ? "save" : "update"
                  }`
                            ),
                            method: "post",
                            data: this.$http.adornData({
         
                                attrGroupId: this.dataForm.attrGroupId || undefined,
                                attrGroupName: this.dataForm.attrGroupName,
                                sort: this.dataForm.sort,
                                descript: this.dataForm.descript,
                                icon: this.dataForm.icon,
                                catelogId: this.catelogPath[this.catelogPath.length - 1],
                            }),
                        }).then(({
         
                            data
                        }) => {
         
                            if (data && data.code === 0) {
         
                                this.$message({
         
                                    message: "操作成功",
                                    type: "success",
                                    duration: 1500,
                                    onClose: () => {
         
                                        this.visible = false;
                                        this.$emit("refreshDataList");
                                    },
                                });
                            } else {
         
                                this.$message.error(data.msg);
                            }
                        });
                    }
                });
    

    该方法会获取到用户所有输入的值,至于级联选择器的值默认是一个数组,我们只要把该数组分割,只要最后一个值就可以了。

    有人问,级联选择器的值是怎么获取的?

     <category-cascader :catelogPath.sync="catelogPath">category-cascader>
    

    当级联选择器的路径发生了变化,就会发送一个消息给父组件,带上最新变化的值:

       paths(v){
         
          this.$emit("update:catelogPath",v);
          //还可以使用pubsub-js进行传值
          this.PubSub.publish("catPath",v);
        }
    

    父组件接收到最新变化的值,之后赋值给了catelogPath这个属性,该值是一个数组,提交的时候,只提交我们需要的最后一个分类的id就行了。

    API-属性分组-分组修改&级联选择器回显

    点击修改会触发这个回调方法:

      <el-button
                    type="text"
                    size="small"
                    @click="addOrUpdateHandle(scope.row.attrGroupId)"
                  >修改el-button>
    

    来到这个方法

     // 新增 / 修改
        addOrUpdateHandle(id) {
         
          this.addOrUpdateVisible = true;
          this.$nextTick(() => {
         
            this.$refs.addOrUpdate.init(id);
          });
        },
    

    这次由于是修改,带上了分组id,还是打开对话框,调用初始化init方法。

       init(id) {
         
          this.dataForm.attrGroupId = id || 0;
          this.visible = true;
          this.$nextTick(() => {
         
            this.$refs["dataForm"].resetFields();
            if (this.dataForm.attrGroupId) {
         
              this.$http({
         
                url: this.$http.adornUrl(
                  `/product/attrgroup/info/${
           this.dataForm.attrGroupId}`
                ),
                method: "get",
                params: this.$http.adornParams()
              }).then(({
          data }) => {
         
                if (data && data.code === 0) {
         
                  this.dataForm.attrGroupName = data.attrGroup.attrGroupName;
                  this.dataForm.sort = data.attrGroup.sort;
                  this.dataForm.descript = data.attrGroup.descript;
                  this.dataForm.icon = data.attrGroup.icon;
                  this.dataForm.catelogId = data.attrGroup.catelogId;
                  //查出catelogId的完整路径
                  this.catelogPath =  data.attrGroup.catelogPath;
                }
              });
            }
    

    由于是修改传递了分组id,修改需要回显字段值到文本框,所以就拿着分组id发送请求获取到该分组最新的数据来赋值到文本框进行回显来供用户修改,catelogPath完整路径是根据属性分组所属分类id递归查询所得到的,如下所示:

       //[2,25,225]
        @Override
        public Long[] findCatelogPath(Long catelogId) {
         
            List<Long> paths = new ArrayList<>();
            List<Long> parentPath = findParentPath(catelogId, paths);
    
            Collections.reverse(parentPath);
    
    
            return parentPath.toArray(new Long[parentPath.size()]);
        }
    
      //225,25,2
        private List<Long> findParentPath(Long catelogId,List<Long> paths){
         
            //1、收集当前节点id
            paths.add(catelogId);
            CategoryEntity byId = this.getById(catelogId);
            if(byId.getParentCid()!=0){
         
                findParentPath(byId.getParentCid(),paths);
            }
            return paths;
    
        }
    

    修改回显也没啥问题。

    用户修改后点击提交还是会调用这个方法,因为有分组Id,所以请求的是分组修改的接口。

     // 表单提交
            dataFormSubmit() {
         
                alert(this.catelogPath[this.catelogPath.length - 1]);
                this.$refs["dataForm"].validate((valid) => {
         
                    if (valid) {
         
                        this.$http({
         
                            url: this.$http.adornUrl(
                                `/product/attrgroup/${
           
                    !this.dataForm.attrGroupId ? "save" : "update"
                  }`
                            ),
                            method: "post",
                            data: this.$http.adornData({
         
                                attrGroupId: this.dataForm.attrGroupId || undefined,
                                attrGroupName: this.dataForm.attrGroupName,
                                sort: this.dataForm.sort,
                                descript: this.dataForm.descript,
                                icon: this.dataForm.icon,
                                catelogId: this.catelogPath[this.catelogPath.length - 1],
                            }),
                        }).then(({
         
                            data
                        }) => {
         
                            if (data && data.code === 0) {
         
                                this.$message({
         
                                    message: "操作成功",
                                    type: "success",
                                    duration: 1500,
                                    onClose: () => {
         
                                        this.visible = false;
                                        this.$emit("refreshDataList");
                                    },
                                });
                            } else {
         
                                this.$message.error(data.msg);
                            }
                        });
                    }
                });
    

    也没啥问题。

    API-品牌管理-品牌分类关联与级联更新

    品牌列表分页显示有问题,那是由于没有配MybatisPlus的分页插件。

    MybatisPlus设置分页插件配置:

    
    @Configuration
    @EnableTransactionManagement //开启事务
    @MapperScan("com.atguigu.gulimall.product.dao")
    public class MyBatisConfig {
         
    
        //引入分页插件
        @Bean
        public PaginationInterceptor paginationInterceptor() {
         
            PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
            // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
             paginationInterceptor.setOverflow(true);
            // 设置最大单页限制数量,默认 500 条,-1 不受限制
            paginationInterceptor.setLimit(1000);
            return paginationInterceptor;
        }
    }
    
    

    另外,默认生成的逆向工程代码没有带关键字查询,所以我们要手动修改一下默认生成的接口代码给他带上按照关键字查询:

      @Override
        public PageUtils queryPage(Map<String, Object> params) {
         
            //1、获取key
            String key = (String) params.get("key");
            QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
            if(!StringUtils.isEmpty(key)){
         
                queryWrapper.eq("brand_id",key).or().like("name",key);
            }
    
            IPage<BrandEntity> page = this.page(
                    new Query<BrandEntity>().getPage(params),
                    queryWrapper
    
            );
    
            return new PageUtils(page);
        }
    
    

    关联分类(一个分类下可能有多个品牌,比如手机分类下有小米,华为,苹果等,一个品牌又可能属于多个分类,比如小米,它既关联手机分类,又关联电视分类,等等,我们可以给品牌关联上属于自己的分类)

    首先点击关联分类,会带上品牌的id去数据库中间表中找到与该品牌关联的所有已关联分类信息来进行一个展示,请求接口是:

    分布式基础篇_第15张图片

    还没有任何分类与该品牌关联,我们可以新增一个分类关联关系。

    分布式基础篇_第16张图片

    点击新增关联,会列出所有的分类数据来供用户选择到底要和哪个分类进行关联:

    分布式基础篇_第17张图片

    当用户点击确定,又会发送一个请求,该请求会带上关联的分类信息和品牌Id,去数据库关联表中新增一条记录,请求接口为:

    分布式基础篇_第18张图片

    刚新增的关联记录就会有显示:

    分布式基础篇_第19张图片

    我们也可以移除该关联关系,都没啥问题。

    包括我们在修改品牌信息或者分类信息的时候,不应该只单纯的修改品牌信息和分类信息,还需要保证冗余字段的一致性,所以还需要把关联表的数据一并修改,这里这是说一下,具体的操作步骤也非常简单,就不列出来了。

    API-平台属性-规格参数新增与VO

    规格参数新增时,请求的URL:Request URL:

    http://localhost:88/api/product/attr/base/list/0?t=1588731762158&page=1&limit=10&key=

    当有新增字段时,我们往往会在entity实体类中新建一个字段,并标注数据库中不存在该字段,然而这种方式并不规范

    1588732021702

    比较规范的做法是,新建一个vo文件夹,将每种不同的对象,按照它的功能进行了划分。在java中,涉及到了这几种类型

    1588732152646

    Request URL: http://localhost:88/api/product/attr/save,现在的情况是,它在保存的时候,只是保存了attr,并没有保存attrgroup,为了解决这个问题,我们新建了一个vo/AttrVo,在原AttrEntity基础上增加了attrGroupId字段,使得保存新增数据的时候,也保存了它们之间的关系。

    通过" BeanUtils.copyProperties(attr,attrEntity);"能够实现在两个Bean之间拷贝数据,但是两个Bean的字段要相同

       @Override
        public void saveAttr(AttrVo attr) {
         
                AttrEntity attrEntity = new AttrEntity();
    //        attrEntity.setAttrName(attr.getAttrName());
            BeanUtils.copyProperties(attr,attrEntity);
            //1、保存基本数据
            this.save(attrEntity);
            //2、保存关联关系
            if(attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId()!=null){
         
                AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
                relationEntity.setAttrGroupId(attr.getAttrGroupId());
                relationEntity.setAttrId(attrEntity.getAttrId());
                relationDao.insert(relationEntity);
            }
        }
    

    问题:现在有两个查询,一个是查询部分,另外一个是查询全部,但是又必须这样来做吗?还是有必要的,但是可以在后台进行设计,两种查询是根据catId是否为零进行区分的。

    API-平台属性-规格参数列表

    接着我们编写规格参数相关的功能,首先刷新页面,会发送这么一个请求(点击左边分类菜单会传入分类id查询参数,点击关键字会传入key,默认什么都不带就查所有,查询全部也是如此)

    分布式基础篇_第20张图片

    来到接口实现(type为base则查询规格,否则查销售):

     @Override
        public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
         
            QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
    
            if(catelogId != 0){
         
                queryWrapper.eq("catelog_id",catelogId);
            }
    
            String key = (String) params.get("key");
            if(!StringUtils.isEmpty(key)){
         
                //attr_id  attr_name
                queryWrapper.and((wrapper)->{
         
                    wrapper.eq("attr_id",key).or().like("attr_name",key);
                });
            }
    
            IPage<AttrEntity> page = this.page(
                    new Query<AttrEntity>().getPage(params),
                    queryWrapper
            );
    
            PageUtils pageUtils = new PageUtils(page);
            List<AttrEntity> records = page.getRecords();
            List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
         
                AttrRespVo attrRespVo = new AttrRespVo();
                BeanUtils.copyProperties(attrEntity, attrRespVo);
    
                //1、设置分类和分组的名字
                if("base".equalsIgnoreCase(type)){
         
                    AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
                    if (attrId != null && attrId.getAttrGroupId()!=null) {
         
                        AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
                        attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
                    }
    
                }
    
    
                CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
                if (categoryEntity != null) {
         
                    attrRespVo.setCatelogName(categoryEntity.getName());
                }
                return attrRespVo;
            }).collect(Collectors.toList());
    
            pageUtils.setList(respVos);
            return pageUtils;
        }
    

    API-平台属性-规格修改

    在规格参数上点击修改,需要回显该规格参数的信息来供用户修改,如下所示:
    分布式基础篇_第21张图片

    其他字段的回显值还好些,关键是所属分类和所属分组我们必须查询出对应的完整路径才行。点击修改,会发送如下请求,请求如下接口:

     http://localhost:88/api/product/attr/info/1?t=1603170822719
    

    来到接口实现:(根据前端传递的属性id来找到该属性的一系列信息返回给前端进行回显,包括了该属性对应的分组和分类的完整路径信息)

    @Override
    public AttrRespVo getAttrInfo(Long attrId) {
         
        AttrRespVo respVo = new AttrRespVo();
        AttrEntity attrEntity = this.getById(attrId);
        BeanUtils.copyProperties(attrEntity,respVo);
    
    
    
        if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
         
            //1、设置分组信息
            AttrAttrgroupRelationEntity attrgroupRelation = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrId));
            if(attrgroupRelation!=null){
         
                respVo.setAttrGroupId(attrgroupRelation.getAttrGroupId());
                AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupRelation.getAttrGroupId());
                if(attrGroupEntity!=null){
         
                    respVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }
        }
    
    
        //2、设置分类信息
        Long catelogId = attrEntity.getCatelogId();
        Long[] catelogPath = categoryService.findCatelogPath(catelogId);
        respVo.setCatelogPath(catelogPath);
    
        CategoryEntity categoryEntity = categoryDao.selectById(catelogId);
        if(categoryEntity!=null){
         
            respVo.setCatelogName(categoryEntity.getName());
        }
    
    
        return respVo;
    }
    

    当用户修改后,点击确定,来到该接口(如果是基本,除了修改基本信息外,还需要修改关联信息,如果没有关联信息,就新增):

    @Transactional
        @Override
        public void updateAttr(AttrVo attr) {
         
            AttrEntity attrEntity = new AttrEntity();
            BeanUtils.copyProperties(attr,attrEntity);
            //如果是销售属性,直接修改即可
            this.updateById(attrEntity);
    
            //如果是基本属性,为了数据的一致性还需要修改与他关联的分组属性关联表。
            if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
         
                //1、修改分组关联,如果关联表有该属性的关联,就修改,如果没有就新增
                AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
    
                relationEntity.setAttrGroupId(attr.getAttrGroupId());
                relationEntity.setAttrId(attr.getAttrId());
    
                Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
                if(count>0){
         
    
                    relationDao.update(relationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId()));
    
                }else{
         
                    relationDao.insert(relationEntity);
                }
            }
    
    
        }
    

    API-平台属性-销售属性维护

    前面编写完了基本属性的增删改查,这次编写销售属性的。(在数据库中,销售属性和基本属性都是在一个表里面,用一个字段来表示哪个是销售哪个是基本,0是销售属性,1是基本属性)

    点击销售属性目录,会发送这么一个请求,要到销售属性列表:

    http://localhost:88/api/product/attr/sale/list/0?t=1603173667313&page=1&limit=10&key=
    

    既然销售属性和基本属性操作的都是同一个表,那么生成的增删改查代码也都是在一起的,为了简单起见,获取销售属性的代码就用基本属性的,只不过这个代码要判断一下,通过路径变量,获取销售属性时,是sale,获取基本时,是base,可以利用它做一个路径变量,通过不同的路径变量,来查询返回不同的属性数据,如下:

        @GetMapping("/{attrType}/list/{catelogId}")
        public R baseAttrList(@RequestParam Map<String, Object> params,
                              @PathVariable("catelogId") Long catelogId,
                              @PathVariable("attrType")String type){
         
    
            PageUtils page = attrService.queryBaseAttrPage(params,catelogId,type);
            return R.ok().put("page", page);
        }
    

    service细节(这次三元走的是销售,所以仅仅查销售属性):

       @Override
        public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String type) {
         
            QueryWrapper<AttrEntity> queryWrapper = new QueryWrapper<AttrEntity>().eq("attr_type","base".equalsIgnoreCase(type)?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode():ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());
    
            if(catelogId != 0){
         
                queryWrapper.eq("catelog_id",catelogId);
            }
    
            String key = (String) params.get("key");
            if(!StringUtils.isEmpty(key)){
         
                //attr_id  attr_name
                queryWrapper.and((wrapper)->{
         
                    wrapper.eq("attr_id",key).or().like("attr_name",key);
                });
            }
    
            IPage<AttrEntity> page = this.page(
                    new Query<AttrEntity>().getPage(params),
                    queryWrapper
            );
    
            PageUtils pageUtils = new PageUtils(page);
            List<AttrEntity> records = page.getRecords();
            List<AttrRespVo> respVos = records.stream().map((attrEntity) -> {
         
                AttrRespVo attrRespVo = new AttrRespVo();
                BeanUtils.copyProperties(attrEntity, attrRespVo);
    
                //1、设置分类和分组的名字(只有基本属性可以)
                if("base".equalsIgnoreCase(type)){
         
                    AttrAttrgroupRelationEntity attrId = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
                    if (attrId != null && attrId.getAttrGroupId()!=null) {
         
                        AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrId.getAttrGroupId());
                        attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
                    }
    
                }
    
    
                CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
                if (categoryEntity != null) {
         
                    attrRespVo.setCatelogName(categoryEntity.getName());
                }
                return attrRespVo;
            }).collect(Collectors.toList());
    
            pageUtils.setList(respVos);
            return pageUtils;
        }
    

    销售属性的查询也就没啥问题了。

    分布式基础篇_第22张图片

    其次就是新增,当前还没有任何销售属性,我们可以给他录几个进去(注意:由于销售属性和基本属性都用的是一个接口,必须是基本属性,才需要查分组信息,新增的时候,只有类型为基本属性的时候才新增上关联信息)。

      //新增接口
      @Transactional
        @Override
        public void saveAttr(AttrVo attr) {
         
            AttrEntity attrEntity = new AttrEntity();
    //        attrEntity.setAttrName(attr.getAttrName());
            BeanUtils.copyProperties(attr,attrEntity);
            //1、保存基本数据
            this.save(attrEntity);
            //2、保存关联关系(仅基本属性可以)
            if(attr.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attr.getAttrGroupId()!=null){
         
                AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
                relationEntity.setAttrGroupId(attr.getAttrGroupId());
                relationEntity.setAttrId(attrEntity.getAttrId());
                relationDao.insert(relationEntity);
            }
    
    
        }
    

    修改的时候,如果当前要修改的属性是销售属性,那还好,但是如果修改的是基本属性,并且有传入的关联分组信息,为了保证数据一致性,那么就涉及到了同步修改与他相关的关联表信息,如果这个基本属性修改的时候传入了所属分组id,暂时还没有关联表信息,那么就视为是一个新增操作。

    @Transactional
        @Override
        public void updateAttr(AttrVo attr) {
         
            AttrEntity attrEntity = new AttrEntity();
            BeanUtils.copyProperties(attr,attrEntity);
            //如果是销售属性,直接修改即可
            this.updateById(attrEntity);
    
            //如果是基本属性,为了数据的一致性还需要修改与他关联的分组属性关联表。
            if(attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()){
         
                //1、修改分组关联,如果关联表有该属性的关联,就修改,如果没有就新增
                AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
    
                relationEntity.setAttrGroupId(attr.getAttrGroupId());
                relationEntity.setAttrId(attr.getAttrId());
    
                Integer count = relationDao.selectCount(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attr.getAttrId()));
                if(count>0){
         
    
                    relationDao.update(relationEntity,new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id",attr.getAttrId()));
    
                }else{
         
                    relationDao.insert(relationEntity);
                }
            }
    
    
        }
    

    也没啥问题。

    API-平台属性-查询分组关联属性&删除关联

    在属性分组上点击关联,先需要查到该分组关联的所有base属性做一个展示,请求接口如下:

        /**
         * 根据分组id查找关联的所有基本属性
         * @param attrgroupId
         * @return
         */
        @Override
        public List<AttrEntity> getRelationAttr(Long attrgroupId) {
         
            List<AttrAttrgroupRelationEntity> entities = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_group_id", attrgroupId));
    
            List<Long> attrIds = entities.stream().map((attr) -> {
         
                return attr.getAttrId();
            }).collect(Collectors.toList());
    
            if(attrIds == null || attrIds.size() == 0){
         
                return null;
            }
            Collection<AttrEntity> attrEntities = this.listByIds(attrIds);
            return (List<AttrEntity>) attrEntities;
        }
    

    可以看到,和主体相关联的基本属性有入网型号

    分布式基础篇_第23张图片

    我们也可以移除他,都是逆向生成的方法,都没啥问题。

    API-平台属性-查询分组未关联的属性

    可以再给主体关联上几个基本属性,点击新建关联,会查询出该分组所有未被关联的基本属性(注意,必须和分组在同一分类下,且不能被其他分组引用关联),供用户选择关联。

    
        /**
         * 获取当前分组没有关联的所有属性
         * @param params
         * @param attrgroupId
         * @return
         */
        @Override
        public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
         
            //1、当前分组只能关联自己所属的分类里面的所有属性
            AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
            Long catelogId = attrGroupEntity.getCatelogId();
            //2、当前分组只能关联别的分组没有引用的属性
            //2.1)、当前分类下的其他分组
            List<AttrGroupEntity> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
            List<Long> collect = group.stream().map(item -> {
         
                return item.getAttrGroupId();
            }).collect(Collectors.toList());
    
            //2.2)、这些分组关联的属性
            List<AttrAttrgroupRelationEntity> groupId = relationDao.selectList(new QueryWrapper<AttrAttrgroupRelationEntity>().in("attr_group_id", collect));
            List<Long> attrIds = groupId.stream().map(item -> {
         
                return item.getAttrId();
            }).collect(Collectors.toList());
    
            //2.3)、从当前分类的所有属性中移除这些属性;
            QueryWrapper<AttrEntity> wrapper = new QueryWrapper<AttrEntity>().eq("catelog_id", catelogId).eq("attr_type",ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode());
            if(attrIds!=null && attrIds.size()>0){
         
                wrapper.notIn("attr_id", attrIds);
            }
            String key = (String) params.get("key");
            if(!StringUtils.isEmpty(key)){
         
                wrapper.and((w)->{
         
                    w.eq("attr_id",key).or().like("attr_name",key);
                });
            }
            IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params), wrapper);
    
            PageUtils pageUtils = new PageUtils(page);
    
            return pageUtils;
        }
    

    API-平台属性-新增分组与属性关联

    分布式基础篇_第24张图片

    点击新建关联,这些属性还没有被任何分组关联过,并且是和本分组在同一个分类下,我们就可以选择他们做一个关联。

    选择后,点击确认新增,会带上关联属性的id和本分组的id去关联表插入两条关联数据。

    请求接口:

        @Override
        public void saveBatch(List<AttrGroupRelationVo> vos) {
         
            List<AttrAttrgroupRelationEntity> collect = vos.stream().map(item -> {
         
                AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
                BeanUtils.copyProperties(item, relationEntity);
                return relationEntity;
            }).collect(Collectors.toList());
            this.saveBatch(collect);
        }
    
    

    API-新增商品-调试会员等级相关接口

    http://www.gulixueyuan.com/course/369/task/13365/show#

    API-新增商品-获取分类关联的品牌

    在新增商品的时候,当用户选择了商品所属分类后,需要发送请求获取该分类下的所有品牌,供用户选择。(传入分类id,鼠标离开事件触发)

    分布式基础篇_第25张图片

    http://localhost:88/api/product/categorybrandrelation/brands/list?t=1603245936856&catId=225
    

    查询该分类下所有的品牌信息:

       *  /product/categorybrandrelation/brands/list
         *
         *  1、Controller:处理请求,接受和校验数据
         *  2、Service接受controller传来的数据,进行业务处理
         *  3、Controller接受Service处理完的数据,封装页面指定的vo
         */
        @GetMapping("/brands/list")
        public R relationBrandsList(@RequestParam(value = "catId",required = true)Long catId){
         
            List<BrandEntity> vos = categoryBrandRelationService.getBrandsByCatId(catId);
    
            List<BrandVo> collect = vos.stream().map(item -> {
         
                BrandVo brandVo = new BrandVo();
                brandVo.setBrandId(item.getBrandId());
                brandVo.setBrandName(item.getName());
    
                return brandVo;
            }).collect(Collectors.toList());
    
            return R.ok().put("data",collect);
    
        }
    
    

    API-新增商品-获取分类下所有分组以及属性

    点击下一步,需要给这个商品录一些属性信息,需要先查出来当前分类下的所有属性分组和属性信息做一个展示供用户选择。

    http://localhost:88/api/product/attrgroup/225/withattr?t=1603247117104
    

    请求接口:

      ///product/attrgroup/{catelogId}/withattr
        @GetMapping("/{catelogId}/withattr")
        public R getAttrGroupWithAttrs(@PathVariable("catelogId")Long catelogId){
         
    
            //1、查出当前分类下的所有属性分组,
            //2、查出每个属性分组的所有属性
           List<AttrGroupWithAttrsVo> vos =  attrGroupService.getAttrGroupWithAttrsByCatelogId(catelogId);
           return R.ok().put("data",vos);
        }
    

    service细节:
    $$

    $$

        /**
         * 根据分类id查出所有的分组以及这些组里面的属性
         * @param catelogId
         * @return
         */
        @Override
        public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrsByCatelogId(Long catelogId) {
         
                //com.atguigu.gulimall.product.vo
            //1、查询分组信息
            List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
    
            //2、查询所有属性
            List<AttrGroupWithAttrsVo> collect = attrGroupEntities.stream().map(group -> {
         
                AttrGroupWithAttrsVo attrsVo = new AttrGroupWithAttrsVo();
                BeanUtils.copyProperties(group,attrsVo);
                List<AttrEntity> attrs = attrService.getRelationAttr(attrsVo.getAttrGroupId());
                attrsVo.setAttrs(attrs);
                return attrsVo;
            }).collect(Collectors.toList());
    
            return collect;
    
    
        }
    

    效果如下:

    分布式基础篇_第26张图片

    包括这些可选值都是从数据库中拿到的,也没啥问题。

    API-新增商品-商品新增vo抽取

    设置完商品一些列的值之后,点击保存商品信息,将要发送请求去保存这些商品的信息,下面这些是本次大保存涉及到的数据,需要传给后台,做一个大保存

    {
         
    	"spuName": "苹果Apple iPhone X全网通4G手机",
    	"spuDescription": "苹果Apple iPhone X全网通4G手机",
    	"catalogId": 225,
    	"brandId": 3,
    	"weight": 0.168,
    	"publishStatus": 0,
    	"decript": ["https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/212fe7ef-42c1-4e00-86d0-bb8f382920f1_f205d9c99a2b4b01.jpg"],
    	"images": ["https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/81a7afa2-075f-43e1-868b-8c2c1bdbc444_2b1837c6c50add30.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d20f3f2b-5acd-445a-8815-91e92f4704f1_5b5e74d0978360a1.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/be699789-a46d-4d45-8cc3-69efd35ffcd1_6a1b2703a9ed8737.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/fe6aeec8-b243-403c-8882-ee138f3469d2_7ae0120ec27dc3a7.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/0e8f75a6-1fef-4a64-8116-d6070eb66d43_23cd65077f12f7f5.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d0d64cf4-e7a8-4826-8c62-d861ec5958d3_63e862164165f483.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/b74953b5-c954-43d1-8448-a95682f55bf2_749d8efdff062fb0.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/dd28d44e-2b47-4ebe-80d6-aa0fac3cb813_a2c208410ae84d1f.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/cb5325c0-a774-4335-834e-8d2df3951aeb_b8494bf281991f94.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/37c60f20-4c4f-44e2-887a-89d21b6be866_ccd1077b985c7150.jpg", "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/5ae221d9-5e75-4c8f-8d20-e1e593dcbf79_e3284f319e256a5d.jpg"],
    	"bounds": {
         
    		"buyBounds": 1000,
    		"growBounds": 1000
    	},
    	"baseAttrs": [{
         
    		"attrId": 1,
    		"attrValues": "A1865",
    		"showDesc": 1
    	}, {
         
    		"attrId": 4,
    		"attrValues": "2019",
    		"showDesc": 1
    	}, {
         
    		"attrId": 6,
    		"attrValues": "177mm",
    		"showDesc": 1
    	}, {
         
    		"attrId": 7,
    		"attrValues": "以官网信息为准",
    		"showDesc": 1
    	}, {
         
    		"attrId": 9,
    		"attrValues": "机遇",
    		"showDesc": 0
    	}, {
         
    		"attrId": 11,
    		"attrValues": "牛逼克拉斯",
    		"showDesc": 0
    	}],
    	"skus": [{
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "黑色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+256"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 黑色 8+256",
    		"price": "5999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 黑色 8+256",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/81a7afa2-075f-43e1-868b-8c2c1bdbc444_2b1837c6c50add30.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/dd28d44e-2b47-4ebe-80d6-aa0fac3cb813_a2c208410ae84d1f.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/cb5325c0-a774-4335-834e-8d2df3951aeb_b8494bf281991f94.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}],
    		"descar": ["黑色", "8+256"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}, {
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "黑色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+128"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 黑色 8+128",
    		"price": "4999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 黑色 8+128",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/81a7afa2-075f-43e1-868b-8c2c1bdbc444_2b1837c6c50add30.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/dd28d44e-2b47-4ebe-80d6-aa0fac3cb813_a2c208410ae84d1f.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/cb5325c0-a774-4335-834e-8d2df3951aeb_b8494bf281991f94.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}],
    		"descar": ["黑色", "8+128"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}, {
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "白色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+256"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 白色 8+256",
    		"price": "5999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 白色 8+256",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/0e8f75a6-1fef-4a64-8116-d6070eb66d43_23cd65077f12f7f5.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/b74953b5-c954-43d1-8448-a95682f55bf2_749d8efdff062fb0.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}],
    		"descar": ["白色", "8+256"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}, {
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "白色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+128"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 白色 8+128",
    		"price": "4999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 白色 8+128",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/0e8f75a6-1fef-4a64-8116-d6070eb66d43_23cd65077f12f7f5.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/b74953b5-c954-43d1-8448-a95682f55bf2_749d8efdff062fb0.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}],
    		"descar": ["白色", "8+128"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}, {
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "红色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+256"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 红色 8+256",
    		"price": "5999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 红色 8+256",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d20f3f2b-5acd-445a-8815-91e92f4704f1_5b5e74d0978360a1.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/be699789-a46d-4d45-8cc3-69efd35ffcd1_6a1b2703a9ed8737.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d0d64cf4-e7a8-4826-8c62-d861ec5958d3_63e862164165f483.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/37c60f20-4c4f-44e2-887a-89d21b6be866_ccd1077b985c7150.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/5ae221d9-5e75-4c8f-8d20-e1e593dcbf79_e3284f319e256a5d.jpg",
    			"defaultImg": 0
    		}],
    		"descar": ["红色", "8+256"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}, {
         
    		"attr": [{
         
    			"attrId": 2,
    			"attrName": "颜色",
    			"attrValue": "红色"
    		}, {
         
    			"attrId": 12,
    			"attrName": "版本",
    			"attrValue": "8+128"
    		}],
    		"skuName": "苹果Apple iPhone X全网通4G手机 红色 8+128",
    		"price": "4999",
    		"skuTitle": "苹果Apple iPhone X全网通4G手机 红色 8+128",
    		"skuSubtitle": "【顺丰快递】【假一赔十】今日下单可送精美礼品查看>",
    		"images": [{
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d20f3f2b-5acd-445a-8815-91e92f4704f1_5b5e74d0978360a1.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/be699789-a46d-4d45-8cc3-69efd35ffcd1_6a1b2703a9ed8737.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/d0d64cf4-e7a8-4826-8c62-d861ec5958d3_63e862164165f483.jpg",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "",
    			"defaultImg": 0
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/37c60f20-4c4f-44e2-887a-89d21b6be866_ccd1077b985c7150.jpg",
    			"defaultImg": 1
    		}, {
         
    			"imgUrl": "https://sunyihang.oss-cn-beijing.aliyuncs.com/2020-10-21/5ae221d9-5e75-4c8f-8d20-e1e593dcbf79_e3284f319e256a5d.jpg",
    			"defaultImg": 0
    		}],
    		"descar": ["红色", "8+128"],
    		"fullCount": 0,
    		"discount": 0,
    		"countStatus": 0,
    		"fullPrice": 0,
    		"reducePrice": 0,
    		"priceStatus": 0,
    		"memberPrice": [{
         
    			"id": 2,
    			"name": "铜牌会员",
    			"price": 0
    		}, {
         
    			"id": 3,
    			"name": "银牌会员",
    			"price": 0
    		}]
    	}]
    }
    

    后台就需要准备一个vo来接收这些数据,如下:

    /*
    新增商品vo
    */
    @Data
    public class SpuSaveVo {
         
    
        private String spuName;    //spu名字
        private String spuDescription;  //spu的描述
        private Long catalogId;        //所属分类
        private Long brandId;        //所属品牌
        private BigDecimal weight;    //宽度
        private int publishStatus;      //发布状态
        private List<String> decript;   //说明信息
        private List<String> images;    //所有sku图片
        private Bounds bounds;          //spu的积分信息
        private List<BaseAttrs> baseAttrs; //所有基本属性
        private List<Skus> skus;   //所有sku商品
    
    
    }
    

    API-新增商品-商品新增业务流程分析

    http://www.gulixueyuan.com/course/369/task/13369/show#

    API-新增商品-保存SPU基本信息&保存SKU基本信息&调用远程服务保存优惠等信息

    大保存业务处理:

        /**
         * //TODO 高级部分完善
         * @param vo
         */
        @Transactional
        @Override
        public void saveSpuInfo(SpuSaveVo vo) {
         
    
            //1、保存spu基本信息 pms_spu_info
            SpuInfoEntity infoEntity = new SpuInfoEntity();
            BeanUtils.copyProperties(vo,infoEntity);
            infoEntity.setCreateTime(new Date());
            infoEntity.setUpdateTime(new Date());
            this.saveBaseSpuInfo(infoEntity);
    
            //2、保存Spu的描述图片 pms_spu_info_desc
            List<String> decript = vo.getDecript();
            SpuInfoDescEntity descEntity = new SpuInfoDescEntity();
            descEntity.setSpuId(infoEntity.getId());
            descEntity.setDecript(String.join(",",decript));
            spuInfoDescService.saveSpuInfoDesc(descEntity);
    
    
    
            //3、保存spu的图片集 pms_spu_images
            List<String> images = vo.getImages();
            imagesService.saveImages(infoEntity.getId(),images);
    
    
            //4、保存spu的规格参数;pms_product_attr_value
            List<BaseAttrs> baseAttrs = vo.getBaseAttrs();
            List<ProductAttrValueEntity> collect = baseAttrs.stream().map(attr -> {
         
                ProductAttrValueEntity valueEntity = new ProductAttrValueEntity();
                valueEntity.setAttrId(attr.getAttrId());
                AttrEntity id = attrService.getById(attr.getAttrId());
                valueEntity.setAttrName(id.getAttrName());
                valueEntity.setAttrValue(attr.getAttrValues());
                valueEntity.setQuickShow(attr.getShowDesc());
                valueEntity.setSpuId(infoEntity.getId());
    
                return valueEntity;
            }).collect(Collectors.toList());
            attrValueService.saveProductAttr(collect);
    
    
            //5、保存spu的积分信息;gulimall_sms->sms_spu_bounds
            Bounds bounds = vo.getBounds();
            SpuBoundTo spuBoundTo = new SpuBoundTo();
            BeanUtils.copyProperties(bounds,spuBoundTo);
            spuBoundTo.setSpuId(infoEntity.getId());
            R r = couponFeignService.saveSpuBounds(spuBoundTo);
            if(r.getCode() != 0){
         
                log.error("远程保存spu积分信息失败");
            }
    
    
            //5、保存当前spu对应的所有sku信息;
    
            List<Skus> skus = vo.getSkus();
            if(skus!=null && skus.size()>0){
         
                skus.forEach(item->{
         
                    String defaultImg = "";
                    for (Images image : item.getImages()) {
         
                        if(image.getDefaultImg() == 1){
         
                            defaultImg = image.getImgUrl();
                        }
                    }
                    //    private String skuName;
                    //    private BigDecimal price;
                    //    private String skuTitle;
                    //    private String skuSubtitle;
                    SkuInfoEntity skuInfoEntity = new SkuInfoEntity();
                    BeanUtils.copyProperties(item,skuInfoEntity);
                    skuInfoEntity.setBrandId(infoEntity.getBrandId());
                    skuInfoEntity.setCatalogId(infoEntity.getCatalogId());
                    skuInfoEntity.setSaleCount(0L);
                    skuInfoEntity.setSpuId(infoEntity.getId());
                    skuInfoEntity.setSkuDefaultImg(defaultImg);
                    //5.1)、sku的基本信息;pms_sku_info
                    skuInfoService.saveSkuInfo(skuInfoEntity);
    
                    Long skuId = skuInfoEntity.getSkuId();
    
                    List<SkuImagesEntity> imagesEntities = item.getImages().stream().map(img -> {
         
                        SkuImagesEntity skuImagesEntity = new SkuImagesEntity();
                        skuImagesEntity.setSkuId(skuId);
                        skuImagesEntity.setImgUrl(img.getImgUrl());
                        skuImagesEntity.setDefaultImg(img.getDefaultImg());
                        return skuImagesEntity;
                    }).filter(entity->{
         
                        //返回true就是需要,false就是剔除
                        return !StringUtils.isEmpty(entity.getImgUrl());
                    }).collect(Collectors.toList());
                    //5.2)、sku的图片信息;pms_sku_image
                    skuImagesService.saveBatch(imagesEntities);
                    //TODO 没有图片路径的无需保存
    
                    List<Attr> attr = item.getAttr();
                    List<SkuSaleAttrValueEntity> skuSaleAttrValueEntities = attr.stream().map(a -> {
         
                        SkuSaleAttrValueEntity attrValueEntity = new SkuSaleAttrValueEntity();
                        BeanUtils.copyProperties(a, attrValueEntity);
                        attrValueEntity.setSkuId(skuId);
    
                        return attrValueEntity;
                    }).collect(Collectors.toList());
                    //5.3)、sku的销售属性信息:pms_sku_sale_attr_value
                    skuSaleAttrValueService.saveBatch(skuSaleAttrValueEntities);
    
                    // //5.4)、sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
                    SkuReductionTo skuReductionTo = new SkuReductionTo();
                    BeanUtils.copyProperties(item,skuReductionTo);
                    skuReductionTo.setSkuId(skuId);
                    if(skuReductionTo.getFullCount() >0 || skuReductionTo.getFullPrice().compareTo(new BigDecimal("0")) == 1){
         
                        R r1 = couponFeignService.saveSkuReduction(skuReductionTo);
                        if(r1.getCode() != 0){
         
                            log.error("远程保存sku优惠信息失败");
                        }
                    }
    
    
    
                });
            }
    
    
       //保存优惠服务代码
       @Override
        public void saveSkuReduction(SkuReductionTo reductionTo) {
         
            //1、// //5.4)、sku的优惠、满减等信息;gulimall_sms->sms_sku_ladder\sms_sku_full_reduction\sms_member_price
            //sms_sku_ladder
            SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
            skuLadderEntity.setSkuId(reductionTo.getSkuId());
            skuLadderEntity.setFullCount(reductionTo.getFullCount());
            skuLadderEntity.setDiscount(reductionTo.getDiscount());
            skuLadderEntity.setAddOther(reductionTo.getCountStatus());
            if(reductionTo.getFullCount() > 0){
         
                skuLadderService.save(skuLadderEntity);
            }
    
    
    
    
            //2、sms_sku_full_reduction
            SkuFullReductionEntity reductionEntity = new SkuFullReductionEntity();
            BeanUtils.copyProperties(reductionTo,reductionEntity);
            if(reductionEntity.getFullPrice().compareTo(new BigDecimal("0"))==1){
         
                this.save(reductionEntity);
            }
    
    
            //3、sms_member_price
            List<MemberPrice> memberPrice = reductionTo.getMemberPrice();
    
            List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
         
                MemberPriceEntity priceEntity = new MemberPriceEntity();
                priceEntity.setSkuId(reductionTo.getSkuId());
                priceEntity.setMemberLevelId(item.getId());
                priceEntity.setMemberLevelName(item.getName());
                priceEntity.setMemberPrice(item.getPrice());
                priceEntity.setAddOther(1);
                return priceEntity;
            }).filter(item->{
         
                return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
            }).collect(Collectors.toList());
    
            memberPriceService.saveBatch(collect);
        }
    
    }
    

    API-新增商品-商品保存debug完成

    http://www.gulixueyuan.com/course/369/task/13372/show

    API-新增商品-商品保存其他问题处理

    1)在保存sku图片集的时候,如果是空图片路径,就不保存了,

    分布式基础篇_第27张图片

    2)在保存优惠信息的时候,有些优惠没写,但是前端也将默认的值传到后台了,还是录入了数据库,痛点如下:

    分布式基础篇_第28张图片

    如果是没有设置优惠信息,就给他从数据中剔除,不往数据库里塞。

    分布式基础篇_第29张图片

    如果没有优惠满减信息,就不用远程保存优惠信息了。

    包括在保存sku满减打折信息的时候,如果没有满减打折信息,就不保存了。

    
            //sms_sku_ladder 保存打折信息
            SkuLadderEntity skuLadderEntity = new SkuLadderEntity();
            skuLadderEntity.setSkuId(reductionTo.getSkuId());
            skuLadderEntity.setFullCount(reductionTo.getFullCount());
            skuLadderEntity.setDiscount(reductionTo.getDiscount());
            skuLadderEntity.setAddOther(reductionTo.getCountStatus());
            if(reductionTo.getFullCount() > 0){
         
                skuLadderService.save(skuLadderEntity);
            }
    
           
            //2、sms_sku_full_reduction 保存满减信息
            SkuFullReductionEntity reductionEntity = new SkuFullReductionEntity();
            BeanUtils.copyProperties(reductionTo,reductionEntity);
            if(reductionEntity.getFullPrice().compareTo(new BigDecimal("0"))==1){
         
                this.save(reductionEntity);
            }
    

    在保存优惠积分信息时,如果会员价都没有,就不用保存会员价格了。

            //3、sms_member_price 保存会员价格
            List<MemberPrice> memberPrice = reductionTo.getMemberPrice();
    
            List<MemberPriceEntity> collect = memberPrice.stream().map(item -> {
         
                MemberPriceEntity priceEntity = new MemberPriceEntity();
                priceEntity.setSkuId(reductionTo.getSkuId());
                priceEntity.setMemberLevelId(item.getId());
                priceEntity.setMemberLevelName(item.getName());
                priceEntity.setMemberPrice(item.getPrice());
                priceEntity.setAddOther(1);
                return priceEntity;
            }).filter(item->{
         
                return item.getMemberPrice().compareTo(new BigDecimal("0")) == 1;
            }).collect(Collectors.toList());
    
            memberPriceService.saveBatch(collect);
        }
    
    }
    

    成功容易,失败难,如果积分保存失败了,该怎么办,这些东西我们放到高级篇再来探索,至此,商品新增就完了。

    API-商品管理-SPU检索

    当点击spu管理目录后,页面初始化时会发送一个请求获取到所有的spu信息来供用户做一套增删改查:

    http://localhost:88/api/product/spuinfo/list?t=1603263634865&page=1&limit=10
    

    请求接口业务细节(该页面和前面几个一样,也涉及到了各种条件的复杂检索):

        @Override
        public PageUtils queryPageByCondition(Map<String, Object> params) {
         
    
            QueryWrapper<SpuInfoEntity> wrapper = new QueryWrapper<>();
    
            //按照关键字查
            String key = (String) params.get("key");
            if(!StringUtils.isEmpty(key)){
         
                wrapper.and((w)->{
         
                    w.eq("id",key).or().like("spu_name",key);
                });
            }
            // status=1 and (id=1 or spu_name like xxx) 按照发布状态查
            String status = (String) params.get("status");
            if(!StringUtils.isEmpty(status)){
         
                wrapper.eq("publish_status",status);
            }
    
            //按照品牌查
            String brandId = (String) params.get("brandId");
            if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(brandId)){
         
                wrapper.eq("brand_id",brandId);
            }
            
            //按照分类查
            String catelogId = (String) params.get("catelogId");
            if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){
         
                wrapper.eq("catalog_id",catelogId);
            }
    
            /**
             * status: 2
             * key:
             * brandId: 9
             * catelogId: 225
             */
    
            IPage<SpuInfoEntity> page = this.page(
                    new Query<SpuInfoEntity>().getPage(params),
                    wrapper
            );
    
            return new PageUtils(page);
        }
    
    

    在SPU中,写出的日期数据都不符合规则:
    image-20200509083248660

    想要符合规则,可以设置写出数据的规则:

    spring.jackson

      jackson:
        date-format: yyyy-MM-dd HH:mm:ss
    

    也没啥问题。

    API-商品管理-SKU检索

    当点击商品管理目录后,页面初始化时会发送一个请求获取到所有的sku信息来供用户做一套增删改查:

    http://localhost:88/api/product/skuinfo/list?t=1603265481373&page=1&limit=10&key=&catelogId=0&brandId=0&min=0&max=0
    

    请求接口业务细节(该页面和前面几个一样,也涉及到了各种条件的复杂检索):

        @Override
        public PageUtils queryPageByCondition(Map<String, Object> params) {
         
            QueryWrapper<SkuInfoEntity> queryWrapper = new QueryWrapper<>();
            /**
             * key:
             * catelogId: 0
             * brandId: 0
             * min: 0
             * max: 0
             */
            
            //按关键字查
            String key = (String) params.get("key");
            if(!StringUtils.isEmpty(key)){
         
                queryWrapper.and((wrapper)->{
         
                   wrapper.eq("sku_id",key).or().like("sku_name",key);
                });
            }
            
            //按分类查
            String catelogId = (String) params.get("catelogId");
            if(!StringUtils.isEmpty(catelogId)&&!"0".equalsIgnoreCase(catelogId)){
         
    
                queryWrapper.eq("catalog_id",catelogId);
            }
            
            //按品牌查
            String brandId = (String) params.get("brandId");
            if(!StringUtils.isEmpty(brandId)&&!"0".equalsIgnoreCase(catelogId)){
         
                queryWrapper.eq("brand_id",brandId);
            }
    
            //按照价格区间查询
            String min = (String) params.get("min");
            if(!StringUtils.isEmpty(min)){
         
                queryWrapper.ge("price",min);
            }
    
            String max = (String) params.get("max");
    
            if(!StringUtils.isEmpty(max)  ){
         
                try{
         
                    BigDecimal bigDecimal = new BigDecimal(max);
    
                    if(bigDecimal.compareTo(new BigDecimal("0"))==1){
         
                        queryWrapper.le("price",max);
                    }
                }catch (Exception e){
         
    
                }
    
            }
    
    
            IPage<SkuInfoEntity> page = this.page(
                    new Query<SkuInfoEntity>().getPage(params),
                    queryWrapper
            );
    
            return new PageUtils(page);
        }
    
    

    也没啥问题。

    7)仓储服务

    API-仓库管理-整合ware服务&获取仓库列表

    将库存服务注册到注册中心去,再来配置一下网关路由,让前端能够访问通库存服务,启动库存服务。

    确认前端访问没问题了之后,点击仓库维护,发现还没任何的仓库:

    分布式基础篇_第30张图片

    可以给他新增几个(默认都是逆向生成的方法,也没啥问题)。

    分布式基础篇_第31张图片

    除了这些生成的基本增删改查外,这个关键字查询,我们需要自己做一下,这里就不再列出了。

    API-仓库管理-查询库存&创建采购需求

    点击商品库存,需要查询出所有的库存信息再页面上做一个展示:

    http://localhost:88/api/ware/waresku/list?t=1603267581015&page=1&limit=10&skuId=&wareId=
    

    请求接口:

        @Override
        public PageUtils queryPage(Map<String, Object> params) {
         
            /**
             * skuId: 1
             * wareId: 2
             */
            QueryWrapper<WareSkuEntity> queryWrapper = new QueryWrapper<>();
            String skuId = (String) params.get("skuId");
            if(!StringUtils.isEmpty(skuId)){
         
                queryWrapper.eq("sku_id",skuId);
            }
    
            String wareId = (String) params.get("wareId");
            if(!StringUtils.isEmpty(wareId)){
         
                queryWrapper.eq("ware_id",wareId);
            }
    
    
            IPage<WareSkuEntity> page = this.page(
                    new Query<WareSkuEntity>().getPage(params),
                    queryWrapper
            );
    
            return new PageUtils(page);
        }
    

    还没有库存信息,我们可以点击新增,新增几条库存信息。

    分布式基础篇_第32张图片

    包括哪个商品的库存,哪个仓库的库存,库存数多少,锁了几件库存
    分布式基础篇_第33张图片

    点击确定需要发送请求,给库存表里面新增一条记录,看似简单的操作,其实新增库存并不是这么新增库存的,这只是一个数据库的可视化操作界面,对于我们这种自营的商城平台,库存是需要通过采购人员通过将采购需求合并成采购单,库存人员拿着这个采购单去采购,如果采购成功,采购人员通过一个手机APP往数据库中去新增库存,包括新增在哪个仓库,库存数库存名和采购人等等。(采购单采购需求可以理解为小时候你妈妈叫你去小卖部买盐,醋,辣椒粉…这些都是你妈妈对你提出的采购需求,你妈妈把这些要买的东西写到一张纸上,这张纸就是采购单,而你就是采购人员,你去小卖部买到东西,就交给你妈妈,你妈妈把这些东西放到冰箱,冰箱就相当于仓库)

    照着前面这个逻辑,我们想要给商品添加库存,首先要认清采购需求,点击采购需求目录:

    分布式基础篇_第34张图片

    点击新增采购需求,新增几条(新增采购需求可以是人工手动添加,也可以是系统定时任务观察到库存不足,自动创建的采购需求)。

    分布式基础篇_第35张图片

    API-仓库管理-合并采购需求

    接下来可以选中两个采购需求,点击批量操作里的合并整单,需要查询出还没有被分配并且是新建,或者是已经分配给采购人员但是还没出发采购的采购单。

    分布式基础篇_第36张图片

    如果没有选择要合并到哪个采购单上,将会新建一个采购单,合并到他身上。(系统也会有提示)

    分布式基础篇_第37张图片

    点击确定,发现刚才的那两个采购需求状态变为了已分配,来到新建的采购单:

    分布式基础篇_第38张图片

    我们可以把这个采购单分配给相关的采购人员,让他去采购。(你妈妈已经把想买的东西写在了纸上,现在只差把这张纸给你,让你去帮她买了)

    点击分配:

    选择你需要分配给的人(具体的人员可以在管理员列表去添加):

    分布式基础篇_第39张图片

    将采购单分配给采购人员之后,采购单状态就变为了已分配。

    分布式基础篇_第40张图片

    image-20200509191108806

    API-仓库管理-领取采购单

    当采购单分配给指定的采购人员之后,采购人员根据手机app就可以看到,哦,今天这个采购单分配给我了,又有活淦了,之后,该采购人员点击领取采购单,就需要修改采购单的状态,为已领取,同时,已经被领取的采购单,对应的采购需求就应该为正在采购,不能再次被合并。

    请求接口

    采购人员领取采购单: https://easydoc.xyz/doc/75716633/ZUqEdvA4/vXMBBgw1

    service细节:

     /**
         *
         * @param ids 采购单id
         */
        @Override
        public void received(List<Long> ids) {
         
            //1、确认当前采购单是新建或者已分配状态
            List<PurchaseEntity> collect = ids.stream().map(id -> {
         
                PurchaseEntity byId = this.getById(id);
                return byId;
            }).filter(item -> {
         
                if (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||
                        item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {
         
                    return true;
                }
                return false;
            }).map(item->{
         
                item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
                item.setUpdateTime(new Date());
                return item;
            }).collect(Collectors.toList());
    
            //2、改变采购单的状态
            this.updateBatchById(collect);
    
    
    
            //3、改变采购项的状态
            collect.forEach((item)->{
         
                List<PurchaseDetailEntity> entities = detailService.listDetailByPurchaseId(item.getId());
                List<PurchaseDetailEntity> detailEntities = entities.stream().map(entity -> {
         
                    PurchaseDetailEntity entity1 = new PurchaseDetailEntity();
                    entity1.setId(entity.getId());
                    entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
                    return entity1;
                }).collect(Collectors.toList());
                detailService.updateBatchById(detailEntities);
            });
        }
    

    模拟领取采购单:

    分布式基础篇_第41张图片

    在这里插入图片描述

    分布式基础篇_第42张图片

    API-仓库管理-完成采购

    采购人员采购完成后,将商品放入了真实仓库,就需要在手机app上执行采购完成的操作(改变采购单采购项状态、将采购数据入数据库)。

    请求接口:

     @Transactional
        @Override
        public void done(PurchaseDoneVo doneVo) {
         
    
            Long id = doneVo.getId();
    
    
            //2、改变采购项的状态
            Boolean flag = true;
            List<PurchaseItemDoneVo> items = doneVo.getItems();
    
            List<PurchaseDetailEntity> updates = new ArrayList<>();
            for (PurchaseItemDoneVo item : items) {
         
                PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
                if(item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()){
         
                    flag = false;
                    detailEntity.setStatus(item.getStatus());
                }else{
         
                    detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode());
                    3、将成功采购的进行入库
                    PurchaseDetailEntity entity = detailService.getById(item.getItemId());
                    wareSkuService.addStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum());
    
                }
                detailEntity.setId(item.getItemId());
                updates.add(detailEntity);
            }
    
            detailService.updateBatchById(updates);
    
            //1、改变采购单状态
            PurchaseEntity purchaseEntity = new PurchaseEntity();
            purchaseEntity.setId(id);
            purchaseEntity.setStatus(flag?WareConstant.PurchaseStatusEnum.FINISH.getCode():WareConstant.PurchaseStatusEnum.HASERROR.getCode());
            purchaseEntity.setUpdateTime(new Date());
            this.updateById(purchaseEntity);
    
    
    
    
        }
    

    测试采购完成接口(一个采购项成功,一个失败,采购单只要有一个采购失败,就是有异常):

    分布式基础篇_第43张图片

    效果如下:

    分布式基础篇_第44张图片

    模拟完成采购就完成了。

    API-商品管理-SPU规格维护

    获取spu规格

    在SPU管理页面,获取商品规格的时候,出现400异常,浏览器显示跳转不了

    问题现象:

    image-20200510182051355

    出现问题的代码:

        attrUpdateShow(row) {
         
          console.log(row);
          this.$router.push({
         
            path: "/product-attrupdate",
            query: {
          spuId: row.id, catalogId: row.catalogId }
          });
        },
    

    暂时不知道如何解决问题。只能留待以后解决。

    经过测试发现,问题和上面的代码没有关系,问题出现在“attrupdate.vue”上,该vue页面无法通过浏览器访问,当输入访问URL( http://localhost:8001/#/product-attrupdate )的时候,就会出现404,而其他的请求则不会出现这种情况,不知为何。

    通过POSTMAN进行请求的时候,能够请求到数据。

    经过分析发现,是因为在数据库中没有该页面的导航所导致的,为了修正这个问题,可以在“sys-menu”表中添加一行,内容位:

    image-20200510231012714

    这样当再次访问的时候,在“平台属性”下,会出现“规格维护”菜单,

    image-20200510231041708

    当再次点击“规格”的时候,显示出菜单

    image-20200510231200130

    不过这种菜单并不符合我们的需要,我们需要让它以弹出框的形式出现。

    修改商品规格

    API: https://easydoc.xyz/doc/75716633/ZUqEdvA4/GhnJ0L85

    URL:/product/attr/update/{spuId}

    8)分布式基础篇总结

    );
    String skuId = (String) params.get(“skuId”);
    if(!StringUtils.isEmpty(skuId)){
    queryWrapper.eq(“sku_id”,skuId);
    }

        String wareId = (String) params.get("wareId");
        if(!StringUtils.isEmpty(wareId)){
            queryWrapper.eq("ware_id",wareId);
        }
    
    
        IPage page = this.page(
                new Query().getPage(params),
                queryWrapper
        );
    
        return new PageUtils(page);
    }
    
    
    还没有库存信息,我们可以点击新增,新增几条库存信息。
    
    [外链图片转存中...(img-sFMDzUhf-1603345710035)]
    
    包括哪个商品的库存,哪个仓库的库存,库存数多少,锁了几件库存
    
    [外链图片转存中...(img-H7ixksV0-1603345710035)]
    
    点击确定需要发送请求,给库存表里面新增一条记录,看似简单的操作,其实新增库存并不是这么新增库存的,这只是一个数据库的可视化操作界面,对于我们这种自营的商城平台,库存是需要通过采购人员通过将采购需求合并成采购单,库存人员拿着这个采购单去采购,如果采购成功,采购人员通过一个手机APP往数据库中去新增库存,包括新增在哪个仓库,库存数库存名和采购人等等。(采购单采购需求可以理解为小时候你妈妈叫你去小卖部买盐,醋,辣椒粉...这些都是你妈妈对你提出的采购需求,你妈妈把这些要买的东西写到一张纸上,这张纸就是采购单,而你就是采购人员,你去小卖部买到东西,就交给你妈妈,你妈妈把这些东西放到冰箱,冰箱就相当于仓库)
    
    照着前面这个逻辑,我们想要给商品添加库存,首先要认清采购需求,点击采购需求目录:
    
    [外链图片转存中...(img-JbkY7jY2-1603345710036)]
    
    点击新增采购需求,新增几条(新增采购需求可以是人工手动添加,也可以是系统定时任务观察到库存不足,自动创建的采购需求)。
    
    [外链图片转存中...(img-tNmtIhY8-1603345710037)]
    
    
    
    ## API-仓库管理-合并采购需求
    
    接下来可以选中两个采购需求,点击批量操作里的合并整单,需要查询出还没有被分配并且是新建,或者是已经分配给采购人员但是还没出发采购的采购单。
    
    [外链图片转存中...(img-ejf4SElJ-1603345710039)]
    
    如果没有选择要合并到哪个采购单上,将会新建一个采购单,合并到他身上。(系统也会有提示)
    
    [外链图片转存中...(img-SPmP3Pxv-1603345710040)]
    
    点击确定,发现刚才的那两个采购需求状态变为了已分配,来到新建的采购单:
    
    [外链图片转存中...(img-GmTXQJ14-1603345710041)]
    
    我们可以把这个采购单分配给相关的采购人员,让他去采购。(你妈妈已经把想买的东西写在了纸上,现在只差把这张纸给你,让你去帮她买了)
    
    点击分配:
    
    选择你需要分配给的人(具体的人员可以在管理员列表去添加):
    
    [外链图片转存中...(img-kncwSBkk-1603345710043)]
    
    将采购单分配给采购人员之后,采购单状态就变为了已分配。
    
    [外链图片转存中...(img-Mx0RakqI-1603345710044)]
    
    ![image-20200509191108806](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9mZXJtaGFuLm9zcy1jbi1xaW5nZGFvLmFsaXl1bmNzLmNvbS9ndWxpL2ltYWdlLTIwMjAwNTA5MTkxMTA4ODA2LnBuZw?x-oss-process=image/format,png)
    
    
    
    ## API-仓库管理-领取采购单
    
    当采购单分配给指定的采购人员之后,采购人员根据手机app就可以看到,哦,今天这个采购单分配给我了,又有活淦了,之后,该采购人员点击领取采购单,就需要修改采购单的状态,为已领取,同时,已经被领取的采购单,对应的采购需求就应该为正在采购,不能再次被合并。
    
    请求接口
    
    采购人员领取采购单: https://easydoc.xyz/doc/75716633/ZUqEdvA4/vXMBBgw1
    
    service细节:
    
    ```java
     /**
         *
         * @param ids 采购单id
         */
        @Override
        public void received(List ids) {
            //1、确认当前采购单是新建或者已分配状态
            List collect = ids.stream().map(id -> {
                PurchaseEntity byId = this.getById(id);
                return byId;
            }).filter(item -> {
                if (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||
                        item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {
                    return true;
                }
                return false;
            }).map(item->{
                item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());
                item.setUpdateTime(new Date());
                return item;
            }).collect(Collectors.toList());
    
            //2、改变采购单的状态
            this.updateBatchById(collect);
    
    
    
            //3、改变采购项的状态
            collect.forEach((item)->{
                List entities = detailService.listDetailByPurchaseId(item.getId());
                List detailEntities = entities.stream().map(entity -> {
                    PurchaseDetailEntity entity1 = new PurchaseDetailEntity();
                    entity1.setId(entity.getId());
                    entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());
                    return entity1;
                }).collect(Collectors.toList());
                detailService.updateBatchById(detailEntities);
            });
        }
    

    模拟领取采购单:

    [外链图片转存中…(img-7idYRGau-1603345710045)]

    [外链图片转存中…(img-XGrnYCF3-1603345710047)]

    [外链图片转存中…(img-R3qix7zO-1603345710048)]

    API-仓库管理-完成采购

    采购人员采购完成后,将商品放入了真实仓库,就需要在手机app上执行采购完成的操作(改变采购单采购项状态、将采购数据入数据库)。

    请求接口:

     @Transactional
        @Override
        public void done(PurchaseDoneVo doneVo) {
         
    
            Long id = doneVo.getId();
    
    
            //2、改变采购项的状态
            Boolean flag = true;
            List<PurchaseItemDoneVo> items = doneVo.getItems();
    
            List<PurchaseDetailEntity> updates = new ArrayList<>();
            for (PurchaseItemDoneVo item : items) {
         
                PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();
                if(item.getStatus() == WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()){
         
                    flag = false;
                    detailEntity.setStatus(item.getStatus());
                }else{
         
                    detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode());
                    3、将成功采购的进行入库
                    PurchaseDetailEntity entity = detailService.getById(item.getItemId());
                    wareSkuService.addStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum());
    
                }
                detailEntity.setId(item.getItemId());
                updates.add(detailEntity);
            }
    
            detailService.updateBatchById(updates);
    
            //1、改变采购单状态
            PurchaseEntity purchaseEntity = new PurchaseEntity();
            purchaseEntity.setId(id);
            purchaseEntity.setStatus(flag?WareConstant.PurchaseStatusEnum.FINISH.getCode():WareConstant.PurchaseStatusEnum.HASERROR.getCode());
            purchaseEntity.setUpdateTime(new Date());
            this.updateById(purchaseEntity);
    
    
    
    
        }
    

    测试采购完成接口(一个采购项成功,一个失败,采购单只要有一个采购失败,就是有异常):

    [外链图片转存中…(img-eg3u0Dmj-1603345710050)]

    效果如下:

    [外链图片转存中…(img-CZrvcelf-1603345710051)]

    模拟完成采购就完成了。

    8)分布式基础篇总结

    分布式基础篇_第45张图片

你可能感兴趣的:(java,spring,mysql,redis,数据库)