谷粒商城-基础篇

谷粒商城

1.环境搭建

1.1 使用vagrant创建linux环境

VirtualBox:https://www.virtualbox.org/wiki/Downloads

Vagrant:https://www.vagrantup.com/downloads

seata:http://seata.io/zh-cn/blog/download.html

Sentinel:https://github.com/alibaba/Sentinel

SwitchHosts:https://github.com/oldj/SwitchHosts

vue-devtools:https://github.com/vuejs/devtools

1.2安装VirtualBox和Vagrant

(1)下载后先安装VirtualBox,然后安装Vagrant,一路next即可,建议不要安装c盘

(2)cmd查看Vagrant是否安装成功

vagrant

image-20220211145606749

(3)vagrant init centos/7:初始化

https://app.vagrantup.com/boxes/search

image-20220211145659169

image-20220211145800646

(4)vagrant up:启动虚拟机

第一次启动需要去下载centos/7

image-20220211154938097

说明启动成功

image-20220211155431642

(5)vagrant ssh 打开命令行

image-20220211155535791

1.3 网络配置

(1)找到配置文件,修改配置

C:\Users\Peng\Vagrantfile

image-20220211155754022

(2)ipconfig 检查本机VirtualBox地址,修改配置的网段需要一致,这里都是56

image-20220211160519748

(3)vagrant reload 重启虚拟机

image-20220211160701279

(4)测试本机与虚拟机能否互相ping通

查看ip命令

winows:ipconfig

centos:ip addr

image-20220211161459821

image-20220211161541807

1.4安装docker

(1)删除老版本

sudo yum remove docker \
                  docker-client \
                  docker-client-latest \
                  docker-common \
                  docker-latest \
                  docker-latest-logrotate \
                  docker-logrotate \
                  docker-engine

(2) 安装工具包并设置存储库

sudo yum install -y yum-utils

sudo yum-config-manager \
    --add-repo \
    https://download.docker.com/linux/centos/docker-ce.repo

(3)安装docker引擎

sudo yum install docker-ce docker-ce-cli containerd.io

(4)docker基本使用命令

启动docker

sudo systemctl start docker

检查docker版本

docker -v

查看docker已有镜像

sudo docker images

设置docker开机启动

sudo systemctl enable docker

(5)设置阿里云镜像仓库

创建文件

sudo mkdir -p /etc/docker

修改配置, 设置镜像

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

重启后台线程

sudo systemctl daemon-reload

重启docker

sudo systemctl restart docker

docker查错

 sudo dockerd --debug

1.5docker安装mysql

(1)docker安装mysql

sudo docker pull mysql:5.7

(2)docker启动mysql

参数:

  • -p 3306:3306:将容器的3306端口映射到主机的3306端口

  • --name:给容器命名

  • -v /mydata/mysql/log:/var/log/mysql:将配置文件挂载到主机/mydata/..

  • -e MYSQL_ROOT_PASSWORD=root:初始化root用户的密码为root

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

(3)查看docker启动的容器:

sudo docker ps

(4)配置mysql

进入挂载的mysql配置目录

cd /mydata/mysql/conf

修改配置文件 my.cnf

vi my.cnf

输入以下内容

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

# Esc
# :wq

docker重启mysql使配置生效

docker restart mysql

1.6docker安装redis

(1)docker拉取redis镜像

docker pull redis

(2)docker启动redis

mkdir -p /mydata/redis/conf

touch /mydata/redis/conf/redis.conf

(3)启动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

(4)配置redis持久化

echo "appendonly yes"  >> /mydata/redis/conf/redis.conf

#重启生效
docker restart redis

(5)容器随docker启动自动运行

# mysql
docker update mysql --restart=always

# redis
docker update redis --restart=always

1.7开发环境

(1)maven

<mirror>
    <id>alimaven</id>
    <mirrorOf>central</mirrorOf>
    <name>aliyun maven</name>
    <url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>

(2)插件

image-20220211164207737

lombok

image-20220211164139925

mybatisx

image-20220211164116111

(3)vscode

地址:https://code.visualstudio.com/

安装以下组件:

  • Auto Close Tag
  • Auto Rename Tag
  • Chinese
  • ESlint
  • HTML CSS Support
  • HTML Snippets
  • JavaScript ES6
  • Live Server
  • open in brower
  • Vetur

1.8配置git-ssh

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

配置邮箱
git config --global user.email "55333@qq.com" // 注册账号时使用的邮箱

配置ssh免密登录
ssh-keygen -t rsa -C "55333@qq.com"
三次回车后生成了密钥:公钥私钥
cat ~/.ssh/id_rsa.pub

也可以查看密钥
浏览器登录码云后,个人头像上点设置--ssh公钥---随便填个标题---复制

测试
ssh -T git@gitee.com

1.9 码云项目构建

(1)创建项目仓库

image-20220211165857559

image-20220211165918177

(2)使用idea Clone项目

image-20220211171504066

image-20220211170158404

(3)创建微服务模块

  • com.peng.gulimall
  • gulimall-product
  • com.peng.gulimall.product

image-20220211171342764

选择springWeb和OpenFeign

image-20220211171329570

(4)项目结构

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

image-20220211220056358

(5)建立聚合服务

image-20220211220304551

<modelVersion>4.0.0</modelVersion>
<groupId>com.peng.gulimall</groupId>
<artifactId>gulimall</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>gulimall</name>
<description>聚合服务</description>
<packaging>pom</packaging>
<modules>
    <module>gulimail-product</module>
    <module>gulimall-coupon</module>
    <module>gulimall-member</module>
    <module>gulimall-order</module>
    <module>gulimall-ware</module>
</modules>

(6)修改.gitignore

**/mvnw
**/mvnw.cmd

**/.mvn
**/target/

.idea

**/.gitignore

image-20220211175954004

(7)刷新后纳入版本控制

image-20220211180119684

(8)idea安装码云

image-20220211180239221

1.10数据库

地址:

create database gulimall_admin default character set utf8mb4 collate utf8mb4_general_ci;
create database gulimall_oms default character set utf8mb4 collate utf8mb4_general_ci;
create database gulimall_pms default character set utf8mb4 collate utf8mb4_general_ci;
create database gulimall_sms default character set utf8mb4 collate utf8mb4_general_ci;
create database gulimall_ums default character set utf8mb4 collate utf8mb4_general_ci;
create database gulimall_wms default character set utf8mb4 collate utf8mb4_general_ci;

1.11 vagrant和virtualbox本地安装

下载地址:

https://app.vagrantup.com/boxes/search

image-20220211145659169

 $ vagrant box add {title} {url}
 $ vagrant init {title}
 $ vagrant up

例如:

vagrant box add CentOS E:\Data\tool\vagrant\CentOS-7-x86_64-Vagrant-2004_01.VirtualBox.box

vagrant init CentOS 

vagrant up

image-20220215002735071

2.快速开发

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

(1)clone

地址:https://gitee.com/renrenio

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

image-20220211222822716

$ git clone git@gitee.com:renrenio/renren-fast.git

image-20220211222841114

(2)复制到项目中

<modules>
    <module>gulimail-product</module>
    <module>gulimall-coupon</module>
    <module>gulimall-member</module>
    <module>gulimall-order</module>
    <module>gulimall-ware</module>
    <module>renren-fast</module>
</modules>

image-20220211223049763

(3)执行mysql

image-20220211225704433

(4)修改启动端口号(默认80,容易冲突)和数据库连接地址

server:
  port: 8888

url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root

image-20220212001119603

(5)运行

image-20220211225412670

2.2 搭建前端环境

(1)安装node

地址:https://nodejs.org/zh-cn/download/

下载后选择安装地址(不建议c盘),一路next即可

检查node版本

node -v 

设置npm镜像地址

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

(2)前后联调

项目安装npm

npm install

image-20220211230326893

启动

npm run dev 

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

输出用户名admin密码admin 验证码,登录成功,前后端联调成功

image-20220211230622359

2.3逆向工程搭建&使用

(1)下载代码人人开源代码生成器

$ git clone git@gitee.com:renrenio/renren-generator.git

image-20220211231046749

(2)renren-generator加载到项目中

image-20220211232849028

(3)修改mysql数据库相关配置

url: jdbc:mysql://192.168.10.56:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: root

image-20220211233140996

(4)修改代码生成配置

# 主目录
mainPath=com.peng
#包名
package=com.peng.gulimall
moduleName=product
#作者
author=peng
#Email
email=pengpeng6135@163.com
#表前缀(类名不会包含表前缀)
tablePrefix=pms_

image-20220211233506098

(5)修改contoller模板
注释import org.apache.shiro.authz.annotation.RequiresPermissions命名空间

image-20220212002343153

注释Controller.java.vm模板中所有的@RequiresPermissions("${moduleName}😒{pathName}:list")

image-20220211233741331

(6)新建公共模块

新建模块

image-20220211234026550

选择maven项目

image-20220211234046280

新建gulimall-common

image-20220211234115991

配置gulimall-common的pom.xml

<parent>
    <artifactId>gulimall</artifactId>
    <groupId>com.peng.gulimall</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<packaging>pom</packaging>
<artifactId>gulimall-common</artifactId>
<description>每一个微服务公共的的依赖、bean、工具类</description>

image-20220211234657141

gulimall-common添加到pom.xml

image-20220211234518726

(7)gulimall-common依赖

  <dependencies>
        <!-- mybatisPLUS -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

        <!--简化实体类,用@Data代替getset方法-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.8</version>
        </dependency>
        <!-- httpcomponent包 -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpcore</artifactId>
            <version>4.4.13</version>
        </dependency>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>

        <!-- 数据库驱动 https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.17</version>
        </dependency>


    </dependencies>

(8)启动,访问 http://localhost:8888/#generator.html

image-20220212001308228

(9)生成代码

image-20220212001402745

(10)生成的代码复制到项目中去

image-20220212013646489

2.4测试基本基本的CRUD功能

(1)整合MyBatis-Plus

<!-- mybatisPLUS -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.3.2</version>
</dependency>

image-20220212005230572

(2)@MapperScan配置注入的数据访问层

image-20220212013121589

(3)配置application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.jdbc.Driver

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

image-20220212013545826

(4)测试

image-20220212013814420

2.5 所有微服务的CURD

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

如果UndoLog的有关服务报错,可以都先注释掉,暂时用不上

image-20220212164459645

2.5.1 coupon 优惠卷服务

renren-generator工程,修改generator.properties

image-20220212160222795

#主目录
mainPath=com.peng
#包名
package=com.peng.gulimall
#模块名
moduleName=coupon
#作者
author=peng
#email
email=pengpeng6135@163.com
#表前缀(类名不会包含表前缀) # 我们的sms数据库中的表的前缀都sms
如果写了表前缀,每一张表对于的javaBean就不会添加前缀了
tablePrefix=sms_ 

renren-generator工程,修改application.yml数据库信息

image-20220212160450187

server:
  port: 8888

# mysql
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #MySQL配置
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: root

gulimall-coupon工程,依赖于common,修改pom.xml

image-20220212160852383

<dependency>
    <groupId>com.peng.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>        
</dependency>

gulimall-coupon工程,修改application.yml数据库配置

image-20220212160923654

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

server:
  port: 7001

运行gulimallCouponApplication.java

image-20220212161128218

访问http://localhost:8888/#generator.html

image-20220212161210063

生成完的代码赋值到gulimall-coupon,以下是项目结构

image-20220212163848164

配置mapper映射,并启动

image-20220212164032885

@MapperScan("com.peng.gulimall.coupon.dao")

测试gulimall-coupon,访问http://localhost:7001/coupon/coupon/list

image-20220212164051551

2.5.2 member 用户服务

(1)renren-generator修改generator.properties

#模块名
moduleName=member
#表前缀(类名不会包含表前缀) # 我们的ums数据库中的表的前缀都ums
如果写了表前缀,每一张表对于的javaBean就不会添加前缀了
tablePrefix=ums_ 

image-20220212165609185

(2)renren-generator修改application.yml

 url: jdbc:mysql://192.168.56.10:3306/gulimall_ums?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai  

image-20220212165703440

(3)gulimall-member修改pom.xml

<dependency>
    <groupId>com.peng.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

image-20220212165823132

(4)gulimall-member修改application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_ums?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

server:
  port: 8002

(5)入口增加mapper映射

image-20220212170017639

(6)访问:http://localhost:8002/member/growthchangehistory/list

2.5.3 order 订单服务

(1)renren-generator修改generator.properties

#模块名
moduleName=order
#表前缀(类名不会包含表前缀) # 我们的oms数据库中的表的前缀都oms
如果写了表前缀,每一张表对于的javaBean就不会添加前缀了
tablePrefix=oms_ 

(2)renren-generator修改application.yml

url: jdbc:mysql://192.168.56.10:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai

(3)gulimall-member修改pom.xml

<dependency>
    <groupId>com.peng.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

(4)gulimall-member修改application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_oms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

server:
  port: 9000

(5)入口增加mapper映射
@MapperScan("com.peng.gulimall.order.dao")

(6)访问:http://localhost:9000/order/mqmessage/list

2.5.4 ware 存储服务

(1)renren-generator修改generator.properties

#模块名
moduleName=ware
#表前缀(类名不会包含表前缀) # 我们的wms数据库中的表的前缀都wms
如果写了表前缀,每一张表对于的javaBean就不会添加前缀了
tablePrefix=wms_ 

(2)renren-generator修改application.yml

url: jdbc:mysql://192.168.56.10:3306/gulimall_wms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai

(3)gulimall-member修改pom.xml

<dependency>
    <groupId>com.peng.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

(4)gulimall-member修改application.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_wms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver


mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

server:
  port: 10000

(5)入口增加mapper映射

@MapperScan("com.peng.gulimall.ware.dao")

(6)访问:http://localhost:10000/ware/purchase/list

3.Spring Cloud alibaba

3.1 Spring Cloud alibaba

地址:https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/README-zh.md

引入依赖:

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

image-20220212152535459

3.2 Spring Cloud alibaba Nacos 注册中心

地址:https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example/readme-zh.md

(1)下载Nacos Server

地址:https://github.com/alibaba/nacos/releases

image-20220212171850357

image-20220212173819748

(2)解压运行

默认是cluster,可以改成standalone,或者如下命令行运行,image-20220212225142450

单机运行

startup.cmd -m standalone

image-20220212212319512

(3)访问:http://127.0.0.1:8848/nacos/index.html#/login

用户名密码都是nacos

image-20220212212410875

(4)gulimall-common的pom.xml导入依赖:

 <dependency>
     <groupId>com.alibaba.cloud</groupId>
     <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>
 
 /*
 //如果出现loadbalancer相关错误,在nacos"包中移除ribbion依赖,并加入loadbalancer依赖"
         <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <!--不使用Ribbon 进行客户端负载均衡-->
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.cloud</groupId>
                    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
 */

(5)gulimall-member的application.yml增加nacos配置

cloud:
  nacos:
    discovery:
      server-addr: 127.0.0.1:8848
application:
  name: gulimall-member

image-20220212225347385

(6)gulimall-member使用 @EnableDiscoveryClient 注解开启服务注册与发现功能

@EnableDiscoveryClient

image-20220212225451384

(7)gulimall-coupon的application.yml增加nacos配置

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

image-20220212225610615

(8)gulimall-coupon使用 @EnableDiscoveryClient 注解开启服务注册与发现功能

@EnableDiscoveryClient

image-20220212225636440

(9)运行GulimallCouponApplication 和GulimallMemberApplication

image-20220212225745870

(10)登录nacos查看,服务已经注册进来

image-20220212225824470

注意:com.alibaba.cloud版本和nacos版本问题

com.alibaba.cloud是2.2.7.RELEASE版本,nacos如果是1.多版本就会注册不进去,服务起不来

image-20220212230057326

3.3 Spring Cloud Openfeign 远程调用

(1)导入依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
 //如果出现loadbalancer相关错误,在nacos"包中移除ribbion依赖,并加入loadbalancer依赖"
<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-loadbalancer</artifactId>
 </dependency>

image-20220212230404036

(2)gulimall-coupon的CouponController增加测试接口

@RequestMapping("/member/list")
public R membercoupons(){
    // 全系统的所有返回都返回R
    // 应该去数据库查用户对于的优惠券,但这个我们简化了,不去数据库查了,构造了一个优惠券给他返回
    CouponEntity couponEntity = new CouponEntity();
    couponEntity.setCouponName("满100-10");//优惠券的名字
    return R.ok().put("coupons",Arrays.asList(couponEntity));
}

image-20220212231408850

(2)开启远程调用功能 @EnableFeignClients,要指定远程调用功能放的基础包

@EnableFeignClients(basePackages="com.peng.gulimall.member.feign")//扫描接口方法注解

image-20220212231644234

(3)调用远程服务的接口

package com.peng.gulimall.member.feign;

import com.peng.common.utils.R;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
// 告诉spring cloud这个接口是一个远程客户端,要调用coupon服务(nacos中找到),
// 具体是调用coupon服务的/coupon/coupon/member/list对应的方法
@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    // 远程服务的url
    // 注意写全优惠券类上还有映射
    // 注意我们这个地方不是控制层,所以这个请求映射请求的不是我们服务器上的东西,而是nacos注册中心的
    // 得到一个R对象
    @RequestMapping("/coupon/coupon/member/list")
    public R membercoupons();
}

image-20220212231828720

(4)在gulimall-member的MemberController写一个测试接口

@Autowired
CouponFeignService couponFeignService;

@RequestMapping("/coupons")
public R test(){
    MemberEntity memberEntity = new MemberEntity();
    memberEntity.setNickname("会员昵称张三");
    R membercoupons = couponFeignService.membercoupons();//假设张三去数据库查了后返回了张三的优惠券信息

    //打印会员和优惠券信息
    return R.ok().put("member",memberEntity).put("coupons",membercoupons.get("coupons"));
}

image-20220212232045341

(5)访问:http://localhost:8005/member/member/coupons

image-20220212235117548

出现loadbalancer相关的错误

在nacos"包中移除ribbion依赖,并加入loadbalancer依赖"

image-20220212234532098

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    <!--不使用Ribbon 进行客户端负载均衡-->
    <exclusions>
        <exclusion>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </exclusion>
    </exclusions>
</dependency>

在需要远程调用的服务加上loadbalancer

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

image-20220212235039315

不识别bootstrap.properties的错误

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

3.4 Spring Cloud alibaba Naloas 配置中心

地址:https://github.com/alibaba/spring-cloud-alibaba/blob/2.2.x/spring-cloud-alibaba-examples/nacos-example/nacos-config-example/readme-zh.md

(1)导入依赖

<!--nacos配置中心-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

image-20220212235406430
(2)增加bootstrap.properties配置,这个文件是springboot里规定的,他优先级别application.properties高

# 改名字,对应nacos里的配置文件名
spring.application.name=gulimall-coupon
spring.cloud.nacos.config.server-addr=127.0.0.1:8848

image-20220213000006368

(3)增加application.properties配置

coupon.user.name = application.properties
coupon.user.age = 18

image-20220213000042792

(4)CouponController增加测试代码

//从application.properties中获取//不要写user.name,他是环境里的变量
@Value("${coupon.user.name}")
private String name;
@Value("${coupon.user.age}")
private Integer age;
@RequestMapping("/test")
public R test(){

    return R.ok().put("name",name).put("age",age);
}

image-20220213000154849

(5)测试:http://localhost:7005/coupon/coupon/test

这里就可以访问本地application.properties的配置文件

image-20220213000241323

(6)nacos增加配置并发布

image-20220213000435745

3.5 Spring Cloud alibaba Nacos 配置中心-命名空间与配置分组

3.5.1 命名空间

nacos新增命名空间

image-20220213141406088

prop配置

coupon.user.name = nacos.prop
coupon.user.age = 30

image-20220213141430932

image-20220213141253434

# 可以选择对应的命名空间 # 写上对应环境的命名空间ID
spring.cloud.nacos.config.namespace=d408db52-7aa9-4348-81b4-cc5d39f91c55

image-20220213141122995

测试访问:http://localhost:7005/coupon/coupon/test

image-20220213141201571

  • 命名空间:用作配置隔离。(一般每个微服务一个命名空间)

    • 默认public。默认新增的配置都在public空间下

    • 开发、测试、开发可以用命名空间分割。properties每个空间有一份。也可以为每个微服务配置一个命名空间,微服务互相隔离

    • 在bootstrap.properties里配置

      spring.cloud.nacos.config.namespace=d408db52-7aa9-4348-81b4-cc5d39f91c55
      

      image-20220213140628230

  • 配置集:一组相关或不相关配置项的集合。

  • 配置集ID:类似于配置文件名,即Data ID

  • 配置分组:默认所有的配置集都属于DEFAULT_GROUP。双十一,618的优惠策略改分组即可

    spring.cloud.nacos.config.group=DEFAULT_GROUP
    

3.5.2 配置分组

(1)为coupon新增一个命名空间

image-20220213141658701

(2)gulimall-coupon只读取自己的配置(基于微服务隔离)

image-20220213142124267

(3)可以设置不同的分组

coupon.user.name = coupon.11

coupon.user.age = 60

image-20220213142734839

image-20220213142628274

(4)测试:http://localhost:7005/coupon/coupon/test

image-20220213142706963

(5)新增dev(开发环境)和prod(生产环境)分组,默认为dev分组

dev配置:

coupon.user.name = coupon.dev
coupon.user.age = 50

prod配置:

coupon.user.name = coupon.prod
coupon.user.age = 70

image-20220213143207865

测试访问:http://localhost:7005/coupon/coupon/test

image-20220213143406444

3.6 Spring Cloud alibaba Nacos 配置中心-加载多配制集

(1)注释application.yml

image-20220213150728605

(2)nacos新增配置

datasource.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_sms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver
  profiles:
    active: dev

mybatis.yml

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

other.yml

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

server:
  port: 7005

image-20220213150320322

(3)修改bootstrap.properties

# 改名字,对应nacos里的配置文件名

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

# 可以选择对应的命名空间 # 写上对应环境的命名空间ID
spring.cloud.nacos.config.namespace=8531791b-f632-4588-a406-090f42c04d48

# 分组 默认DEFAULT_GROUP
spring.cloud.nacos.config.group=dev

spring.cloud.nacos.config.extension-configs[0].data-id=datasource.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=mybatis.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

image-20220213150507873

(4)测试运行,nacos配置读取正常

访问:http://localhost:7005/coupon/coupon/test

image-20220213150624589

访问:http://localhost:7005/coupon/coupon/list

image-20220213150644134

3.7 Spring Cloud Gateway

地址:https://spring.io/projects/spring-cloud-gateway

三大核心概念:

  • Route(路由):
  • Predicate(断言):
  • Filter(过滤器):

(1)spring向导创建网关,选择网关

image-20220213152405548

image-20220213152539303

(2)nacos增加gateway配置

增加gateway命名空间

image-20220213155346010

gulimall-gateway.properties

spring:
    application:
        name: gulimall-gateway

image-20220213155415901

(3)bootstrap.properties配置

spring.application.name=gulimall-gateway
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=debd0e0d-de9a-48c8-b7c1-dbed3b775402

image-20220213155518505

(4)application.properties配置

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

image-20220213155631774

(5)application.yml路由配置

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

        - id: qq_route
          uri: https://www.qq.com
          predicates:
            - Query=url,qq

image-20220213155716208

(6)主程序加上@EnableDiscoveryClient,@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})

如果不加exclude= {DataSourceAutoConfiguration.class,排除datasource相关配置,会报错Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured,这里暂时用不上datasource的一些相关属性

在gateway服务中开启注册服务发现@EnableDiscoveryClient,配置nacos注册中心地址applicaion.properties。这样gateway也注册到了nacos中,其他服务就能找到nacos,网关也能通过nacos找到其他服务

@SpringBootApplication(exclude= {DataSourceAutoConfiguration.class})
@EnableDiscoveryClient

image-20220213155747723

4.前端

VBoxManage.exe internalcommands sethduuid “D:\VirtualBox VMs\Peng_default_1644565405392_61498\Peng_default_1644565405392_61498.vbox”

4.1 ES6

4.1 let和var

let不会作用到{}外,var会越域跳到{}外

 {
     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定义之前不可使用。(变量提升问题)

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

const声明之后不允许改变

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

4.2 解构表达式

数组解构

//数组解构
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;

//对象解构 // 把name属性变为abc,声明了abc、age、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 = `<div>
            <span>hello world<span>
          </div>`;
console.log(ss);
        
function fun() {
    return "这是一个函数"
}

字符串插入变量和表达式

// 2、字符串插入变量和表达式。变量名写在 ${} 中,${} 中可以放入 JavaScript 表达式。
let info = `我是${abc},今年${age + 10}了, 我想说: ${fun()}`;
console.log(info);

4.3 函数优化

//在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)、箭头函数。lambda
        //以前声明一个方法
        // 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); 

4.4 对象优化

        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);//{a: 1, b: 2, c: 3}

        //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)

4.5 map和reduce

        //数组中新增了map和reduce方法。
        let arr = ['1', '20', '-5', '3'];
         
         //map():接收一个函数,将原数组中的所有元素用这个函数处理后放入新数组返回。
         //  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)

4.6 promise

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</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": 10,
    "name": "chinese"
}

user_corse_1.json 课程

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

4.7 import和export

  • 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变量都可以导出。比如:基本类型变量、函数、数组、对象。

7.import和export.js

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

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

4.2 Vue基本语法

MVVM思想

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

v-bind 缩写

<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>

v-on 缩写

<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>

官网:https://cn.vuejs.org/index.html

npm init -y
npm install vue
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="num" />
      v-model实现双向绑定。此处代表输入框和vue里的data绑定
      <button v-on:click="num++">点赞</button>
      v-on:click绑定事件,实现自增。
      <button v-on:click="cancel">取消</button>
      回调自定义的方法。 此时字符串里代表的函数
      <h3>{{name}} ,你好 ,有{{num}}个人为他点赞</h3>
      先从vue中拿到值填充到dom,input再改变num值,vue实例更新,然后此处也更新
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <!-- <script src="./node_modules/vue/dist/vue.js"></script> -->

    <script>
      //1、vue声明式渲染
      let vm = new Vue({
        el: "#app",
        data: {
          name: "彭小帅",
          num: 1,
        },
        methods: {
          cancel(){
            this.num--;
          },
          hello(){
            return "1";
          }
        },
      });
    </script>
  </body>
</html>

4.2.1 vue-devtools插件安装

(1)下载地址:https://github.com/vuejs/devtools

image-20220213212012729

(2)下载后解压,cmd到解压目录,执行npm install

如果在E盘,cmd输入e:,让后cd到路径

或者直接在devtools-main解压目录输入cmd,回车

image-20220213215834234

image-20220213214856901

(3)执行npm run build,新版本使用npm run build报错

image-20220213215303683

(4)安装yarn

npm install -g yarn

image-20220213215427890

(5)使用前更换淘宝镜像

yarn config set registry https://registry.npm.taobao.org --global
yarn config set disturl https://npm.taobao.org/dist --global
yarn config get registry (如果上面配置正确这个命令会输出淘宝镜像地址)

(6)配置插件

yarn install

image-20220213221802320

中间失败了一次,又执行了一次就成功了

image-20220213221854102

yarn run build

4.2.2 v-text、v-html、v-ref

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
        <p>前面的内容如果网速慢的话会先显示括号,然后才替换成数据。
            v-html 和v-text能解决这个问题</p>
        {{msg}}  {{1+1}}  {{hello()}} 

        <p>用v-html取内容</p>
        <p><span v-html="msg"></span></p>
        
        <p>用v-text取内容</p>
        <p><span v-text="msg"></span></p>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>

      let vm = new Vue({
        el:"#app",
            data:{
                msg:"<h1>Hello</h1>",
                link:"http://www.baidu.com"
            },
            methods:{
                hello(){
                    return "World"
                }
            }
      });
    </script>
  </body>
</html>

4.2.3 v-bind

v-bind:,简写为:。表示把model绑定到view。可以设置src、title、class等

class 与 style 是 HTML 元素的属性,用于设置元素的样式,我们可以用 v-bind 来设置样式属性。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <a v-bind:href="link">跳转</a>
       <!-- class,style  {class名:vue值}-->
       <span v-bind:class="{active:isActive,'text-danger':hasError}"
       :style="{color: color1,fontSize: size}">你好</span>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      let vm = new Vue({
        el: "#app",
        data: {
          link: "http://www.baidu.com",
          isActive: true,
          hasError: true,
          color1: "red",
          size: "36px",
        },
        methods: {},
      });
    </script>
  </body>
</html>

4.2.4 v-model

v-model双向绑定,v-bind只能从model到view。v-model能从view到mode

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      精通的语言:
      <input type="checkbox" v-model="language" value="Java" /> Java<br />
      <input type="checkbox" v-model="language" value="PHP" /> PHP<br />
      <input type="checkbox" v-model="language" value="Python" /> Python<br />
      选中了 {{language.join(",")}}
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      let vm = new Vue({
        el: "#app",
        data: {
            language:[]
        }     
      });
    </script>
  </body>
</html>

4.2.5 v-on

事件监听可以使用 v-on 指令

Vue.js 为 v-on 提供了事件修饰符来处理 DOM 事件细节,如:event.preventDefault() 或 event.stopPropagation()。

Vue.js 通过由点 . 表示的指令后缀来调用修饰符。

  • .stop - 阻止冒泡
  • .prevent - 阻止默认事件
  • .capture - 阻止捕获
  • .self - 只监听触发该元素的事件
  • .once - 只触发一次
  • .left - 左键事件
  • .right - 右键事件
  • .middle - 中间滚轮事件
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <!--事件中直接写js片段-->
      <button v-on:click="num++">点赞</button>
      <!--事件指定一个回调函数,必须是Vue实例中定义的函数-->
      <button @click="cancel">取消</button>
      <!--  -->
      <h1>有{{num}}个赞</h1>

      <!-- 事件修饰符 -->
      <div style="border: 1px solid red; padding: 20px" v-on:click.once="hello">
        大div
        <div style="border: 1px solid blue; padding: 20px" @click.stop="hello">
          小div <br />
          <a href="http://www.baidu.com" @click.prevent.stop="hello">去百度</a>
        </div>
      </div>

      <!-- 按键修饰符: -->
      <input
        type="text"
        v-model="num"
        v-on:keyup.up="num+=2"
        @keyup.down="num-=2"
        @click.ctrl="num=10"
      /><br />
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      let vm = new Vue({
        el:"#app",
            data:{
                num: 1
            },
            methods:{
                cancel(){
                    this.num--;
                },
                hello(){
                    alert("点击了")
                }
            }
      });
    </script>
  </body>
</html>

4.2.6 v-for

可以遍历 数组[] 字典{} 。对于字典 v-for="(value, key, index) in object

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
        <ul>
            <!-- 4、遍历的时候都加上:key来区分不同数据,提高vue渲染效率 -->
            <li v-for="(user,index) in users" :key="user.name" v-if="user.gender == '女'">
                <!-- 1、显示user信息:v-for="item in items" -->
               当前索引:{{index}} ==> {{user.name}}  ==>   
                  {{user.gender}} ==>{{user.age}} <br>
                <!-- 2、获取数组下标:v-for="(item,index) in items" -->
                <!-- 3、遍历对象:
                        v-for="value in object"
                        v-for="(value,key) in object"
                        v-for="(value,key,index) in object" 
                -->
                对象信息:
                <span v-for="(v,k,i) in user">{{k}}=={{v}}=={{i}};</span>
                <!-- 4、遍历的时候都加上:key来区分不同数据,提高vue渲染效率 -->
            </li>

            
        </ul>

        <ul>
            <li v-for="(num,index) in nums" :key="index"></li>
        </ul>

    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      let app = new Vue({
            el: "#app",
            data: {
                users: [
                { name: '柳岩', gender: '女', age: 21 },
                { name: '张三', gender: '男', age: 18 },
                { name: '范冰冰', gender: '女', age: 24 },
                { name: '刘亦菲', gender: '女', age: 18 },
                { name: '古力娜扎', gender: '女', age: 25 }
                ],
                nums: [1,2,3,4,4]
            },
        })
   </script>
  </body>
</html>

4.2.7 v-if和v-show

v-if元素会被引出,v-show只是隐藏

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 
        v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。
        v-show,当得到结果为true时,所在的元素才会被显示。 
    -->
    <div id="app">
        <button v-on:click="show = !show">点我呀</button>
        <!-- 1、使用v-if显示 -->
        <h1 v-if="show">if=看到我....</h1>
        <!-- 2、使用v-show显示 -->
        <h1 v-show="show">show=看到我</h1>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
        
    <script>
        let app = new Vue({
            el: "#app",
            data: {
                show: true
            }
        })
    </script>

  </body>
</html>

4.2.8 v-else和v-else-if

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 
        v-if,顾名思义,条件判断。当得到结果为true时,所在的元素才会被渲染。
        v-show,当得到结果为true时,所在的元素才会被显示。 
    -->
    <div id="app">
        <button v-on:click="show = !show">点我呀</button>
        <!-- 1、使用v-if显示 -->
        <h1 v-if="show">if=看到我....</h1>
        <!-- 2、使用v-show显示 -->
        <h1 v-show="show">show=看到我</h1>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
        
    <script>
        let app = new Vue({
            el: "#app",
            data: {
                show: true
            }
        })
    </script>

  </body>
</html>

4.3 Vue计算属性和侦听器

4.3.1 计算属性computed

什么是计算属性:属性不是具体值,而是通过一个函数计算出来的,随时变化

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p>原始字符串: {{ message }}</p>
      <p>计算后反转字符串: {{ reversedMessage }}</p>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          message: "Runoob!",
        },
        computed: {
          // 计算属性的 getter
          reversedMessage: function () {
            // `this` 指向 vm 实例
            return this.message.split("").reverse().join("");
          },
        },
      });
    </script>
  </body>
</html>

4.3.2 监听watch

监听属性 watch,我们可以通过 watch 来响应数据的变化。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p style="font-size: 25px">计数器: {{ counter }}</p>
      <button @click="counter++" style="font-size: 25px">点我</button>
      <br />

      <!-- 某些结果是基于之前数据实时计算出来的,我们可以利用计算属性。来完成 -->
      <ul>
        <li>
          西游记; 价格:{{xyjPrice}},数量:<input
            type="number"
            v-model="xyjNum"
          />
        </li>
        <li>
          水浒传; 价格:{{shzPrice}},数量:<input
            type="number"
            v-model="shzNum"
          />
        </li>
        <li>总价:{{totalPrice}}</li>
        {{msg}}
      </ul>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          counter: 1,
          xyjPrice: 99.98,
          shzPrice: 98.0,
          xyjNum: 1,
          shzNum: 1,
          msg: "",
        },
        computed: {
          totalPrice() {
            return this.xyjPrice * this.xyjNum + this.shzPrice * this.shzNum;
          },
        },
        watch: {
          xyjNum: function (newVal, oldVal) {
            if (newVal >= 3) {
              this.msg = "库存超出限制";
              this.xyjNum = 3;
            } else {
              this.msg = "";
            }
          },
        },
      });
      vm.$watch("counter", function (nval, oval) {
        // new old
        alert("计数器值的变化 :" + oval + " 变为 " + nval + "!");
      });
    </script>
  </body>
</html>

4.4过滤器filter

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
       <!-- 过滤器常用来处理文本格式化的操作。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 -->
    <div id="app">
        <ul>
            <li v-for="user in userList">
                {{user.id}} ==> {{user.name}} ==> {{user.gender == 1?"男":"女"}} ==>
                {{user.gender | genderFilter}} ==> {{user.gender | gFilter}}
            </li>
        </ul>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
        
      // 全局过滤器
      Vue.filter("gFilter", function (val) {
            if (val == 1) {
                return "男~~~";
            } else {
                return "女~~~";
            }
        })

        let vm = new Vue({
            el: "#app",
            data: {
                userList: [
                    { id: 1, name: 'jacky', gender: 1 },
                    { id: 2, name: 'peter', gender: 0 }
                ]
            },
            filters: { // 局部过滤器,只可以在当前vue实例中使用
                genderFilter(val) {
                    if (val == 1) {
                        return "男";
                    } else {
                        return "女";
                    }
                }
            }
        })

    </script>
  </body>
</html>

image-20220213235041764

4.5 组件化

  • 组件其实也是一个vue实例,因此它在定义时也会接收:data、methods、生命周期函数等
  • 不同的是组件不会与页面的元素绑定(所以不写el),否则就无法复用了,因此没有el属性。
  • 但是组件渲染需要html模板,所以增加了template属性,值就是HTML模板
  • data必须是一个函数,不再是一个对象。
  • 全局组件定义完毕,任何vue实例都可以直接在HTML中通过组件名称来使用组件了
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <button v-on:click="count++">我被点击了 {{count}} 次</button>

      每个对象都是独立统计的
      <counter></counter>
      <counter></counter>
      <counter></counter>
      <counter></counter>
      <counter></counter>

      <button-counter></button-counter>
    </div>

    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

    <script>
      //1、全局声明注册一个组件 // counter标签,代表button
      // 把页面中<counter>标签替换为指定的template,而template中的数据用data填充
      Vue.component("counter", {
        template: `<button v-on:click="count++">我被点击了 {{count}} 次</button>`,
        data() {
          // 如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到其它所有实例:
          return {
            count: 1, // 数据
          };
        },
      });

      //2、局部声明一个组件
      const buttonCounter = {
        template: `<button v-on:click="count++">我被点击了 {{count}} 次~~~</button>`,
        data() {
          return {
            count: 1,
          };
        },
      };

      let vm = new Vue({
        el: "#app",
        data: {
          count: 1,
        },
        methods: {},
      });
    </script>
  </body>
</html>

4.6 生命周期钩子函数

每个vue实例在被创建时都要经过一系列的初始化过程:创建实例,装载模板、渲染模板等等。vue为生命周期中的每个状态都设置了钩子函数(监听函)。每当vue实列处于不同的生命周期时,对应的函数就会被触发调用。

  • beforeCreate

  • created

  • beforeMount

  • mounted

  • beforeUpdate

  • updated

  • beforeDestroy

  • destroyed

<!DOCTYPE html>
<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>Document</title>
</head>

<body>
    <div id="app">
        <span id="num">{{num}}</span>
        <button @click="num++">赞!</button>
        <h2>{{name}},有{{num}}个人点赞</h2>
    </div>

    <script src="../node_modules/vue/dist/vue.js"></script>
    
    <script>
        let app = new Vue({
            el: "#app",
            data: {
                name: "张三",
                num: 100
            },
            methods: {
                show() {
                    return this.name;
                },
                add() {
                    this.num++;
                }
            },
            beforeCreate() {
                console.log("=========beforeCreate=============");
                console.log("数据模型未加载:" + this.name, this.num);
                console.log("方法未加载:" + this.show());
                console.log("html模板未加载:" + document.getElementById("num"));
            },
            created: function () {
                console.log("=========created=============");
                console.log("数据模型已加载:" + this.name, this.num);
                console.log("方法已加载:" + this.show());
                console.log("html模板已加载:" + document.getElementById("num"));
                console.log("html模板未渲染:" + document.getElementById("num").innerText);
            },
            beforeMount() {
                console.log("=========beforeMount=============");
                console.log("html模板未渲染:" + document.getElementById("num").innerText);
            },
            mounted() {
                console.log("=========mounted=============");
                console.log("html模板已渲染:" + document.getElementById("num").innerText);
            },
            beforeUpdate() {
                console.log("=========beforeUpdate=============");
                console.log("数据模型已更新:" + this.num);
                console.log("html模板未更新:" + document.getElementById("num").innerText);
            },
            updated() {
                console.log("=========updated=============");
                console.log("数据模型已更新:" + this.num);
                console.log("html模板已更新:" + document.getElementById("num").innerText);
            }
        });
    </script>
</body>

</html>

4.7 使用Vue脚手架快速开发

(1)全局安装webpack

npm install webpack -g

image-20220214003415738

(2)全局安装vue脚手架

npm install -g @vue/cli-init

image-20220214003538547

(3)初始化Vue项目

vue init webpack 项目名称

image-20220214003751232

(4)启动vue项目

npm run start

image-20220214004002384

4.8整合Element-UI

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

(1)安装 element-ui

 npm i element-ui

image-20220214005153669

(2)引入Element-UI

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

image-20220214005423073

(3)把elementUi的container布局容器代码拷到App.vue中

image-20220214010329098

(4)修改App.vue中的 ,动态获取视图

image-20220214011921297

(5)路由配置

export default new Router({
  routes: [
    {
      path: '/',
      name: 'HelloWorld',
      component: HelloWorld
    },
    {
      path: '/hello',
      name: 'Hello',
      component: Hello
    },
    {
      path: '/table',
      name: 'MyTable',
      component: MyTable
    }
  ]
})

image-20220214012106757

(6)el-menu设置router,并添加跳转路由

image-20220214012026031

5. 商品服务-三级分类

5.1 递归实现商品服务分类

CategoryController:

    /*
    * 查出所有分类 以及子分类,以树形结构组装起来
    * */
    @RequestMapping("/list/tree")
    public R listtree() {
        List<CategoryEntity> entityList = categoryService.listWithTree();
        return R.ok().put("data", entityList);
    }

CategoryServiceImpl:

@Override
public List<CategoryEntity> listWithTree() {

    //1、查出所有分类
    List<CategoryEntity> categoryEntities = baseMapper.selectList(null);

    // 2 组装成父子的树型结构
    // 2.1 找到所有一级分类
    List<CategoryEntity> level1Menus = categoryEntities.stream().filter((categoryEntity -> {
        return categoryEntity.getParentCid() == 0;
    })).map(menu -> {
        menu.setChildren(getChildren(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;
}

/*
 * 获取某一个菜单的子菜单
 * 在all里找root的子菜单
 * */

private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
    List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
        return categoryEntity.getParentCid() == root.getCatId();
    }).map(categoryEntity -> {
        categoryEntity.setChildren(getChildren(categoryEntity, all));
        return categoryEntity;
    }).sorted((menu1, menu2) -> {
        return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
    }).collect(Collectors.toList());
    return children;
}

访问:http://localhost:11000/product/category/list/tree

image-20220214160011207

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

(1)运行人人开源程序

image-20220214160511157

(2)增加商品系统菜单

image-20220214160710002

给商品系统增加商品维护子菜单

菜单路由对应前端工程的路径

image-20220214160900531

结果:

image-20220214161044924

(3)在renren-fast-vue增加商品维护的代码,路径匹配配置的菜单路由

image-20220214161328682

(4)前端修改网关服务器的地址

image-20220214162203675

(5)将renren-fast注册到nacos中心

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

image-20220214162510778

开启服务的注册发现

@EnableDiscoveryClient

image-20220214162614475

引入:gulimall-common

<dependency>
<groupId>com.peng.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
   </dependency>

(6)配置路由

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

导入spring-cloud-starter-loadbalancer依赖

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

image-20220214172348011

(7)重新运行,启动gulimall-gateway和renren-fast工程:

image-20220214164516579

(8)查看获取验证码接口,通过网关正常访问

image-20220214172459200

5.3 网关统一配置跨域

(1)gulimall-gateway:使用UrlBasedCorsConfigurationSource和CorsConfiguration

@Configuration // gateway
public class GulimallCorsConfiguration {

    @Bean // 添加过滤器
    public CorsWebFilter corsWebFilter(){
        // 基于url跨域,选择reactive包下的
        UrlBasedCorsConfigurationSource source=new UrlBasedCorsConfigurationSource();
        // 跨域配置信息
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 允许跨域的头
        corsConfiguration.addAllowedHeader("*");
        // 允许跨域的请求方式
        corsConfiguration.addAllowedMethod("*");
        // 允许跨域的请求来源
        corsConfiguration.addAllowedOrigin("*");
        // 是否允许携带cookie跨域
        corsConfiguration.setAllowCredentials(true);

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

image-20220214173327335

(2)注释renren-fast配置的跨域配置

image-20220214173557046

(3)跨域配置完成,登录成功

image-20220214173804961

5.4 树形展示三级分类数据

(1)使用nacos远程配置

新建product命名空间

image-20220214181401616

新建product配置,配置按服务隔离

datasource.yml

spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.56.10:3306/gulimall_pms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis.yml

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

other.yml

server:
  port: 11005

效果图

image-20220214181450891

gulimall-product主程序添加@EnableDiscoveryClient

image-20220214182210468

(3)启动运行,确保gulimall-product,gulimall-gateway,renren-fast注册到nacos中

image-20220214182711314

(4)gulimall-product 配置网关路由

admin_route和product_route都是路径匹配,需要将product_route放前面负责可能会匹配到admin_route

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

image-20220214182331250

(5)前端调用接口

5.5 三级分类删除页面效果

前端代码不过多展示,主要是去官方文档查看

<el-tree
      :data="menus"
      :props="defaultProps"
      @node-click="handleNodeClick"
      :expand-on-click-node="false"
      show-checkbox
      node-key="catId"
      :default-expanded-keys="expandedKey"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            type="text"
            size="mini"
            @click="() => append(data)"
            >Append</el-button
          >
          <el-button type="text" size="mini" @click="edit(data)"
            >edit</el-button
          >
          <el-button
            v-if="node.childNodes.length == 0"
            type="text"
            size="mini"
            @click="() => remove(node, data)"
            >Delete</el-button
          >
        </span>
      </span>
    </el-tree>

5.6 逻辑删除

(1)MyBatis配置逻辑删除

我这里是用的nacos远程配置

image-20220214220630272

logic-delete-value: 1代表1是删除

logic-not-delete-value: 0代表0是未删除

mybatis-plus:
  mapper-locations: classpath:/mapper/**/*.xml
  global-config:
    db-config:
      id-type: auto
      logic-delete-value: 1
      logic-not-delete-value: 0

image-20220214220714212

(2)实体类的逻辑删除字段加上TableLogic,这里@TableLogic(value="1")代表1是未删除,0是已删除,因为数据库跟全局的配置可能是反的,所以可以单独配置

image-20220214220941214

错误:com.google.common

com.google.common.collect.Sets$SetView.iterator()Lcom/google/common/collect/UnmodifiableIterator;

导入:

<dependency>
   <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>20.0</version>
   </dependency>

错误:配置完路由后,前端访问503

需要导入依赖:

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

6.商品服务-品牌管理

6.1使用逆向工程前后端代码

(1)增加品牌管理菜单

image-20220215202438654

(2)逆向生成的前端代码拷贝到vue工程product目录下

image-20220215202746691

(3)取消Eslint语法检查

image-20220215195646152

(4)取消权限检查

image-20220215204605575

(4)重新启动前端工程

6.2 优化快速显示开关

前端代码:

加上el-switch开关,并在@change方法中发送请求

<el-table-column prop="showStatus" header-align="center" align="center" label="显示状态">
        <template slot-scope="scope">
          <el-switch
            v-model="scope.row.showStatus"
            active-color="#13ce66"
            inactive-color="#ff4949"
            :active-value="1"
            :inactive-value="0"
            @change="updateBrandStatus(scope.row)"
          ></el-switch>
        </template>
 </el-table-column>

updateBrandStatus:

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:showStatus?1:0 }, false)
      }).then(({ data }) => {
        this.$message({
          type: "success",
          message: "状态更新成功"
        });
      });
},

java后端代码(自动生产的):

/**
 * 修改
 */
@RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
    categoryService.updateById(category);
    return R.ok();
}

6.3 阿里云对象存储开通与使用

(1)打开自己的阿里云工作台

image-20220215205228972

(2)创建Bucket

image-20220215205613901

(3)文件管理上传图片

image-20220215210237165

(4)点击详情,复制路径

image-20220215210339185

(5)浏览器打开时候能正常下载

6.4 阿里云对象存储-OSS测试

6.4.1 阿里云配置

地址:https://help.aliyun.com/document_detail/32013.html

(1)添加AccessKey

image-20220215214658550

(2)使用子用户

image-20220215214930967

(3)创建访问用户

image-20220215214741375

(4)验证后获取AccessKey ID和AccessKey Secret

image-20220215214834349

(5)账号分配权限

image-20220215215603451

6.4.2 java代码测试

(1)导入阿里云依赖

<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.10.2</version>
</dependency>

(2)EndPoint地域节点

oss-cn-beijing.aliyuncs.com

image-20220215215746871

(3)AccessKey ID和AccessKey Secret

image-20220215220746382

(4)Bucket

image-20220215220947464

(2)测试代码

@Test
public  void testUpload(){
    // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。
    //oss-cn-beijing.aliyuncs.com
    String endpoint = "https://oss-cn-beijing.aliyuncs.com";
    // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。
    String accessKeyId = "LTAI5t6WczYReBvbRjY1cscr";
        String accessKeySecret = "ROidZCyNL0mMexts35tDxLI8VYZbMF";
    // 填写Bucket名称,例如examplebucket。
    String bucketName = "peng-aliyun";
    // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。
    //上传后的名字
    String objectName = "1.jpg";
    // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。
    // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。
    String filePath= "C:\\Users\\Peng\\Desktop\\temporary\\testpic\\1.jpg";
    // 创建OSSClient实例。
    OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

    try {
        InputStream inputStream = new FileInputStream(filePath);
        // 创建PutObject请求。
        ossClient.putObject(bucketName, objectName, inputStream);
    } catch (OSSException oe) {
        System.out.println("Caught an OSSException, which means your request made it to OSS, "
                + "but was rejected with an error response for some reason.");
        System.out.println("Error Message:" + oe.getErrorMessage());
        System.out.println("Error Code:" + oe.getErrorCode());
        System.out.println("Request ID:" + oe.getRequestId());
        System.out.println("Host ID:" + oe.getHostId());
    } catch (ClientException ce) {
        System.out.println("Caught an ClientException, which means the client encountered "
                + "a serious internal problem while trying to communicate with OSS, "
                + "such as not being able to access the network.");
        System.out.println("Error Message:" + ce.getMessage());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ossClient != null) {
            ossClient.shutdown();
        }
    }
    System.out.printf("上传完成");
}

6.4.3 Aliyun Spring Boot OSS 上传

地址:https://github.com/alibaba/aliyun-spring-boot/tree/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample

(1)gulimall-common导入依赖,

只导入aliyun-oss-spring-boot-starter,版本过高程序运行不起来,需要导入aliyun-java-sdk-core

aliyun-spring-boot-dependencies需要aliyun-oss-spring-boot-starter一起导入,不然会报错

        <!--阿里云存储-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>aliyun-oss-spring-boot-starter</artifactId>
        </dependency>
        <!--导入新的依赖-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
        </dependency>
        
        
        <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>aliyun-spring-boot-dependencies</artifactId>
                <version>1.0.0</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

(2)阿里云存储配置

alibaba:
  cloud:
    access-key: LTAI5t6WczYReBvbRjY1cscr
    secret-key: ROidZCyNL0mMexts35tDxLI8VYZbMF
    oss:
      endpoint: https://oss-cn-beijing.aliyuncs.com

image-20220215232743833

(3)测试代码:

    @Autowired
    OSSClient ossClient;
    @Test
    public  void testAliyunUpload(){
        //下载
        //ossClient.getObject(new GetObjectRequest("peng-aliyun", "2,.jpg"), new File("C:\\Users\\Peng\\Desktop\\temporary\\testpic\\2.jpg"));
        //上传
        ossClient.putObject("peng-aliyun", "1.jpg", new File("C:\\Users\\Peng\\Desktop\\temporary\\testpic\\1.jpg"));
        ossClient.shutdown();
    }

image-20220215232828105

6.5 阿里云对象存储-服务端签名后直传

(1)创建第三方spring服务模块

image-20220215233321017

(2)选中spring Web和OpenFeign

image-20220215233355627

(3)将gulimall-common阿里云存储相关服务导入到gulimall-third-party

image-20220215233759603

(4)把gulimall-third-party注册到nacos服务中

配置nacos相关配置

spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=b719fb01-6656-4b1f-872e-b6ddbf170214

## 分组 默认DEFAULT_GROUP
spring.cloud.nacos.config.group=dev

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

image-20220215235001563

增加gulimall-third-party命名空间和oss.yml,命名空间为dev

image-20220215234524522

oss.yml配置文件

alibaba:
  cloud:
    access-key: LTAI5t6WczYReBvbRjY1cscr
    secret-key: ROidZCyNL0mMexts35tDxLI8VYZbMF
    oss:
      endpoint: https://oss-cn-beijing.aliyuncs.com

(5)增加application.yml

spring:
  cloud:
    acos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: gulimall-third-party

server:
  port: 30000

image-20220215235725753

(6)引入com.peng.gulimall排除掉mybatis,暂时用不上

<dependency>
    <groupId>com.peng.gulimall</groupId>
    <artifactId>gulimall-common</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <exclusions>
        <exclusion>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </exclusion>
    </exclusions>
</dependency>

(7)开启服务发现

image-20220215235801725

(8)新增other.yml作为其他配置

server:
  port: 30000

other.yml

image-20220216002232050

(9)服务端签名后直传文档

地址:https://help.aliyun.com/document_detail/31926.html

image-20220216003903836

https://help.aliyun.com/document_detail/91868.htm?spm=a2c4g.11186623.0.0.16073967YFZamz#concept-ahk-rfz-2fb

image-20220216004038234

(10)OssController

package com.peng.gulimall.thirdparty.controller;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
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 ("${alibaba.cloud.oss.endpoint}")
    String endpoint ;

    @Value("${alibaba.cloud.oss.bucket}")
    String bucket ;

    @Value("${alibaba.cloud.access-key}")
    String accessId ;
    @Value("${alibaba.cloud..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;
    }

}

(11)配置路由

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

(12)访问:http://localhost:88/api/thirdparty/oss/policy

image-20220216013018287

6.6 OSS前后端联调

(1)跨域配置

  • 登录OSS管理控制台
  • 单击Bucket列表,之后单击目标Bucket名称。
  • 单击*权限管理* > *跨域设置*,在跨域设置区域单击设置**。

image-20220216014623835

(2)前端配置Bucket域名

image-20220216015748916

(3)前端引入并使用上传组件

image-20220216020345769

(4)查看阿里云,目录和文件都成功创建

image-20220216020441284

6.7 Vue表单校验&自定义校验

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"
          }
        ]
      }

6.8 JSR303检验

6.8.1 JSR303数据校验

(1)导入依赖

<!--jsr3参数校验器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

(2)使用注解进行校验

  • @NotNull 该属性不能为null

  • @NotEmpty 该字段不能为null或""

  • @NotBlank 不能为空,不能仅为一个空格

(3)@Valid

这里内置异常的意思是发生异常时返回的json不是我们的R对象,而是mvc的内置类

  @RequestMapping("/save")
  //@RequiresPermissions("product:brand:save")
  public R save(@Valid @RequestBody BrandEntity brand){
brandService.save(brand);

      return R.ok();
  }

(4)自定义错误消息

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

(5)ValidationMessages_zh_CN.properties

image-20220216190258731

(6)Entity校验认证

package com.peng.gulimall.product.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;

import java.io.Serializable;
import java.util.Date;


import lombok.Data;
import org.hibernate.validator.constraints.URL;

import javax.validation.constraints.*;

/**
 * 品牌
 * 
 * @author peng
 * @email pengpeng6135@163.com
 * @date 2022-02-12 01:03:04
 */
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
	private static final long serialVersionUID = 1L;

	/**
	 * 品牌id
	 */
	@TableId
	private Long brandId;
	/**
	 * 品牌名
	 */
	@NotBlank(message = "品牌名必须非空")
	private String name;
	/**
	 * 品牌logo地址
	 */
	@NotEmpty()
	@URL(message = "logo必须是一个合法的url地址")
	private String logo;
	/**
	 * 介绍
	 */
	private String descript;
	/**
	 * 显示状态[0-不显示;1-显示]
	 */
	private Integer showStatus;
	/**
	 * 检索首字母
	 */
	@NotEmpty()
	@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母")
	private String firstLetter;
	/**
	 * 排序
	 */
	@NotEmpty()
	@Min(value = 0,message = "排序必须大于等于0")
	private Integer sort;

}

(7)局部异常处理BindingResult

    /**
     * 保存
     */
    @RequestMapping("/save")
    //@RequiresPermissions("product:brand:save")
    public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) {

        if (result.hasErrors()) {
            Map<String, String> map = new HashMap<>();
            //1、获取检验的错误结果
            result.getFieldErrors().forEach((item) -> {
                //FieldError 获得错误提示
                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();
    }

6.8.2 统一异常处理

(1)自定义错误码状态

统一在gulimall-common中,别的服务也要引用

package com.peng.common.exception;

import com.peng.common.constant.ProductConstant;

/***
 * 错误码和错误信息定义类
 * 1. 错误码定义规则为5为数字
 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
 * 错误码列表:
 *  10: 通用
 *      001:参数格式校验
 *  11: 商品
 *  12: 订单
 *  13: 购物车
 *  14: 物流
 */
public enum BizCodeEnum {
    UNKNOWN_EXCEPTION(10000, "系统未知异常"),
    VAILD_EXCEPTION(10001, "参数格式校验失败"),
    SMS_CODE_EXCEPTION(10002, "验证码获取频率太高,稍后再试"),
    TO_MANY_REQUEST(10003, "请求流量过大"),
    SMS_SEND_CODE_EXCEPTION(10403, "短信发送失败"),
    USER_EXIST_EXCEPTION(15001, "用户已经存在"),
    PHONE_EXIST_EXCEPTION(15002, "手机号已经存在"),
    LOGINACTT_PASSWORD_ERROR(15003, "账号或密码错误"),
    SOCIALUSER_LOGIN_ERROR(15004, "社交账号登录失败"),
    NOT_STOCK_EXCEPTION(21000, "商品库存不足"),
    PRODUCT_UP_EXCEPTION(11000,"商品上架异常");

    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;
    }
}

image-20220216191936139

(2)统一异常处理GulimallExceptionControllerAdvice

package com.peng.gulimall.product.exception;

import com.peng.common.exception.BizCodeEnum;
import com.peng.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.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@Slf4j
// @RestControllerAdvice和@ControllerAdvice的关系类似于@RestController
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")//管理的controller
public class GulimallExceptionControllerAdvice {

    @ExceptionHandler(value = Exception.class) // 也可以返回ModelAndView
    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);
    }

    @ExceptionHandler(value = Throwable.class)//异常的范围更大
    public R handleException(Throwable throwable) {
        log.error("未知异常{},异常类型{}",
                throwable.getMessage(),
                throwable.getClass());

        return R.error(BizCodeEnum.UNKNOWN_EXCEPTION.getCode(),
                BizCodeEnum.UNKNOWN_EXCEPTION.getMsg());
    }
}

image-20220216192058960

6.8.3JSR303分组检验

(1)新增三个验证接口

image-20220216193035945

(2)注解验证的时候加上分组

例如品牌id:新增的时候必须加上id,添加的时候不需要

image-20220216193143510

(3)controller方法必须使用Validated指定分组,负责不生效

image-20220216193438387

6.8.4 自定义校验注解

(1)创建ListValue,继承Constraint,并指定校验器

port java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * <p>Title: ListValue</p>
 * Description:JSR303自定义注解 必须有前三个方法
 * date:2020/6/1 23:25
 */
@Documented
// 指定校验器   这里可以指定多个不同的校验器
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
    // 1 使用该属性去Validation.properties中取
    String message() default "{com.atguigu.common.valid.ListValue.message}";

    // 2
    Class<?>[] groups() default { };

    // 3
    Class<? extends Payload>[] payload() default { };

    int[] vals() default { };
}

(2)ListValueConstraintValidator:获取传入的参数,并在isValid进行验证

package com.peng.common.valid;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;

public class ListValueConstraintValidator
        implements ConstraintValidator<ListValue,Integer> {//泛型参数<校验注解,标注字段类型>

    /**
     * set 里面就是使用注解时规定的值, 例如: @ListValue(vals = {0,1})  set= {0,1}
     */
    private Set<Integer> set = new HashSet<>();

    //初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        // 获取java后端写好的限制
        int[] vals = constraintAnnotation.vals();
        for (int val : vals) {
            set.add(val);
        }
    }

    /**
     * 判断是否校验成功
     * @param value 需要校验的值
     *              判断这个值再set里面没
     */
    @Override
    public boolean isValid(Integer value, ConstraintValidatorContext context) {
        // 每次请求传过来的值是否在java后端限制的值里
        return set.contains(value);
    }
}

(3)调用

ListValue只允许showStatus是0或1

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

7.规格参数&销售属性 概念

  • SPU:standard product unit(标准化产品单元):是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

  • 如iphoneX是SPU
    SKU:stock keeping unit(库存量单位):库存进出计量的基本单元,可以是件/盒/托盘等单位。SKU是对于大型连锁超市DC配送中心物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品对应有唯一的SKU号。

  • 如iphoneX 64G 黑色 是SKU
    基础属性:同一个SPU拥有的特性叫基本属性。如机身长度,这个是手机共用的属性。而每款手机的属性值不同

  • 也可以叫规格参数
    销售属性:能决定库存量的叫销售属性。如颜色

8 属性分组

8.1 前端组件抽取&父子组件交互

通过$emit向父组件发送事件,

image-20220216205521677

引入注册完成后调用,注意方法名要和$emit传入的一致

image-20220216205749781

8.2 分类查询接口

controller

传入的catelogId需要通过@PathVariable注解获取

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

AttrServiceImpl

通过QueryWrapper拼接查询条件

@Override
public PageUtils queryPage(Map<String, Object> params, Long catelogId) {
    String key = (String) params.get("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);
    }
}

8.3分组新增&级联选择器

(1)三级分类没有childdren时候不显示,通过设置JsonInclude注解

/**
 * 子分类
 * 不是数据表里的属性
 * 将当前菜单的子分类都保存到里面
 * 
 * */
@TableField(exist =false)
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private List<CategoryEntity> children;

image-20220216220016968

8.4 分组修改&级联选择器回显

(1)属性分组实体增加字段,用于显示完整的类别路径

/**
 * 三级分类修改的时候回显路径
 */
@TableField(exist = false)
private Long[] catelogPath;

(2)后端通过

 @Override
    public Long[] findCateLogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        paths = findParentPath(catelogId,paths);
        // 收集的时候是顺序 前端是逆序显示的 所以用集合工具类给它逆序一下
        Collections.reverse(paths);
        return  paths.toArray(new Long[paths.size()]);
    }

    private  List<Long> findParentPath(Long catlogId, List<Long> paths) {
        // 1、收集当前节点id
        paths.add(catlogId);
        CategoryEntity categoryEntity = this.getById(catlogId);
        if (categoryEntity.getParentCid() != 0) {
            findParentPath(categoryEntity.getParentCid(), paths);
        }
        return paths;
    }

8.5 品牌管理

(1)mybatis-plus分页插件

地址:https://baomidou.com/pages/97710a/#paginationinnerinterceptor

<!--mybatis-plus-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>

(2)MybatisConfig

package com.peng.gulimall.product.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@EnableTransactionManagement
@MapperScan("com.peng.gulimall.product.dao")
@Configuration
public class MybatisConfig {

    //    PaginationInterceptor
    @Bean
    public PaginationInnerInterceptor paginationInterceptor() {
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor();
        // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求  默认false
        paginationInterceptor.setOverflow(true);
        // 设置最大单页限制数量,默认 500 条,-1 不受限制
        paginationInterceptor.setMaxLimit(1000L);
        return paginationInterceptor;
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }

//    @Bean
//    public ConfigurationCustomizer configurationCustomizer() {
//        return configuration -> configuration.setUseDeprecatedExecutor(false);
//    }
}

(3)分页条件查询品牌

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
//        IPage<BrandEntity> page = this.page(
//                new Query<BrandEntity>().getPage(params),
//                new QueryWrapper<BrandEntity>()
//        );

        String key = (String) params.get("key");
        QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
        if (!StringUtil.isNullOrEmpty(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);
    }

9 平台属性

9.1 规格参数新增与VO

BeanUtils.copyProperties:vo对象和entity对象转换

@Override
public void saveAttr(AttrVo attrVo) {
    AttrEntity attrEntity = new AttrEntity();
    //vo对象和entity对象转换
    BeanUtils.copyProperties(attrVo, attrEntity);
    //1、保存基本数据
    this.save(attrEntity);
    //2、保存关联关系
    if (attrVo.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode() && attrVo.getAttrGroupId() != null) {
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();
        relationEntity.setAttrGroupId(attrVo.getAttrGroupId());
        relationEntity.setAttrId(attrEntity.getAttrId());
        relationEntity.setAttrSort(0);
        relationDao.insert(relationEntity);
    }

}

9.2 规格参数列表

AttrController

@GetMapping("/{attrType}/list/{catelogId}")
public R baseAttrList(@RequestParam Map<String, Object> params ,@PathVariable("catelogId") Long catelogId, @PathVariable("attrType") String attrType){

    PageUtils page = attrService.queryBaseAttrPage(params, catelogId, attrType);
    return R.ok().put("page", page);
}

AttrServiceImpl

    @Resource
    private AttrAttrgroupRelationDao relationDao;
    @Resource
    private AttrGroupDao attrGroupDao;
    @Resource
    private CategoryDao categoryDao;

  
  @Override
    public PageUtils queryBaseAttrPage(Map<String, Object> params, Long catelogId, String attrType) {
        // 传入的attrType是"base"或其他,但是数据库存的是 "0"销售 / "1"基本
        // 属性都在pms_attr表中混合着
        QueryWrapper<AttrEntity> wrapper =
                new QueryWrapper<AttrEntity>().eq("attr_type", "base".equalsIgnoreCase(attrType)
                        ?ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()
                        :ProductConstant.AttrEnum.ATTR_TYPE_SALE.getCode());

        // 如果参数带有分类id,则按分类查询
        if (catelogId != 0L ) {
            wrapper.eq("catelog_id", catelogId);
        }
        // 支持模糊查询,用id或者name查
        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
        );
        List<AttrEntity> records = page.getRecords();
        PageUtils pageUtils = new PageUtils(page);

        // 查到属性后还要结合分类名字、分组名字(分类->属性->分组) 封装为AttrRespVo对象
        List<AttrRespVo> attrRespVos = records.stream().map((attrEntity) -> {
            AttrRespVo attrRespVo = new AttrRespVo();
            BeanUtils.copyProperties(attrEntity, attrRespVo);

            // 1.设置分类和分组的名字  先获取中间表对象  给attrRespVo 封装分组名字
            if("base".equalsIgnoreCase(attrType)){ // 如果是规格参数才查询,或者说销售属性没有属性分组,只有分类
                // 根据属性id查询关联表,得到其属性分组
                AttrAttrgroupRelationEntity entity = relationDao.selectOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrEntity.getAttrId()));
                if (entity != null && entity.getAttrGroupId() != null) {
                    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(entity);
                    // 设置属性分组的名字
                    attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());
                }
            }

            // 2.查询分类id 给attrRespVo 封装三级分类名字
            CategoryEntity categoryEntity = categoryDao.selectById(attrEntity.getCatelogId());
            if (categoryEntity != null) {
                attrRespVo.setCatelogName(categoryEntity.getName());
            }
            return attrRespVo;
        }).collect(Collectors.toList());
        pageUtils.setList(attrRespVos);
        return pageUtils;
    }

9.3 规格参数修改

@Override
public void updateAttr(AttrVo attrVo) {
    AttrEntity attrEntity = new AttrEntity();
    BeanUtils.copyProperties(attrVo, attrEntity);
    this.updateById(attrEntity);

    // 基本类型才进行修改
    if (attrEntity.getAttrType() == ProductConstant.AttrEnum.ATTR_TYPE_BASE.getCode()) {
        // 修改分组关联
        AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();

        relationEntity.setAttrGroupId(attrVo.getAttrGroupId());
        relationEntity.setAttrId(attrVo.getAttrId());
        // 查询 attr_id 在 pms_attr_attrgroup_relation 表中是否已经存在 不存在返回0 表示这是添加 反之返回1 为修改 [这里的修改可以修复之前没有设置上的属性]
        Long count = relationDao.selectCount(new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrVo.getAttrId()));
        if(count > 0){
            relationDao.update(relationEntity, new UpdateWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", attrVo.getAttrId()));
        }else {
            relationDao.insert(relationEntity);
        }
    }
}

9.4 销售属性维护

主要步骤

  • 1.base是基础属性,sale是销售属性,根据attrType筛选AttrEntity(属性)
  • 2.如果查询的是基础属性需要设置分类名称

base是基础属性,sale是销售属性,根据attrType筛选AttrEntity(属性)

image-20240702042807431

基础属性需要设置分类名称

image-20240702042926335

9.5查询分组未关联的属性&删除关联

主要步骤

  • 1.获取当前分组的分类
  • 2.获取分类下所有属性分组
  • 3.获取分类下所有属性分组的属性
  • 4.筛选已经绑定的分组和分组属性

AttrAttrgroupRelationDao

package com.peng.gulimall.product.dao;

import com.peng.gulimall.product.entity.AttrAttrgroupRelationEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 属性&属性分组关联
 * 
 * @author peng
 * @email pengpeng6135@163.com
 * @date 2022-02-12 01:03:04
 */
@Mapper
public interface AttrAttrgroupRelationDao extends BaseMapper<AttrAttrgroupRelationEntity> {

    void deleteBatchRelation(@Param("entities") List<AttrAttrgroupRelationEntity> entities);
}

AttrAttrgroupRelationDao.xml

<!-- 发送批量删除语句 -->
<delete id="deleteBatchRelation">
    DELETE FROM `pms_attr_attrgroup_relation` WHERE
    <foreach collection="entities" item="item" separator=" OR ">
        (attr_id = #{item.attrId} AND attr_group_id = #{item.attrGroupId})
    </foreach>
</delete>

9.6 查询分组未关联的属性

@Override
public PageUtils getNoRelationAttr(Map<String, Object> params, Long attrgroupId) {
    //1、当前分组只能关联自己所属的分类里面的所有属性
    AttrGroupEntity attrGroupEntity = attrGroupDao.selectById(attrgroupId);
    Long catelogId = attrGroupEntity.getCatelogId();
    // 2、当前分组只能别的分组没有引用的属性                                                                         并且这个分组的id不是我当前正在查的id
    //2.1)、当前分类下的其他分组
    List<AttrGroupEntity> group = attrGroupDao.selectList(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));
    // 得到当前分类下面的所有分组id
    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));
    // 再次获取跟这些分组有关的属性id的集合
    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);
    }
    // 当搜索框中有key并且不为空的时候 进行模糊查询
    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;
}

9.7 新增分组与属性关联

规格参数(基础属性)添加和修改时后端没有获取值类型这个字段

image-20240702215807919

后端的EntityVo给商品属性加上值类型这个字段

image-20240702215610067

AttrAttrgroupRelationServiceImpl

    @Override
    public void saveBatch(List<AttrGroupRelationVo> vos) {
        // 对拷数据 然后批量保存
        List<AttrAttrgroupRelationEntity> entities = vos.stream().map(item -> {
            AttrAttrgroupRelationEntity entity = new AttrAttrgroupRelationEntity();
            BeanUtils.copyProperties(item, entity);
            entity.setAttrSort(0);
            return entity;
        }).collect(Collectors.toList());
        this.saveBatch(entities);
    }

10 新增商品

10.1调试会员等级相关接口

主要步骤:

  • 复制会员等级页面的前端代码
  • 后端自动化工程生成代码(前面已完成)
  • 配置gulimall-gateway网关服务,给会员服务添加网关配置
  • 添加会员等级的测试数据

(1)gulimall-member开启服务与发现等注解

image-20220501234807936

(2)gulimall-member配置路由

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

image-20220501234947041

(3)运行

image-20220502000233121

10.2获取分类关联的品牌

选择分类时,显示分类的所有品牌

主要步骤:

  • 根据分类Id(catId)查询CategoryBrandRelationEntity分类和品牌映射表,获取所有的分类和品牌映射关系

  • 循环分类和品牌映射关系,查询每个品牌,返回品牌列表集合

  • 流式编程品牌列表转换vo返回给前端

image-20240702222350396

根据分类Id(catId)查询CategoryBrandRelationEntity分类和品牌映射表,获取所有的分类和品牌映射关系

循环分类和品牌映射关系,查询每个品牌,返回品牌列表集合

image-20240702223140838

流式编程品牌列表转换vo返回给前端

image-20240702223209972

CategoryBrandRelationController

    @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 vo = new BrandVo();
            vo.setBrandId(item.getBrandId());
            vo.setBrandName(item.getName());
            return vo;
        }).collect(Collectors.toList());

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

CategoryBrandRelationServiceImpl

@Override
public List<BrandEntity> getBrandsByCatId(Long catId) {
    List<CategoryBrandRelationEntity> catelogId = categoryBrandRelationDao.selectList(new QueryWrapper<CategoryBrandRelationEntity>().eq("catelog_id", catId));
    // 根据品牌id查询详细信息
    List<BrandEntity> collect = catelogId.stream().map(item -> {
        Long brandId = item.getBrandId();
        BrandEntity entity = brandService.getById(brandId);
        return entity;
    }).collect(Collectors.toList());

    return collect;
}

10.3获取分类下的所有分组及属性

获取分类的规格参数(基本属性)

主要步骤

  • 根据商品分类id(catelogId)查询改商品下的所有属性分组
  • 遍历所有属性分组,根据属性分组和规格参数(基本属性)的映射表查询所有的规格参数(基本属性)
  • 返回改分组下的所有属性

image-20240702224607372

根据商品分类id(catelogId)查询改商品下的所有属性分组

遍历所有属性分组,根据属性分组和规格参数(基本属性)的映射表查询所有的规格参数(基本属性)

image-20240702225353329

根据属性熟属性分组attrgroupId查询所有规格参数(基础属性)

image-20240702225428744

AttrGroupController

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

AttrGroupServiceImpl

@Override
public List<AttrGroupWithAttrsVo> getAttrGroupWithAttrByCatelogId(Long catelogId) {
    // 1.查询这个品牌id下所有分组
    List<AttrGroupEntity> attrGroupEntities = this.list(new QueryWrapper<AttrGroupEntity>().eq("catelog_id", catelogId));

    // 2.查询所有属性
    List<AttrGroupWithAttrsVo> collect = attrGroupEntities.stream().map(group ->{
        // 先对拷分组数据
        AttrGroupWithAttrsVo attrVo = new AttrGroupWithAttrsVo();
        BeanUtils.copyProperties(group, attrVo);
        // 按照分组id查询所有关联属性并封装到vo
        List<AttrEntity> attrs = attrService.getRelationAttr(attrVo.getAttrGroupId());
        attrVo.setAttrs(attrs);
        return attrVo;
    }).collect(Collectors.toList());
    return collect;
}

AttrServiceImpl

@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) -> attr.getAttrId()).collect(Collectors.toList());
    // 根据这个属性查询到的id可能是空的
    if(attrIds == null || attrIds.size() == 0){
        return null;
    }
    return this.listByIds(attrIds);
}

10.4商品新增Vo抽取

基本信息

image-20240702234535359

规格参数

image-20240702234512703

销售属性

image-20240702234500746

sku信息

image-20240702234209056

雅丹黑 白沙银 南糯紫
12GB+256GB
12GB+512GB
新品手机 
全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 雅丹黑 12GB+256GB 全网通
华为Mate60 新品手机 雅丹黑 12GB+256GB 全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 雅丹黑 12GB+512GB 全网通
华为Mate60 新品手机 雅丹黑 12GB+512GB 全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 南糯紫 12GB+256GB 全网通
华为Mate60 新品手机 南糯紫 12GB+256GB 全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 南糯紫 12GB+512GB 全网通
华为Mate60 新品手机 南糯紫 12GB+512GB 全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 白沙银 12GB+256GB 全网通
华为Mate60 新品手机 白沙银 12GB+256GB 全网通
清凉季 暑期好物 超值钜惠

华为Mate60 新品手机 白沙银 12GB+512GB 全网通
华为Mate60 新品手机 白沙银 12GB+512GB 全网通
清凉季 暑期好物 超值钜惠

设置优惠、会员

image-20240702235447579

点击下一步,保存商品信息,粘贴商品保存的json

image-20240702235554337

生成json实体

https://www.bejson.com/json2javapojo/new/#google_vignette

image-20240702234354794

10.5商品新增业务流程分析

主要步骤:

  • 1、保存spu的基本信息 gulimall_pms - pms_spu_info
  • 2、保存spu的描述图片 gulimall_pms - pms_spu_info_desc
  • 3、保存spu的图片集 gulimall_pms - pms_spu_images
  • 4、保存spu的规格参数 gulimall_pms - pms_product_attr_value
  • 5、远程调用优惠卷服务gulimall_coupon,保存spu的积分信息 gulimall_sms - sms_spu_bounds
  • 6、保存spu对应的所有sku信息
    • a、sku的基本信息 gulimall_pms - pms_sku_info
    • b、sku的图片信息 gulimall_pms - pms_sku_images
    • c、sku的销售属性 gulimall_pms - pms_sku_sale_attr_value
    • d、远程调用优惠卷服务gulimall_coupon,保存sku的优惠、满减等信息 gulimall_sms - sms_sku_laddersms_sku_full_reductionsms_member_price

10.6保存SPU基本信息

主要步骤:

  • 1、保存spu的基本信息 gulimall_pms - pms_spu_info
  • 2、保存spu的描述图片 gulimall_pms - pms_spu_info_dessc
  • 3、保存spu的图片集 gulimall_pms - pms_spu_images
  • 4、保存spu的规格参数 gulimall_pms - pms_product_attr_value
  • 5、远程调用优惠卷服务gulimall_coupon,保存spu的积分信息 gulimall_sms - sms_spu_bounds
        //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积分信息失败");
        }

10.7保存SKU基本信息

保存spu对应的所有sku信息,主要步骤:

  • a、sku的基本信息 gulimall_pms - pms_sku_info
  • b、sku的图片信息 gulimall_pms - pms_sku_images
  • c、sku的销售属性 gulimall_pms - pms_sku_sale_attr_value
        //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优惠信息失败");
                    }
                }
            });
        }

10.8调用远程服务保存优惠等信息

主要步骤:

  • common中创建to用户服务和服务之间传递参数

  • 远程调用优惠卷服务gulimall_coupon,保存spu的积分信息 gulimall_sms - sms_spu_bounds

  • 远程调用优惠卷服务gulimall_coupon,保存sku的优惠、满减等信息 gulimall_sms - sms_sku_laddersms_sku_full_reductionsms_sku_full_reductionsms_member_price

common中创建to用户服务和服务之间传递参数

image-20240703014427238

gulimall_product创建com.peng.product.feign.CouponFeignService服务远程调用优惠卷服务gulimall_coupon,默认保存积分的接口自动化工程已经帮我们生成好了

image-20240703013352986

优惠卷服务gulimall_coupon创建接口/coupon/skufullreduction/saveinfo保存优惠信息

image-20240703013559568

保存sku阶梯价格信息(折扣)、sku满减信息、商品会员价格

image-20240703014025648

调用优惠卷服务gulimall_coupon的接口/coupon/spubounds/save保存spu的积分信息

image-20240703014721798

调用优惠卷服务gulimall_coupon的接口/coupon/skufullreduction/saveinfo保存sku的优惠、满减等信息

image-20240703014853380

10.9商品保存debug完成

配置服务占用内存

-Xmx 100m

image-20240703023531210

设置数据库当前会话的事务隔离级别是读未提交READ UNCOMMITTED,方便调试的时候查看数据

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  • SET SESSION: 指定更改仅影响当前会话。会话结束后,事务隔离级别将恢复默认值或全局设置。

  • TRANSACTION ISOLATION LEVEL: 设置事务的隔离级别。

  • READ UNCOMMITTED: 指定事务隔离级别为 READ UNCOMMITTED,允许读取未提交的数据(脏读)。

image-20240703025106811

Spu描述图片保存的时候报错,需要设置spuId字段为INPUT(输入)

image-20240703025048127

主键非空不自增,所以需要支持自己输入

image-20240703025427141

调试的时候边调试边查看数据是否正常提交

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

select * from mall_pms.pms_spu_info;

select * from mall_pms.pms_spu_info_desc;

select * from mall_pms.pms_spu_images;

select * from mall_pms.pms_product_attr_value;

select * from mall_sms.sms_spu_bounds;

select * from mall_pms.pms_sku_info;

select * from mall_pms.pms_sku_images;

select * from mall_pms.pms_sku_sale_attr_value;

select * from mall_sms.sms_sku_ladder;

select * from mall_sms.sms_sku_full_reduction;

select * from mall_sms.sms_member_price;


TRUNCATE TABLE mall_pms.pms_spu_info;
TRUNCATE TABLE mall_pms.pms_spu_info_desc;
TRUNCATE TABLE mall_pms.pms_spu_images;
TRUNCATE TABLE mall_pms.pms_product_attr_value;
TRUNCATE TABLE mall_sms.sms_spu_bounds;

TRUNCATE TABLE mall_pms.pms_sku_info;
TRUNCATE TABLE mall_pms.pms_sku_images;
TRUNCATE TABLE mall_pms.pms_sku_sale_attr_value;
TRUNCATE TABLE mall_sms.sms_sku_ladder;
TRUNCATE TABLE mall_sms.sms_sku_full_reduction;
TRUNCATE TABLE mall_sms.sms_member_price;

image-20240703045225356

10.10商品保存其他问题处理

上传SKU图片没有路径地址就不添加数据

image-20240703050808186

满量打折和满价打折大于0才发送请求,添加相应优惠数据

image-20240703050956041

在优惠卷服务里添加的时候也加上判断满量打折和满价打折大于0才添加

image-20240703051209147

会员价格大于0的时候才添加商品会员数据

image-20240703051312628

11.商品管理

开发阶段每次需要输入验证码,先取消一下这个功能,提高开发效率

image-20240703200453666

前段界面给文本框绑定默认值

image-20240703200600402

后端代码注释掉验证码功能

image-20240703200640487

每次登录点击一下就可以了

image-20240703200701692

11.1SPU检索

主要步骤

spu管理查询功能:

  • 根据分类、品牌、状态、id和名字进行条件查询
  • id和名字进行检索的时候注意条件拼接

image-20240703200808737

idspu_name是或者关系,和其他条件是并且关系

image-20240703201702999

完整代码

image-20240703201806791

查询出来的日期显示异常

image-20240703201938062

配置application.yaml

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

image-20240703202020733

已格式化时间

image-20240703202054729

11.2SKU检索

主要步骤

商品管理查询功能:

  • 根据分类、品牌、价格、id和名称进行检索
  • 分类Id、品牌Id查询时不能等于0
  • 价格可以等于0(>0),但是需要大于0
  • 需要对价格进行错误容错,如果不能转换为BigDecimal就不拼接条件

image-20240703202905784

完整代码

@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);
    }

测试

image-20240703203832457

12.仓库管理

12.1整合ware服务&获取仓库列表

主要步骤:

  • 配置仓储服务运行内存
  • 仓储服务gulimall-ware配置服务注册、日志级别
  • 网关服务配置gulimall-gateway配置仓储服务gulimall-ware路由
  • 根据仓库id、仓库名、仓库地址、区域编码查询仓库列表

image-20240703210441089

仓储服务设置运行内存

-Xmx100m

image-20240703205304893

application.yaml配置注册服务中心和日志级别

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.188.180:8848 # nacos地址

logging:
  level:
    com.peng: debug

image-20240703204855546

配置仓储服务路由

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

image-20240703205026592

启动项目,仓储服务gulimall-ware已成功注册

image-20240703205346888

根据仓库id、仓库名、仓库地址、区域编码查询仓库列表

image-20240703205429043

测试

image-20240703210414401

12.2查询库存&创建采购需求

主要步骤:

  • 1.根据仓库idskuId查询商品库存
  • 2.商品管理创建商品库存
  • 3.采购需求、采购单创建商品库存,合并采购需求为采购单
  • 4.根据仓库、状态、关键字查询采购需求

image-20240703220212169

根据仓库idskuId查询商品库存

image-20240703220333259

可以根据商品管理->库存管理添加库存

image-20240703220444816

采购需求、采购单创建商品库存,合并采购需求为采购单

image-20240703222113953

根据仓库、状态、关键字查询采购需求

@Override
    public PageUtils queryPage(Map<String, Object> params) {

        QueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<PurchaseDetailEntity>();

        String key = (String) params.get("key");
        if(!StringUtils.isEmpty(key)){
            //purchase_id  sku_id
            queryWrapper.and(w->{
                w.eq("purchase_id",key).or().eq("sku_id",key);
            });
        }

        String status = (String) params.get("status");
        if(!StringUtils.isEmpty(status)){
            //purchase_id  sku_id
            queryWrapper.eq("status",status);
        }

        String wareId = (String) params.get("wareId");
        if(!StringUtils.isEmpty(wareId)){
            //purchase_id  sku_id
            queryWrapper.eq("ware_id",wareId);
        }

        IPage<PurchaseDetailEntity> page = this.page(
                new Query<PurchaseDetailEntity>().getPage(params),
                queryWrapper
        );

        return new PageUtils(page);
    }

12.3合并采购需求

主要步骤:

  • 1.查询未领取的采购单
  • 2.管理员列表添加采购人员,采购单分配采购人员
  • 3.合并采购需求创建采购单,没有purchaseId创建新的采购单

查询未领取的采购单代码

image-20240703224031787

管理员列表添加采购人员

image-20240703224158234

创建一个采购单

image-20240703224524498

给创建的采购单分配一个采购人员

image-20240703224551363

合并采购需求创建采购单,没有purchaseId创建新的采购单

@Transactional
@Override
    public void mergePurchase(MergeVo mergeVo) {
        Long purchaseId = mergeVo.getPurchaseId();
        if(purchaseId == null){
            //1、新建一个
            PurchaseEntity purchaseEntity = new PurchaseEntity();

            purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());
            purchaseEntity.setCreateTime(new Date());
            purchaseEntity.setUpdateTime(new Date());
            this.save(purchaseEntity);
            purchaseId = purchaseEntity.getId();
        }

        //TODO 确认采购单状态是0,1才可以合并

        List<Long> items = mergeVo.getItems();
        Long finalPurchaseId = purchaseId;
        List<PurchaseDetailEntity> collect = items.stream().map(i -> {
            PurchaseDetailEntity detailEntity = new PurchaseDetailEntity();

            detailEntity.setId(i);
            detailEntity.setPurchaseId(finalPurchaseId);
            detailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());
            return detailEntity;
        }).collect(Collectors.toList());


        detailService.updateBatchById(collect);

        PurchaseEntity purchaseEntity = new PurchaseEntity();
        purchaseEntity.setId(purchaseId);
        purchaseEntity.setUpdateTime(new Date());
        this.updateById(purchaseEntity);
    }

合并成功

image-20240703225616378

没有选择采购单会重新生成采购单

image-20240703225601230

12.4领取采购单

主要步骤:

  • 1.根据id查询所有的采购单,并且筛选出新建和已分配状态
  • 2.改变采购单的状态为已领取
  • 3.改变采购项的状态为为正在采购
@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);
        });
    }

接口测试

image-20240704000820528

输入参数1,然后执行

image-20240704000847621

采购单和对应的采购需求状态都成功改变

image-20240704000925502

合并的时候需要判断采购单是新建和已分配状态

 // TODO 确认采购单状态是0,1才可以合并
        PurchaseEntity purchaseEntity = this.getById(purchaseId);
        if (purchaseEntity.getStatus() != WareConstant.PurchaseStatusEnum.CREATED.getCode()
                || purchaseEntity.getStatus() != WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {
            return;
        }

image-20240704001624837

12.5完成采购

主要步骤:

  • 1.更新采购单的状态
    • 采购项采购失败,采购单的状态需要更新为有异常
    • 采购项采购成功,采购单的状态需要更新已完成
  • 2.更新采购项的状态
    • 采购项采购失败,采购项的状态更新为采购失败,采购单的状态需要更新为有异常
    • 采购项采购成功,采购项的状态更新为已完成
    • 采购项采购成功,需要更新库存
  • 3.采购成功的进行入库

更新采购项的状态

  • 采购项采购失败,采购项的状态更新为采购失败,采购单的状态需要更新为有异常
  • 采购项采购成功,采购项的状态更新为已完成
  • 采购项采购成功,需要更新库存
    @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);    
}

采购成功的进行入库

  • 根据skuId(商品skuId)和ware_id(仓库Id)查询商品库存

  • 库存中没有该商品,需要重新添加

    • 远程调用商品服务gulimall-product获取sku商品名称
  • 库存中有该商品,需要修改库存:库存 = 现有库存 + 增加库存,条件是商品skuIdware_id

    • UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}
      
 @Override
    public void addStock(Long skuId, Long wareId, Integer skuNum) {
        //1、判断如果还没有这个库存记录新增
        List<WareSkuEntity> entities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));
        if(entities == null || entities.size() == 0){
            WareSkuEntity skuEntity = new WareSkuEntity();
            skuEntity.setSkuId(skuId);
            skuEntity.setStock(skuNum);
            skuEntity.setWareId(wareId);
            skuEntity.setStockLocked(0);
            //TODO 远程查询sku的名字,如果失败,整个事务无需回滚
            //1、自己catch异常
            //TODO 还可以用什么办法让异常出现以后不回滚?高级
            try {
                R info = productFeignService.info(skuId);
                Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");

                if(info.getCode() == 0){
                    skuEntity.setSkuName((String) data.get("skuName"));
                }
            }catch (Exception e){

            }
            wareSkuDao.insert(skuEntity);
        }else{
            wareSkuDao.addStock(skuId,wareId,skuNum);
        }

    }

仓储服务gulimall-ware配置远程调用商品服务gulimall-product

  • 让所有请求过网关:
    • @FeignClient("gulimall-gateway"):给gulimall-gateway所在的机器发请求
    • /api/product/skuinfo/info/{skuId}:带上api,网关服务自己解析
  • 直接让后台指定服务处理:
    • @FeignClient("gulimall-product"):给gulimall-product所在的机器发请求
    • /product/skuinfo/info/{skuId}:直接请求gulimall-product接口服务地址

image-20240704024514114

开启远程调用服务

@EnableFeignClients(basePackages = "com.peng.ware.feign")

image-20240704024705669

gulimall-ware服务配置MybatisPlus分页插件

image-20240704025456116

接着测试采购完成的接口

{
  "id": 1,
  "items": [
    {"itemId":1,"status": 3, "reason": ""},
    {"itemId":2,"status": 4, "reason": "无货"}
  ]
}

image-20240704025826016

我们有一个采购项采购失败了,所以采购单状态是有异常,采购项一个是已完成一个是采购失败

image-20240704025942476

采购流程

创建3个采购需求,不选择采购单自动创建采购单id为2的采购单

采购单状态为已分配,采购需求状态为已分配

image-20240704032245219

领取采购单id为2的采购单

采购单状态为已领取,采购需求状态为正在采购

image-20240704032347087

完成采购

采购单状态为已完成,采购需求状态为已完成

如果有一个采购需求为采购失败,那采购单状态为有异常

image-20240704032548732

11商品管理

11.3SPU规格维护

spuId查询所有spu基础属性

 @Override
    public List<ProductAttrValueEntity> baseAttrlistforspu(Long spuId) {
        List<ProductAttrValueEntity> entities = this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));
        return entities;
    }

不需要更新属性,全部删掉属性,然后添加

@Transactional
    @Override
    public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {
        //1、删除这个spuId之前对应的所有属性
        this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));
        List<ProductAttrValueEntity> collect = entities.stream().map(item -> {
            item.setSpuId(spuId);
            return item;
        }).collect(Collectors.toList());
        this.saveBatch(collect);
    }

13.分布式基础总结

清醒,知趣,明得失,知进退

posted @ 2024-10-14 00:58  peng_boke  阅读(160)  评论(1编辑  收藏  举报