SpringCloud 笔记

简单微服务架构

微服务架构的基础框架/组件

  • 服务注册发现
  • 服务网关(Service Gateway)
  • 后端通用服务(也称中间层服务Middle Tier Service)
  • 前端服务(也称边缘服务Edge Service)
SpringCloud架构

在这里插入图片描述

  • 所有请求都统一通过 API 网关(Zuul)来访问内部服务。
  • 网关接收到请求后,从注册中心(Eureka)获取可用服务。
  • 由 Ribbon 进行均衡负载后,分发到后端的具体实例。
  • 微服务之间通过 Feign 进行通信处理业务。
  • Hystrix 负责处理服务超时熔断。
  • Turbine 监控服务间的调用和熔断相关指标。

两大”配方“

阿里系
  • Dubbo
  • zookeeper
  • springmvc or springboot
  • ...
SpringCloud
  • spring cloud netflix eureka
  • springboot
  • ...

SpringCloud 是什么

SpringCloud 是一个开发工具集,包含多个子项目

  • 利用springboot 的开发便利
  • 主要是基于对Netflix 开源组件的进一步封装

SpringCloud 简化了分布式开发

  • 分布式/版本化配置。
  • 服务注册和发现。
  • 路由。
  • 服务和服务之间的调用。
  • 负载均衡。
  • 断路器。
  • 分布式消息传递。

掌握如何使用,更要理解分布式、架构的特点

Spring Cloud 工具框架

  • Spring Cloud Config,配置中心,利用 git 集中管理程序的配置。
  • Spring Cloud Netflix,集成众多 Netflix 的开源软件。
  • Spring Cloud Bus,消息总线,利用分布式消息将服务和服务实例连接在一起,用于在一个集群中传播状态的变化 。
  • Spring Cloud for Cloud Foundry,利用 Pivotal Cloudfoundry 集成你的应用程序。
  • Spring Cloud Foundry Service Broker,为建立管理云托管服务的服务代理提供了一个起点。
  • Spring Cloud Cluster,基于 Zookeeper、Redis、Hazelcast、Consul 实现的领导选举和平民状态模式的抽象和实现。
  • Spring Cloud Consul,基于 Hashicorp Consul 实现的服务发现和配置管理。
  • Spring Cloud Security,在 Zuul 代理中为 OAuth2 rest 客户端和认证头转发提供负载均衡。
  • Spring Cloud Sleuth Spring Cloud,应用的分布式追踪系统和 Zipkin、HTrace、ELK 兼容。
  • Spring Cloud Data Flow,一个云本地程序和操作模型,组成数据微服务在一个结构化的平台上。
  • Spring Cloud Stream,基于 Redis、Rabbit、Kafka 实现的消息微服务,简单声明模型用以在 Spring Cloud 应用中收发消息。
  • Spring Cloud Stream App Starters,基于 Spring Boot 为外部系统提供 Spring 的集成。
  • Spring Cloud Task,短生命周期的微服务,为 Spring Boot 应用简单声明添加功能和非功能特性。
  • Spring Cloud Task App Starters。
  • Spring Cloud Zookeeper,服务发现和配置管理基于 Apache Zookeeper。
  • Spring Cloud for Amazon Web Services,快速和亚马逊网络服务集成。
  • Spring Cloud Connectors,便于 PaaS 应用在各种平台上连接到后端像数据库和消息经纪服务。
  • Spring Cloud Starters,项目已经终止并且在 Angel.SR2 后的版本和其他项目合并。
  • Spring Cloud CLI,插件用 Groovy 快速的创建 Spring Cloud 组件应用。

Spring Cloud Eureka

基于Netflix Eureka 做了二次封装

两个组件组成

Eureka Server 注册中心

一、创建在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
二、打包并运行

  1. 项目更目录下面运行打包命令 mvn clean package
  2. java -jar xxxx.jar 启动(后台运行 nohup java -jar xxxxx.jar > /dev/null 2>&1 &)
Eureka Client 服务注册

一、创建在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

统一版本

高可用

spring-cloud为基础的微服务架构,所有的微服务都需要注册到注册中心,如果这个注册中心阻塞或者崩了,那么整个系统都无法继续正常提供服务,所以,这里就需要对注册中心进行集群,换言之,高可用(HA)

在这里插入图片描述在这里插入图片描述在这里插入图片描述

服务发现的两种方式

  • 客户端发现
    • Eureka
  • 服务端发现
    • Nginx
    • Dubbo、Zookeeper
    • Kubernetes

微服务中的服务拆分

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

服务拆分的方法论

在这里插入图片描述如何拆“功能”

  • 单一职责,松耦合、高内聚
  • 关注点分离
    • 按职责
    • 按通用性
    • 按粒度级别

服务和数据的关系

  • 先考虑业务功能,再考虑数据
  • 无状态服务在这里插入图片描述在这里插入图片描述

商品服务API和SQL介绍

API

商品列表
GET /product/list

参数

返回

{
    "code": 0,
    "msg": "成功",
    "data": [
        {
            "name": "热榜",
            "type": 1,
            "foods": [
                {
                    "id": "123456",
                    "name": "皮蛋粥",
                    "price": 1.2,
                    "description": "好吃的皮蛋粥",
                    "icon": "http://xxx.com",
                }
            ]
        },
        {
            "name": "好吃的",
            "type": 2,
            "foods": [
                {
                    "id": "123457",
                    "name": "慕斯蛋糕",
                    "price": 10.9,
                    "description": "美味爽口",
                    "icon": "http://xxx.com",
                }
            ]
        }
    ]
}
创建订单
POST /order/create

参数

name: "张三"
phone: "18868822111"
address: "慕课网总部"
openid: "ew3euwhd7sjw9diwkq" //用户的微信openid
items: [{
    productId: "1423113435324",
    productQuantity: 2 //购买数量
}]

返回

{
  "code": 0,
  "msg": "成功",
  "data": {
      "orderId": "147283992738221" 
  }
}
买家登录
GET /login/buyer

参数

openid: abc

返回

cookie里设置openid=abc

{
    code: 0,
    msg: "成功",
    data: null
}
卖家登录
GET /login/seller

参数

openid: xyz

返回

cookie里设置token=UUID, redis设置key=UUID, value=xyz

{
    code: 0,
    msg: "成功",
    data: null
}

SQL

-- 类目
create table `product_category` (
    `category_id` int not null auto_increment,
    `category_name` varchar(64) not null comment '类目名字',
    `category_type` int not null comment '类目编号',
    `create_time` timestamp not null default current_timestamp comment '创建时间',
    `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
    primary key (`category_id`),
    unique key `uqe_category_type` (`category_type`)
);
INSERT INTO `product_category` (`category_id`, `category_name`, `category_type`, `create_time`, `update_time`)
VALUES
	(1,'热榜',11,'2017-03-28 16:40:22','2017-11-26 23:39:36'),
	(2,'好吃的',22,'2017-03-14 17:38:46','2017-11-26 23:39:40');

-- 商品
create table `product_info` (
    `product_id` varchar(32) not null,
    `product_name` varchar(64) not null comment '商品名称',
    `product_price` decimal(8,2) not null comment '单价',
    `product_stock` int not null comment '库存',
    `product_description` varchar(64) comment '描述',
    `product_icon` varchar(512) comment '小图',
    `product_status` tinyint(3) DEFAULT '0' COMMENT '商品状态,0正常1下架',
    `category_type` int not null comment '类目编号',
    `create_time` timestamp not null default current_timestamp comment '创建时间',
    `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
    primary key (`product_id`)
);
INSERT INTO `product_info` (`product_id`, `product_name`, `product_price`, `product_stock`, `product_description`, `product_icon`, `product_status`, `category_type`, `create_time`, `update_time`)
VALUES
	('157875196366160022','皮蛋粥',0.01,39,'好吃的皮蛋粥','//fuss10.elemecdn.com/0/49/65d10ef215d3c770ebb2b5ea962a7jpeg.jpeg',0,1,'2017-03-28 19:39:15','2017-07-02 11:45:44'),
	('157875227953464068','慕斯蛋糕',10.90,200,'美味爽口','//fuss10.elemecdn.com/9/93/91994e8456818dfe7b0bd95f10a50jpeg.jpeg',1,1,'2017-03-28 19:35:54','2017-04-21 10:05:57'),
	('164103465734242707','蜜汁鸡翅',0.02,982,'好吃','//fuss10.elemecdn.com/7/4a/f307f56216b03f067155aec8b124ejpeg.jpeg',0,1,'2017-03-30 17:11:56','2017-06-24 19:20:54');

-- 订单
create table `order_master` (
    `order_id` varchar(32) not null,
    `buyer_name` varchar(32) not null comment '买家名字',
    `buyer_phone` varchar(32) not null comment '买家电话',
    `buyer_address` varchar(128) not null comment '买家地址',
    `buyer_openid` varchar(64) not null comment '买家微信openid',
    `order_amount` decimal(8,2) not null comment '订单总金额',
    `order_status` tinyint(3) not null default '0' comment '订单状态, 默认为新下单',
    `pay_status` tinyint(3) not null default '0' comment '支付状态, 默认未支付',
    `create_time` timestamp not null default current_timestamp comment '创建时间',
    `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
    primary key (`order_id`),
    key `idx_buyer_openid` (`buyer_openid`)
);

-- 订单商品
create table `order_detail` (
    `detail_id` varchar(32) not null,
    `order_id` varchar(32) not null,
    `product_id` varchar(32) not null,
    `product_name` varchar(64) not null comment '商品名称',
    `product_price` decimal(8,2) not null comment '当前价格,单位分',
    `product_quantity` int not null comment '数量',
    `product_icon` varchar(512) comment '小图',
    `create_time` timestamp not null default current_timestamp comment '创建时间',
    `update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
    primary key (`detail_id`),
    key `idx_order_id` (`order_id`)
);

-- 用户
CREATE TABLE `user_info` (
  `id` varchar(32) NOT NULL,
  `username` varchar(32) DEFAULT '',
  `password` varchar(32) DEFAULT '',
  `openid` varchar(64) DEFAULT '' COMMENT '微信openid',
  `role` tinyint(1) NOT NULL COMMENT '1买家2卖家',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  PRIMARY KEY (`id`)
);

在这里插入图片描述

商品服务编码实战

一、创建在这里插入图片描述在这里插入图片描述
二、项目初始化pom文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

在这里插入图片描述
三、启动类添加该注解在这里插入图片描述
四、基础配置在这里插入图片描述
五、添加数据库依赖

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
	<version>5.1.34</version>
</dependency>

添加配置

spring:
  application:
    name: product
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: root
    url: jdbc:mysql://192.168.180.199:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false
  jpa:
    show-sql: true
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

六、添加lombok

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

七、测试

  1. 商品查询测试

编写实体类

package com.springcloud.product.dataobject;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;
import java.util.Date;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-27
 * @Time: 22:36
 * @ProjectName: product
 * @Name: ProductInfo
 * @Version: 1.0.0
 * @Description: 商品实体类
 */

//@Table(name = "T_xxxxxxx")
@Data
@Entity
public class ProductInfo {
    @Id
    private String productId;
    private String productName;
    private BigDecimal productPrice;
    private Integer productStock;
    private String productDescription;
    private String productIcon;
    private Integer productStatus;
    private Integer categoryType;
    private Date createTime;
    private Date updateTime;
}

编写商品repository接口

package com.springcloud.product.repository;

import com.springcloud.product.dataobject.ProductInfo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-27
 * @Time: 22:36
 * @ProjectName: product
 * @Name: ProductInfoRepository
 * @Version: 1.0.0
 * @Description: 商品repository接口
 */
public interface ProductInfoRepository extends JpaRepository<ProductInfo,String> {
    /**
     * 通过状态查询商品
     * @param productStatus
     * @return
     */
    List<ProductInfo> findByProductStatus(Integer productStatus);
}

编写测试类

package com.springcloud.product.repository;

import com.springcloud.product.dataobject.ProductInfo;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductInfoRepositoryTest {
    @Autowired
    private ProductInfoRepository productInfoRepository;

    @Test
    public void findByProductStatus() throws Exception{
        List<ProductInfo> productInfoList = productInfoRepository.findByProductStatus(1);
        Assert.assertTrue(productInfoList.size() > 0);
    }

}
  1. 商品类目查询测试

编写实体类

package com.springcloud.product.dataobject;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Date;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-27
 * @Time: 23:02
 * @ProjectName: product
 * @Name: ProductCategory
 * @Version: 1.0.0
 * @Description: 商品类别
 */
@Data
@Entity
public class ProductCategory {
    @Id
    @GeneratedValue
    private Integer categoryId;
    private String categoryName;
    private Integer categoryType;
    private Date createTime;
    private Date updateTime;
}

编写DAO层代码

package com.springcloud.product.repository;

import com.springcloud.product.dataobject.ProductCategory;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-27
 * @Time: 22:36
 * @ProjectName: product
 * @Name: ProductCategoryRepository
 * @Version: 1.0.0
 * @Description: 商品类目repository接口
 */
public interface ProductCategoryRepository extends JpaRepository<ProductCategory,Integer> {
    /**
     * 通过类目状态查询商品类目
     * @param categoryTypeList
     * @return
     */
    List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
}

编写测试类

package com.springcloud.product.repository;

import com.springcloud.product.dataobject.ProductCategory;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductCategoryRepositoryTest {
    @Autowired
    private ProductCategoryRepository productCategoryRepository;

    @Test
    public void findByCategoryTypeTest() throws Exception{
        List<ProductCategory> productCategoryList = productCategoryRepository.findByCategoryTypeIn(Arrays.asList(11,12));
        Assert.assertTrue(productCategoryList.size() > 0);
    }

}

八、编写service

  1. interface
  • ProductService
package com.springcloud.product.service;

import com.springcloud.product.dataobject.ProductInfo;

import java.util.List;

/**
 * @author journey
 */
public interface ProductService {
    List<ProductInfo> findUpAll();
}
  • ProductCategoryService
package com.springcloud.product.service;

import com.springcloud.product.dataobject.ProductCategory;

import java.util.List;

/**
 * @author journey
 */
public interface ProductCategoryService {
    /**
     * 通过categoryType 查找productCategory
     * @param categoryTypeList
     * @return
     */
    List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList);
}
  1. 接口实现类
package com.springcloud.product.service.impl;

import com.springcloud.product.common.ProductStatusEnum;
import com.springcloud.product.dataobject.ProductInfo;
import com.springcloud.product.repository.ProductInfoRepository;
import com.springcloud.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-28
 * @Time: 00:02
 * @ProjectName: product
 * @Name: ProductServiceImpl
 * @Version: 1.0.0
 * @Description: 商品service
 */
@Service
public class ProductServiceImpl implements ProductService {
    @Autowired
    private ProductInfoRepository productInfoRepository;

    @Override
    public List<ProductInfo> findUpAll() {
        return productInfoRepository.findByProductStatus(ProductStatusEnum.UP.getCode());
    }
}
package com.springcloud.product.service.impl;

import com.netflix.discovery.converters.Auto;
import com.springcloud.product.dataobject.ProductCategory;
import com.springcloud.product.repository.ProductCategoryRepository;
import com.springcloud.product.service.ProductCategoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 11:20
 * @ProjectName: product
 * @Name: ProductCategoryServiceImpl
 * @Version: 1.0.0
 * @Description: 商品类目接口实现类
 */
@Service
public class ProductCategoryServiceImpl implements ProductCategoryService {
    @Autowired
    private ProductCategoryRepository productCategoryRepository;

    @Override
    public List<ProductCategory> findByCategoryTypeIn(List<Integer> categoryTypeList) {
        return productCategoryRepository.findByCategoryTypeIn(categoryTypeList);
    }
}
  1. 编写返回数据结构类
{
    "code": 0,
    "msg": "成功",
    "data": [
        {
            "name": "热榜",
            "type": 1,
            "foods": [
                {
                    "id": "123456",
                    "name": "皮蛋粥",
                    "price": 1.2,
                    "description": "好吃的皮蛋粥",
                    "icon": "http://xxx.com",
                }
            ]
        },
        {
            "name": "好吃的",
            "type": 2,
            "foods": [
                {
                    "id": "123457",
                    "name": "慕斯蛋糕",
                    "price": 10.9,
                    "description": "美味爽口",
                    "icon": "http://xxx.com",
                }
            ]
        }
    ]
}

ResultVO

package com.springcloud.product.vo;

import lombok.Data;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 11:32
 * @ProjectName: product
 * @Name: ResultVO
 * @Version: 1.0.0
 * @Description: http请求返回的最外层对象
 */
@Data
public class ResultVO<T> {
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 信息
     */
    private String msg;
    /**
     * 具体内容
     */
    private T data;
}

ProductVO

package com.springcloud.product.vo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 11:37
 * @ProjectName: product
 * @Name: ProductVO
 * @Version: 1.0.0
 * @Description: ProductVO
 */
@Data
public class ProductVO {

    @JsonProperty("name")
    private String categoryName;

    @JsonProperty("type")
    private Integer categoryType;

    @JsonProperty("foods")
    List<ProductInfoVO> productInfoVOList;
}

ProductInfoVO

package com.springcloud.product.vo;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;

import java.math.BigDecimal;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 11:40
 * @ProjectName: product
 * @Name: ProductInfoVO
 * @Version: 1.0.0
 * @Description: ProductInfoVO
 */
@Data
public class ProductInfoVO {
    @JsonProperty("id")
    private Integer productId;

    @JsonProperty("name")
    private String productName;

    @JsonProperty("price")
    private BigDecimal productPrice;

    @JsonProperty("description")
    private String productDescription;

    @JsonProperty("icon")
    private String productIcon;
}

编写ResultVOUtil

package com.springcloud.product.util;

import com.springcloud.product.vo.ResultVO;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 12:32
 * @ProjectName: product
 * @Name: ResultVOUtil
 * @Version: 1.0.0
 * @Description: ResultVOUtil
 */
public class ResultVOUtil {

    public static ResultVO success(Object object){
        ResultVO resultVO = new ResultVO();
        resultVO.setData(object);
        resultVO.setMsg("成功");
        resultVO.setCode(0);
        return resultVO;
    }
}

编写controller
ProductContoller

package com.springcloud.product.controller;

import com.springcloud.product.dataobject.ProductCategory;
import com.springcloud.product.dataobject.ProductInfo;
import com.springcloud.product.service.ProductCategoryService;
import com.springcloud.product.service.ProductService;
import com.springcloud.product.vo.ProductInfoVO;
import com.springcloud.product.vo.ProductVO;
import com.springcloud.product.vo.ResultVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

/**
 * @author journey
 */
@RestController
@RequestMapping("/product")
public class ProductContoller {
    @Autowired
    private ProductService productService;

    @Autowired
    private ProductCategoryService productCategoryService;
    /**
     * 1. 查询所有在架的商品
     * 2. 查询类目type列表
     * 3. 查询类目
     * 4. 构造数据
     */
    @GetMapping("/list")
    public ResultVO<ProductVO> list(){
        List<ProductInfo> productInfoList = productService.findUpAll();
        List<Integer> productCategoryTypeList = productInfoList.stream().map(ProductInfo::getCategoryType).collect(Collectors.toList());
        List<ProductCategory> productCategoryList = productCategoryService.findByCategoryTypeIn(productCategoryTypeList);

        List<ProductVO> productVOList = new ArrayList<>();
        for(ProductCategory productCategory: productCategoryList){
            ProductVO productVO = new ProductVO();
            productVO.setCategoryName(productCategory.getCategoryName());
            productVO.setCategoryType(productCategory.getCategoryType());

            List<ProductInfoVO> productInfoVOList = new ArrayList<>();

            for(ProductInfo productInfo: productInfoList){
                if(productInfo.getCategoryType().equals(productCategory.getCategoryType())){
                    ProductInfoVO productInfoVO = new ProductInfoVO();
                    BeanUtils.copyProperties(productInfo , productInfoVO);
                    productInfoVOList.add(productInfoVO);
                }
            }
            productVO.setProductInfoVOList(productInfoVOList);
            productVOList.add(productVO);
        }

        return ResultVOUtil.success(productVOList);
    }
}

订单服务

一、创建

在这里插入图片描述在这里插入图片描述
二、编写相关代码
entity

OrderMaster

package com.springcloud.order.dataobject;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.math.BigDecimal;
import java.util.Date;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 13:44
 * @ProjectName: order
 * @Name: OrderMaster
 * @Version: 1.0.0
 * @Description: 订单实体
 */
@Data
@Entity
public class OrderMaster {
    @Id
    private String orderId;

    private String buyerName;

    private String buyerPhone;

    private String buyerAddress;

    private String buyerOpenid;

    private BigDecimal orderAmount;

    private Integer orderStatus;

    private Integer payStatus;

    private Date createTime;

    private Date updateTime;
}

OrderDetail

package com.springcloud.order.dataobject;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.math.BigDecimal;
import java.util.Date;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 13:57
 * @ProjectName: order
 * @Name: OrderDetail
 * @Version: 1.0.0
 * @Description: OrderDetail
 */
@Data
@Entity
public class OrderDetail {
    @Id
    private String detailId;

    private String orderId;

    private String productId;

    private String productName;

    private BigDecimal productPrice;

    private Integer productQuantity;

    private String productIcon;

    private Date createTime;

    private Date updateTime;
}

reponsitory

  • OrderMasterReponsitory
public interface OrderMasterReponsitory extends JpaRepository<OrderMaster, String> {

}
  • OrderDetailReponsitory
public interface OrderDetailReponsitory extends JpaRepository<OrderDetail, String> {

}

enums

  • OrderStatusEnum
package com.springcloud.order.common;

import lombok.Getter;

@Getter
public enum OrderStatusEnum {
    NEW(0, "新订单"),
    FINISHED(1, "完结"),
    CANCEL(2, "取消"),
    ;
    private Integer code;

    private String message;

    OrderStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
  • PayStatusEnum
package com.springcloud.order.common;

import lombok.Getter;

@Getter
public enum PayStatusEnum {
    WAIT(0, "等待支付"),
    SUCCESS(1, "支付成功"),
    ;
    private Integer code;

    private String message;

    PayStatusEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}
  • ResultEnum
package com.springcloud.order.common;

import lombok.Getter;

@Getter
public enum ResultEnum {
    PARAM_ERROR(1, "参数错误"),
    CART_EMPTY(2, "购物车为空")

    ;

    private Integer code;

    private String message;

    ResultEnum(Integer code, String message ){
        this.code = code;
        this.message = message;
    }
}

controller

@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;


    @PostMapping("/create")
    public ResultVO<Map<String, String>> create(@Valid OrderForm orderForm,
                                                BindingResult bindingResult) {

        if(bindingResult.hasErrors()){
            log.error("【创建订单】参数不正确, orderForm={}", orderForm);
            throw new OrderException(ResultEnum.PARAM_ERROR.getCode(),
                    bindingResult.getFieldError().getDefaultMessage());
        }

        OrderDTO orderDTO = OrderForm2OrderDTOConverter.convert(orderForm);
        if (CollectionUtils.isEmpty(orderDTO.getOrderDetailList())) {
            log.error("【创建订单】购物车信息为空");
            throw new OrderException(ResultEnum.CART_EMPTY);
        }

        OrderDTO result = orderService.create(orderDTO);

        Map<String, String> map = new HashMap<>();
        map.put("orderId", result.getOrderId());
        return ResultVOUtil.success(map);
    }
}

vo

  • ResultVO
@Data
public class ResultVO<T> {
    private Integer code;
    private String msg;
    private T data;
}

form

  • OrderForm
@Data
public class OrderForm {
    @NotEmpty(message = "姓名必填")
    private String name;

    @NotEmpty(message = "手机号必填")
    private String phone;

    @NotEmpty(message = "地址必填")
    private String address;

    @NotEmpty(message = "openid必填")
    private String openid;

    @NotEmpty(message = "购物车不能为空")
    private String items;
}

exception

  • OrderException
public class OrderException extends RuntimeException{
    private Integer code;

    public OrderException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public OrderException(ResultEnum resultEnum) {
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
}

converter
-OrderForm2OrderDTOConverter

@Slf4j
public class OrderForm2OrderDTOConverter {
    public static OrderDTO convert(OrderForm orderForm) {
        Gson gson = new Gson();
        OrderDTO orderDTO = new OrderDTO();
        orderDTO.setBuyerName(orderForm.getName());
        orderDTO.setBuyerPhone(orderForm.getPhone());
        orderDTO.setBuyerAddress(orderForm.getAddress());
        orderDTO.setBuyerOpenid(orderForm.getOpenid());

        List<OrderDetail> orderDetailList = new ArrayList<>();
        try {
            orderDetailList = gson.fromJson(orderForm.getItems(),
                    new TypeToken<List<OrderDetail>>() {
                    }.getType());
        } catch (Exception e) {
            log.error("【json转换】错误, string={}", orderForm.getItems());
            throw new OrderException(ResultEnum.PARAM_ERROR);
        }
        orderDTO.setOrderDetailList(orderDetailList);

        return orderDTO;
    }
}
List<OrderDetail> orderDetailList = new ArrayList<>();
try {
    orderDetailList = gson.fromJson(orderForm.getItems(),
            new TypeToken<List<OrderDetail>>() {
            }.getType());
} catch (Exception e) {
    log.error("【json转换】错误, string={}", orderForm.getItems());
    throw new OrderException(ResultEnum.PARAM_ERROR);
}
orderDTO.setOrderDetailList(orderDetailList);

dto

  • OrderDTO
package com.springcloud.order.dto;

import com.springcloud.order.dataobject.OrderDetail;
import lombok.Data;

import java.math.BigDecimal;
import java.util.List;

/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 14:42
 * @ProjectName: order
 * @Name: OrderDTO
 * @Version: 1.0.0
 * @Description: OrderDTO
 */
@Data
public class OrderDTO {
    /** 订单id. */
    private String orderId;

    /** 买家名字. */
    private String buyerName;

    /** 买家手机号. */
    private String buyerPhone;

    /** 买家地址. */
    private String buyerAddress;

    /** 买家微信Openid. */
    private String buyerOpenid;

    /** 订单总金额. */
    private BigDecimal orderAmount;

    /** 订单状态, 默认为0新下单. */
    private Integer orderStatus;

    /** 支付状态, 默认为0未支付. */
    private Integer payStatus;

    private List<OrderDetail> orderDetailList;
}

注意:使用json转化

name: "张三"
phone: "18868822111"
address: "慕课网总部"
openid: "ew3euwhd7sjw9diwkq" //用户的微信openid
items: [{
    productId: "1423113435324",
    productQuantity: 2 //购买数量
}]

service

  • OrderService
/**
 * @author journey
 */
public interface OrderService {
    /**
     * 创建订单
     * @param orderDTO
     * @return
     */
    OrderDTO create(OrderDTO orderDTO);
}
  • impl
    • OrderServiceImpl
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMasterReponsitory orderMasterReponsitory;

    @Autowired
    private OrderDetailReponsitory orderDetailReponsitory;


    @Override
    public OrderDTO create(OrderDTO orderDTO) {

        String orderId = KeyUtil.genUniqueKey();

        OrderMaster orderMaster = new OrderMaster();
        orderDTO.setOrderId(orderId);
        BeanUtils.copyProperties(orderDTO, orderMaster);
        orderMaster.setOrderAmount(new BigDecimal(5));
        orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
        orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
        orderMasterReponsitory.save(orderMaster);
        return orderDTO;
    }
}

util

  • ResultVOUtil
public class ResultVOUtil {
    public static ResultVO success(Object object) {
        ResultVO resultVO = new ResultVO();
        resultVO.setCode(0);
        resultVO.setMsg("成功");
        resultVO.setData(object);
        return resultVO;
    }
}
  • KeyUtil
public class KeyUtil {
    /**
     * 生成唯一的主键
     * 格式: 时间+随机数
     */
    public static synchronized String genUniqueKey() {
        Random random = new Random();
        Integer number = random.nextInt(900000) + 100000;

        return System.currentTimeMillis() + String.valueOf(number);
    }
}

应用间的通信

HTTP

SpringCloud 中服务间两种restful调用方式
RestTemplate

第一种方式:直接使用restTemplate,url写死

@Slf4j
@RestController
public class ClientController {

    @GetMapping("/getProductMsg")
    public String getProductMsg(){
        // 1、第一种方式(直接使用restTemplate,url写死)
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject("http://localhost:9082/msg",String.class);
        log.info("response={}",response);
        return response;
    }

}

第二种方式:利用loadBalancerClient通过应用名获取url,然后再使用restTemplate

@Slf4j
@RestController
public class ClientController {

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/getProductMsg")
    public String getProductMsg(){

        //2、第二种方式(利用loadBalancerClient通过应用名获取url,然后再使用restTemplate)
        ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCT");
        String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()) + "/msg";
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject(url,String.class);

        log.info("response={}",response);
        return response;
    }

}

第三种方式:利用@LoadBalanced,可再restTemplate里使用应用名字

@Component
public class RestTemplateConfig {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }

}
@Slf4j
@RestController
public class ClientController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/getProductMsg")
    public String getProductMsg(){

        //3、第三种方式(利用@LoadBalanced,可再restTemplate里使用应用名字)
        String response = restTemplate.getForObject("http://PRODUCT/msg",String.class);

        log.info("response={}",response);
        return response;
    }

}
Feign
Feign负载均衡
  • Feign 解决什么问题在这里插入图片描述
  • Feign设计原理在这里插入图片描述
使用Feign实现组件间的通信

添加依赖

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

启动类上加注解

@EnableFeignClients

编写调用接口

@FeignClient(name = "product")
public interface ProductClient {
    @GetMapping("/msg")
    String productMsg();
}

编写controller

@RestController
@Slf4j
public class FeginProductController {
    @Autowired
    private ProductClient productClient;

    @GetMapping("/getProductMsg")
    public String getProductMsg(){
        String response = productClient.productMsg();
        log.info("response={}" ,response);
        return response;
    }
}
fegin实例

获取商品列表
product

  • controller
/**
  * 获取商品列表(给订单服务用的)
  *
  * @param productIdList
  * @return
  */
 @PostMapping("/listForOrder")
 public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList) {
     return productService.findList(productIdList);
 }
  • service
List<ProductInfo> findList(List<String> productIdList);
  • impl
@Override
public List<ProductInfo> findList(List<String> productIdList) {
    return productInfoRepository.findByProductIdIn(productIdList);
}
  • reponsitory
List<ProductInfo> findByProductIdIn(List<String> productIdList);

order

  • client
@FeignClient(name = "product")
public interface ProductClient {
    @GetMapping("/msg")
    String productMsg();

    @PostMapping("/product/listForOrder")
    List<ProductInfo> listForOrder(@RequestBody List<String> productIdList);
}
  • controller
@PostMapping("/listForOrder")
    public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList) {
        return productService.findList(productIdList);
    }

扣库存
product

  • controller
/**
     * 根据商品id 和数量扣去相应存储数量
     *
     * @param cartDTOList
     */
    @PostMapping("/decreaseStock")
    public void decreaseStocl(@RequestBody List<CartDTO> cartDTOList){
        productService.decreaseStock(cartDTOList);
    }
  • service
void decreaseStock(List<CartDTO> cartDTOList);
  • impl
@Override
    @Transactional
    public void decreaseStock(List<CartDTO> cartDTOList) {
        for(CartDTO cartDTO: cartDTOList){
            Optional<ProductInfo> productInfoOptional = productInfoRepository.findById(cartDTO.getProductId());
            // 判断商品是否存在
            if(!productInfoOptional.isPresent()){
                throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
            }
            // 库存是否足够
            ProductInfo productInfo = productInfoOptional.get();
            Integer result = productInfo.getProductStock() - cartDTO.getProductQuantity();
            if(result < 0){
                throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR);
            }

            productInfo.setProductStock(result);
            productInfoRepository.save(productInfo);
        }
    }

Optional:java8新特性类,主要解决空指针异常(NullPointerException)问题。https://www.runoob.com/java/java8-optional-class.html

dto

  • CartDTO
/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-07-07
 * @Time: 15:22
 * @ProjectName: product
 * @Name: CartDTO
 * @Version: 1.0.0
 * @Description: CartDTO
 */
@Data
public class CartDTO {
    /**
     * 商品id
     */
    private String productId;

    /**
     * 商品数量
     */
    private Integer productQuantity;

    public CartDTO() {
    }

    public CartDTO(String productId, Integer productQuantity) {
        this.productId = productId;
        this.productQuantity = productQuantity;
    }
}

exception

  • ProductException
public class ProductException extends RuntimeException {
    private Integer code;

    public ProductException(Integer code , String message){
        super(message);
        this.code = code;
    }

    public ProductException(ResultEnum resultEnum){
        super(resultEnum.getMessage());
        this.code = resultEnum.getCode();
    }
}

enums

  • ResultEnum
@Getter
public enum ResultEnum {
    PRODUCT_NOT_EXIST(1, "商品不存在"),

    PRODUCT_STOCK_ERROR(2, "库存有误")
    ;

    private Integer code;

    private String message;

    ResultEnum(Integer code , String message){
        this.code = code;
        this.message = message;
    }
}

order

  • client
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
  • FeginProductController
@GetMapping("/productDecreaseStock")
    public String productDecreaseStock(){
        productClient.decreaseStock(Arrays.asList(new CartDTO("157875196366160022",2)));
        return "ok";
    }

整合接口打通下单流程

  • OrderServiceImpl
/**
 * Create with IntelliJ IDEA.
 *
 * @Author: journey
 * @Date: 2019-06-30
 * @Time: 14:45
 * @ProjectName: order
 * @Name: OrderServiceImpl
 * @Version: 1.0.0
 * @Description: OrderServiveImpl
 */
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    private OrderMasterReponsitory orderMasterReponsitory;

    @Autowired
    private OrderDetailReponsitory orderDetailReponsitory;

    @Autowired
    private ProductClient productClient;


    @Override
    public OrderDTO create(OrderDTO orderDTO) {
        String orderId = KeyUtil.genUniqueKey();

        // 查询商品信息
        List<String> productIdList = orderDTO.getOrderDetailList().stream().map(OrderDetail::getProductId).collect(Collectors.toList());
        List<ProductInfo> productInfoList = productClient.listForOrder(productIdList);

        // 计算总价
        BigDecimal bigDecimal = new BigDecimal(0);
        for(OrderDetail orderDetail: orderDTO.getOrderDetailList()){
            for(ProductInfo productInfo: productInfoList){
                if(productInfo.getProductId().equals(orderDetail.getProductId())){
                    // 单价 * 数量
                    bigDecimal = productInfo.getProductPrice().multiply(new BigDecimal(orderDetail.getProductQuantity())).add(bigDecimal);

                    BeanUtils.copyProperties(productInfo,orderDetail);
                    orderDetail.setOrderId(orderId);
                    orderDetail.setDetailId(KeyUtil.genUniqueKey());
                    // 订单详情入库
                    orderDetailReponsitory.save(orderDetail);
                }
            }
        }

        // 扣库存
        List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream().map(e -> new CartDTO(e.getProductId(), e.getProductQuantity())).collect(Collectors.toList());
        productClient.decreaseStock(cartDTOList);

        // 订单入口
        OrderMaster orderMaster = new OrderMaster();
        orderDTO.setOrderId(orderId);
        BeanUtils.copyProperties(orderDTO, orderMaster);
//        orderMaster.setOrderAmount(new BigDecimal(5));
        orderMaster.setOrderAmount(bigDecimal);
        orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
        orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
        orderMasterReponsitory.save(orderMaster);
        return orderDTO;
    }
}

在这里插入图片描述

微服务之间的服务调用
服务调用示例
项目改成多模块

在这里插入图片描述
外层product pom

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product</name>
<description>Demo project for Spring Boot</description>
<packaging>pom</packaging>

<modules>
    <module>product-common</module>
    <module>product-client</module>
    <module>product-server</module>
</modules>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.springcloud</groupId>
            <artifactId>product-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</dependencyManagement>

product-common pom

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>com.springcloud</groupId>
    <artifactId>product</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-common</artifactId>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

product-client初始pom.xml

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>com.springcloud</groupId>
    <artifactId>product</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-client</artifactId>

<dependencies>
    <dependency>
        <groupId>com.springcloud</groupId>
        <artifactId>product-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

product-server的初始pom.xml

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product</name>
<description>Demo project for Spring Boot</description>
<packaging>pom</packaging>

<modules>
    <module>product-common</module>
    <module>product-client</module>
    <module>product-server</module>
</modules>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.springcloud</groupId>
            <artifactId>product-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</dependencyManagement>

为product-server添加Eureka client相关依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

启动类加注解@EnableDiscoveryClient

多模块项目创建
构建主工程

File-->New-->Project
-->Maven-->Create from archetype-->maven-archetype-quickstart-Next
-->GroupId={你的GroupId}-->AritifactId={你的ArtifactId}
-->Next-->Next-->Finish-->New Whindow

构建子模块

右键点击项目名称-->New-->Module
选中Spring Initializr-->Next
-->Group={主工程的GroupId}-->Aritifact={当前模块的ArtifactId}、
-->Next-->Next-->Finish

product
root

product的pom

<modelVersion>4.0.0</modelVersion>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<modules>
    <module>product-common</module>
    <module>product-client</module>
    <module>product-server</module>
</modules>
<packaging>pom</packaging>
<name>product</name>
<description>SpringCloud_sell's Demo project for Spring Cloud</description>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.springcloud</groupId>
            <artifactId>product-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</dependencyManagement>
modules

product-common的pom

<parent>
    <artifactId>product</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>product-common</artifactId>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

product-client的pom

<parent>
    <artifactId>product</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>product-client</artifactId>

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

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
    </dependency>

    <dependency>
        <groupId>com.springcloud</groupId>
        <artifactId>product-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>

product-server的pom

<parent>
    <artifactId>product</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>product-server</artifactId>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.34</version>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>com.springcloud</groupId>
        <artifactId>product-common</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>

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

    <!--<dependency>-->
    <!--<groupId>org.springframework.boot</groupId>-->
    <!--<artifactId>spring-boot-starter-amqp</artifactId>-->
    <!--</dependency>-->

    <!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-sleuth</artifactId>-->
    <!--</dependency>-->
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
将product 项目打包成可供依赖的jar包

在这里插入图片描述在这里插入图片描述

order
root

order的pom

<modelVersion>4.0.0</modelVersion>
<modules>
    <module>order-common</module>
    <module>order-client</module>
    <module>order-server</module>
</modules>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud</groupId>
<artifactId>order</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>order</name>
<description>SpringCloud_sell's Demo project for Spring Cloud</description>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

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

        <dependency>
            <groupId>com.springcloud</groupId>
            <artifactId>product-client</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

        <dependency>
            <groupId>com.springcloud</groupId>
            <artifactId>order-common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
</dependencyManagement>

<repositories>
    <repository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

<pluginRepositories>
    <pluginRepository>
        <id>spring-snapshots</id>
        <name>Spring Snapshots</name>
        <url>https://repo.spring.io/snapshot</url>
        <snapshots>
            <enabled>true</enabled>
        </snapshots>
    </pluginRepository>
    <pluginRepository>
        <id>spring-milestones</id>
        <name>Spring Milestones</name>
        <url>https://repo.spring.io/milestone</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
</pluginRepositories>
modules

order-common的pom

<parent>
    <artifactId>order</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>order-common</artifactId>

<name>order-common</name>
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

order-client的pom

<parent>
    <artifactId>order</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>order-client</artifactId>

<name>order-client</name>

order-server的pom

<parent>
    <artifactId>order</artifactId>
    <groupId>com.springcloud</groupId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<groupId>com.springcloud</groupId>
<artifactId>order-server</artifactId>

<name>order-server</name>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>

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

    <dependency>
        <groupId>com.springcloud</groupId>
        <artifactId>product-client</artifactId>
    </dependency>

    <dependency>
        <groupId>com.springcloud</groupId>
        <artifactId>order-common</artifactId>
    </dependency>

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

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-amqp</artifactId>
    </dependency>

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

    <!--<dependency>-->
    <!--<groupId>org.springframework.boot</groupId>-->
    <!--<artifactId>spring-boot-starter-data-redis</artifactId>-->
    <!--</dependency>-->

    <!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>-->
    <!--</dependency>-->

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

    <!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-sleuth-zipkin</artifactId>-->
    <!--</dependency>-->

    <!--包含sleuth和zipkin-->
    <!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-zipkin</artifactId>-->
    <!--</dependency>-->

    <!--<dependency>-->
    <!--<groupId>org.springframework.cloud</groupId>-->
    <!--<artifactId>spring-cloud-starter-bus-amqp</artifactId>-->
    <!--</dependency>-->
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
接口暴露与发现

product

@FeignClient(name="product",fallback = ProductClient.ProductClientFallback.class)
public interface ProductClient {

    @PostMapping("/product/listForOrder")
    List<ProductInfoOutput> listForOrder(@RequestBody List<String> productIdList);

    @PostMapping("/product/decreaseStock")
    void decreaseStock(@RequestBody List<DecreaseStockInput> decreaseStockInputList);

    @Component
    static class ProductClientFallback implements ProductClient {

        @Override
        public List<ProductInfoOutput> listForOrder(List<String> productIdList) {
            return null;
        }

        @Override
        public void decreaseStock(List<DecreaseStockInput> decreaseStockInputList) {

        }
    }
}

order

@EnableFeignClients(basePackages = "com.product.xxxxxxxx")
统一配置中心 Spring Cloud Config
统一配置中心概述

如果微服务架构中没有使用统一配置中心时,所存在的问题:

  • 配置文件分散在各个项目里,不方便维护
  • 配置内容安全与权限,实际开发中,开发人员是不知道线上环境的配置的
  • 更新配置后,项目需要重启在这里插入图片描述
Config server

新建一个Spring Initializr项目在这里插入图片描述
pom依赖在这里插入图片描述
添加注解

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer

新建git仓库

配置yml文件

spring:
  application:
    name: config
  cloud:
    config:
      server:
        git:
          uri: https://github.com/journeyC/SpringCloud.git
          username: 15251693879@163.com
          password:
	baseDir: /Users/journey/code/............
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

启动并运行http://localhost:8080/order-a.ymlhttp://localhost:8080/order-a.propertieshttp://localhost:8080/order-a.json

config client

项目pom文件中添加依赖

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

启动类上添加注解

@EnableDiscoveryClient

配置yml文件
将文件名改为 bootstrap.yml

spring:
  application:
    name: product
  cloud:
    config:
      discovery:
        service-id: CONFIG
        enabled: true
      profile: dev

配置文件路径名称
/{name}-{profiles}.yml
/{label}/{name}-{profiles}.yml

  • label 分支
  • name 服务
  • profiles 环境
  • .yml/json/properties 配置文件格式
Spring Cloud Bus

新增pom依赖

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

修改yml

rabbitmq:
    host: 192.168.180.199
    port: 5672
    username: admin
    password: admin

在这里插入图片描述在这里插入图片描述
使用bus-refresh

management:
  endpoints:
    web:
      exposure:
        include: "*"

http://localhost:8080/actuator/bus-refresh (post)

集成WebHooks实现动态更新

在这里插入图片描述在这里插入图片描述在这里插入图片描述

Ribbon负载均衡

添加Ribbon依赖

<!-- Ribbon相关 -->
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-config</artifactId>
</dependency>

修改yml

eureka:
  client:
    register-with-eureka: false
    service-url: 
      defaultZone: http://xxx:8761/eureka/,http://xxx:8762/eureka/,http://xxx:8763/eureka/

对ConfigBean进行新注解@LoadBalanced 获得Rest时加入Ribbon的配置

@Configuration
public class ConfigBean
{
  @Bean
  @LoadBalanced
  public RestTemplate getRestTemplate()
  {
   return new RestTemplate();
  }
}
异步和消息
RabbitMQ的基本使用

接收MQ消息

@Slf4j
@Component
public class MqReceiver {
    //    手动创建队列,并绑定@RabbitListener(queues = "myQueue")
    //    自动创建队列    @RabbitListener(queuesToDeclare = @Queue("myQueue"))
    //    自动创建 Exchange和Queue绑定
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("myQueue"),
            exchange = @Exchange("myExchange")
    ))
    public void process(String message) {
        log.info("接受到rabbitMQ消息:{}", message);
    }


    /**
     * 数码供应商服务 接受消息
     * @param message 接受消息
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("myOrder"),
            key = "computer",
            exchange = @Exchange("computerOrder")
    ))
    public void processComputer(String message) {
        log.info("Computer,接受到rabbitMQ消息:{}", message);
    }


    /**
     * 水果供应商服务 接受消息
     * @param message 接受消息
     */
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue("myOrder"),
            key = "fruit",
            exchange = @Exchange("fruitOrder")
    ))
    public void processFruit(String message) {
        log.info("fruit,接受到rabbitMQ消息:{}", message);
    }

}

手动创建队列,并绑定@RabbitListener(queues = "myQueue")
自动创建队列 @RabbitListener(queuesToDeclare = @Queue("myQueue"))
自动创建 Exchange和Queue绑定

MQReceiverTest

/**
 * @Author: journey
 * @Date: 2019-07-21
 * @Time: 16:03
 * @Description: 发送mq消息测试
 */
public class MQReceiverTest extends OrderApplicationTest {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @Test
    public void send() {
        amqpTemplate.convertAndSend("myQueue", "发送消息测试");
    }

    @Test
    public void sendMessage() {
        amqpTemplate.convertAndSend("computerOrder","computer", "发送消息测试");
    }
}

OrderApplicationTest

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderApplicationTest {

    @Test
    public void main() {
    }
}

在这里插入图片描述

SpringCloud Stream的基础使用

引入pom依赖

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

配置 yml

stream:
  bindings:
    myMessage:
      group: order

发送方

@RestController
public class SendMessageController {
    @Autowired
    private StreamClient streamClient;
    @GetMapping("/sendMessage")
    public void process (){
        String message = "now " + new Date();
        streamClient.output().send(MessageBuilder.withPayload(message).build());
    }
}

接收方
接口

public interface StreamClient {
    String INPUT = "input";

    String OUTPUT = "output";

    @Input(StreamClient.INPUT)
    SubscribableChannel input();

    @Output(StreamClient.OUTPUT)
    MessageChannel output();
}

实现类

/**
 * @Author: journey
 * @Date: 2019-07-21
 * @Time: 16:48
 * @Description:
 */
@Component
@EnableBinding(StreamClient.class)
@Slf4j
public class StreamReceiver {
    @StreamListener(StreamClient.INPUT)
    @SendTo(StreamClient.OUTPUT)
    public Object process(Object message){
        log.info("StreamReceiver: {}" , message);
        return message;
    }

    @StreamListener(StreamClient.OUTPUT)
    public void processOutput(Object message){
        log.info("StreamReceiver: {}" , message);
    }
}
SpringCloud Stream RabbitMQ Redis案例

product
添加pom依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

添加yml配置

rabbitmq:
    host: 192.168.180.199
    port: 5672
    username: admin
    password: admin
    virtual-host: /

service

@Autowired
private AmqpTemplate amqpTemplate;

......

@Override
public void decreaseStock(List<DecreaseStockInput> decreaseStockInputList) {
    // 发送 mq 消息
    List<ProductInfoOutput> productInfoOutputList = decreaseStockProcess(decreaseStockInputList).stream()
            .map(e -> {
                ProductInfoOutput output = new ProductInfoOutput();
                BeanUtils.copyProperties(e, output);
                return output;
            })
            .collect(Collectors.toList());

    amqpTemplate.convertAndSend("decreaseStockQueue", JsonUtil.toJson(productInfoOutputList));
}

@Transactional
public List<ProductInfo> decreaseStockProcess(List<DecreaseStockInput> decreaseStockInputList) {
    List<ProductInfo> productInfoList = new ArrayList<>();
    for(DecreaseStockInput decreaseStockInput : decreaseStockInputList){
        Optional<ProductInfo> productInfoOptional = productInfoRepository.findById(decreaseStockInput.getProductId());
        // 商品是否存在
        if(!productInfoOptional.isPresent())
            throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
        ProductInfo productInfo = productInfoOptional.get();
        int result = productInfo.getProductStock() - decreaseStockInput.getProductQuantity();
        // 商品库存是否充足
        if(result < 0)
            throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR);
        productInfo.setProductStock(result);
        productInfoRepository.save(productInfo);
        productInfoList.add(productInfo);
    }
    return productInfoList;
}

order
ProductInfoReceiver

@Component
@Slf4j
public class ProductInfoReceiver {
    @RabbitListener(queuesToDeclare = @Queue("decreaseStockQueue"))
    public void process(String message){
        // message => productInfoOutput
        List<ProductInfoOutput> productInfoOutputList = JsonUtil.fromJson(message,
                new TypeReference<List<ProductInfoOutput>>() {});
        log.info("从队列【{}】接收到消息:{}", "decreaseStockQueue", productInfoOutputList);
    }

}

在这里插入图片描述在这里插入图片描述
添加redis数据库缓存
新增redis依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

添加yml配置

redis:
    host: 192.168.180.199
    port: 6379
    database: 0

redis设置
redis-server /etc/redis.conf

/**
 * @Author: journey
 * @Date: 2019-07-21
 * @Time: 20:09
 * @Description:
 */
@Component
@Slf4j
public class ProductInfoReceiver {
    private static String PRODUCT_STOCK_TEMPLATE = "product_stock_%s";

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @RabbitListener(queuesToDeclare = @Queue("decreaseStockQueue"))
    public void process(String message){
        // message => productInfoOutput
        List<ProductInfoOutput> productInfoOutputList = JsonUtil.fromJson(message,
                new TypeReference<List<ProductInfoOutput>>() {});
        log.info("从队列【{}】接收到消息:{}", "decreaseStockQueue", productInfoOutputList);

        // 存储到 redis 中
        for(ProductInfoOutput productInfoOutput : productInfoOutputList)
            stringRedisTemplate.opsForValue()
                    .set(String.format(PRODUCT_STOCK_TEMPLATE, productInfoOutput.getProductId()),
                            String.valueOf(productInfoOutput.getProductStock()));
    }

}

在这里插入图片描述

服务网关和Zuul

zuul路由网关在这里插入图片描述
添加pom依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>

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

    <!--注册中心-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        <exclusions>
            <exclusion>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

添加yml配置

server:
  port: 8000
spring:
  application:
    name: journey-zuul


zuul:
  routes:
    ribbon:
      path: /ribbon/**
      service-id: journey-edu  #转发到消费者 /ribbon/
    feign:
      path: /feign/**
      service-id: journey-edu # 转发到消费者 /feign/

eureka:
  client:
    service-url:
      default-zone: http://localhost:8761/eureka

创建MyFallbackProvider文件

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "journey-edu";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("route:"+route);
        System.out.println("exception:"+cause.getMessage());
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return 200;
            }

            @Override
            public String getStatusText() throws IOException {
                return "ok";
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                return new ByteArrayInputStream("Sorry, the service is unavailable now.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
}

创建MyFilter

Component
public class MyFilter extends ZuulFilter {

    private static Logger log=LoggerFactory.getLogger(MyFilter.class);

    @Override
    public String filterType() {
        return "pre"; // 定义filter的类型,有pre、route、post、error四种
    }

    @Override
    public int filterOrder() {
        return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    public boolean shouldFilter() {
        return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    public Object run() throws ZuulException {
        // filter需要执行的具体操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        System.out.println(token);
        if(token==null){
            log.warn("there is no request token");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("there is no request token");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        log.info("ok");
        return null;
    }
}

创建启动类ZuulApplication

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

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

}

PRC

Dubbo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值