【Spring Cloud Alibaba 温故而知新】(七)模拟电商 - 商品微服务 | Eddie'Blog
【Spring Cloud Alibaba 温故而知新】(七)模拟电商 - 商品微服务

【Spring Cloud Alibaba 温故而知新】(七)模拟电商 - 商品微服务

eddie 453 2021-11-17

【】

目录

10.1.1 商品微服务功能设计

  • 商品微服务功能逻辑架构
    • Tips:微服务功能设计看需求、看业务设定、看个人喜好

在这里插入图片描述

在这里插入图片描述

10.1.1.1 商品微服务架构图

sca-commerce-goods-service

在这里插入图片描述

10.1.1.2 Maven依赖

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>sca-commerce-service</artifactId>
        <groupId>com.edcode.commerce</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>sca-commerce-goods-service</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <!-- 模块名及描述信息 -->
    <name>sca-commerce-goods-service</name>
    <description>商品服务</description>

    <dependencies>
        <!-- spring cloud alibaba nacos discovery 依赖 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <!-- zipkin = spring-cloud-starter-sleuth + spring-cloud-sleuth-zipkin-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zipkin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.kafka</groupId>
            <artifactId>spring-kafka</artifactId>
            <version>2.5.0.RELEASE</version>
        </dependency>
        <!-- 引入 redis 的依赖 -->
<!--        <dependency>-->
<!--            <groupId>org.springframework.boot</groupId>-->
<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
<!--        </dependency>-->
        <!-- Java Persistence API, ORM 规范 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <!-- MySQL 驱动, 注意, 这个需要与 MySQL 版本对应 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.12</version>
            <scope>runtime</scope>
        </dependency>
        <!-- aop 依赖, aspectj -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.edcode.commerce</groupId>
            <artifactId>sca-commerce-service-config</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>com.edcode.commerce</groupId>
            <artifactId>sca-commerce-service-sdk</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

    <!--
        SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持,可以将
        SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
     -->
    <build>
        <finalName>${artifactId}</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

10.1.1.2 bootstrap.yml

server:
  port: 8001
  servlet:
    context-path: /scacommerce-goods-service

spring:
  application:
    name: sca-commerce-goods-service
  cloud:
    nacos:
      # 服务注册发现
      discovery:
        enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可
        #server-addr: ${NACOS_ADDR:127.0.0.1}:8848
        server-addr: ${NACOS_ADDR_1:127.0.0.1}:8848,${NACOS_ADDR_2:127.0.0.1}:8849,${NACOS_ADDR_3:127.0.0.1}:8850 # Nacos 服务器地址
        namespace: ${NAMESPACE_ID:1adcfdd8-5763-4768-9a15-9c7157988950}
        metadata:
          management:
            context-path: ${server.servlet.context-path}/actuator
  sleuth:
    sampler:
      probability: 1.0  # 采样比例, 1.0 表示 100%, 默认是 0.1
  kafka:
    bootstrap-servers: ${KAFKA_SERVER:127.0.0.1}:${KAFKA_PORT:9092}
    producer:
      retries: 3
    consumer:
      auto-offset-reset: latest
  zipkin:
    sender:
      type: ${ZIPKIN_KAFKA_SENDER:web} # 默认是 web
    base-url: http://${ZIPKIN_URL:localhost}:${ZIPKIN_PORT:9411}/
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: none
    properties:
      hibernate.show_sql: true
      hibernate.format_sql: true
    open-in-view: false
  datasource:
    # 数据源
    url: jdbc:mysql://${MYSQL_HOST:127.0.0.1}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:sca_commerce}?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false
    username: ${MYSQL_USERNAME:root}
    password: ${MYSQL_PASSWORD:123456}
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 连接池
    hikari:
      maximum-pool-size: 8
      minimum-idle: 4
      idle-timeout: 30000
      connection-timeout: 30000
      max-lifetime: 45000
      auto-commit: true
      pool-name: ImoocCouponHikariCP
  redis:
    database: 0
    host: ${REDIS_HOST:localhost}
    port: ${REDIS_PORT:6379}
    timeout: 5000

# 暴露端点
management:
  endpoints:
    web:
      exposure:
        include: '*'
  endpoint:
    health:
      show-details: always

10.1.1.3 启动类

package com.edcode.commerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品微服务启动入口
 *
 * 启动依赖组件/中间件: Redis + MySQL + Nacos + Kafka + Zipkin
 *
 * http://127.0.0.1:8001/scacommerce-goods-service/doc.html
 *
 */
@EnableJpaAuditing
@EnableDiscoveryClient
@SpringBootApplication
public class GoodsApplication {

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

10.2.1 商品属性枚举类及转换器定义

10.2.1.1 创建商品状态枚举类

package com.edcode.commerce.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品状态枚举类
 */
@Getter
@AllArgsConstructor
public enum GoodsStatus {

	ONLINE(101, "上线"),
    OFFLINE(102, "下线"),
    STOCK_OUT(103, "缺货"),;

	/** 状态码 */
	private final Integer status;

	/** 状态描述 */
	private final String description;

	/**
	 * 根据 code 获取到 GoodsStatus
	 */
	public static GoodsStatus of(Integer status) {

		Objects.requireNonNull(status);

		return Stream.of(values())
				.filter(bean -> bean.status.equals(status))
				.findAny()
				.orElseThrow(() -> new IllegalArgumentException(status + " not exists"));
	}
}

10.2.1.2 创建商品状态枚举属性转换器

package com.edcode.commerce.converter;

import com.edcode.commerce.constant.GoodsStatus;

import javax.persistence.AttributeConverter;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品状态枚举属性转换器
 */
public class GoodsStatusConverter implements AttributeConverter<GoodsStatus, Integer> {

	/**
	 * 转换成可以存入数据表的基本类型
	 */
	@Override
	public Integer convertToDatabaseColumn(GoodsStatus goodsStatus) {
		return goodsStatus.getStatus();
	}

	/**
	 * 还原数据表中的字段值到 Java 数据类型
	 */
	@Override
	public GoodsStatus convertToEntityAttribute(Integer status) {
		return GoodsStatus.of(status);
	}
}

10.2.1.3 创建品牌分类举类

package com.edcode.commerce.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 品牌分类举类
 */
@Getter
@AllArgsConstructor
public enum BrandCategory {

    BRAND_A("20001", "品牌A"),
    BRAND_B("20002", "品牌B"),
    BRAND_C("20003", "品牌C"),
    BRAND_D("20004", "品牌D"),
    BRAND_E("20005", "品牌E"),
    ;

    /** 品牌分类编码 */
    private final String code;

    /** 品牌分类描述信息 */
    private final String description;

    /**
     * 根据 code 获取到 BrandCategory
     */
    public static BrandCategory of(String code) {

        Objects.requireNonNull(code);

        return Stream.of(values())
                .filter(bean -> bean.code.equals(code))
                .findAny()
                .orElseThrow(
                        () -> new IllegalArgumentException(code + " not exists")
                );
    }
}

10.3.1.1 创建品牌分类枚举属性转换器

package com.edcode.commerce.converter;

import com.edcode.commerce.constant.BrandCategory;

import javax.persistence.AttributeConverter;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 品牌分类枚举属性转换器
 */
public class BrandCategoryConverter implements AttributeConverter<BrandCategory, String> {

	@Override
	public String convertToDatabaseColumn(BrandCategory brandCategory) {
		return brandCategory.getCode();
	}

	@Override
	public BrandCategory convertToEntityAttribute(String code) {
		return BrandCategory.of(code);
	}
}

10.3.1.2 创建商品类别枚举类

package com.edcode.commerce.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.Objects;
import java.util.stream.Stream;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品类别枚举类
 */
@Getter
@AllArgsConstructor
public enum GoodsCategory {

    DIAN_QI("10001", "电器"),
    JIA_JU("10002", "家具"),
    FU_SHI("10003", "服饰"),
    MY_YIN("10004", "母婴"),
    SHI_PIN("10005", "食品"),
    TU_SHU("10006", "图书"),
    ;

    /** 商品类别编码 */
    private final String code;

    /** 商品类别描述信息 */
    private final String description;

    /**
     * 根据 code 获取到 GoodsCategory
     */
    public static GoodsCategory of(String code) {
        Objects.requireNonNull(code);
        return Stream.of(values())
                .filter(bean -> bean.code.equals(code))
                .findAny()
                .orElseThrow(
                        () -> new IllegalArgumentException(code + " not exists")
                );
    }
}

10.3.1.3 创建商品类别枚举属性转换器

package com.edcode.commerce.converter;

import com.edcode.commerce.constant.GoodsCategory;

import javax.persistence.AttributeConverter;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品类别枚举属性转换器
 */
public class GoodsCategoryConverter implements AttributeConverter<GoodsCategory, String> {

	@Override
	public String convertToDatabaseColumn(GoodsCategory goodsCategory) {
		return goodsCategory.getCode();
	}

	@Override
	public GoodsCategory convertToEntityAttribute(String code) {
		return GoodsCategory.of(code);
	}
}

10.4.1 数据表及ORM过程 - 商品表

10.4.1.1 创建 t_scacommerce_goods 数据表

-- 创建 t_scacommerce_goods 数据表
CREATE TABLE IF NOT EXISTS `sca_commerce`.`t_scacommerce_goods` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `goods_category` varchar(64) NOT NULL DEFAULT '' COMMENT '商品类别',
  `brand_category` varchar(64) NOT NULL DEFAULT '' COMMENT '品牌分类',
  `goods_name` varchar(64) NOT NULL DEFAULT '' COMMENT '商品名称',
  `goods_pic` varchar(256) NOT NULL DEFAULT '' COMMENT '商品图片',
  `goods_description` varchar(512) NOT NULL DEFAULT '' COMMENT '商品描述信息',
  `goods_status` int(11) NOT NULL DEFAULT 0 COMMENT '商品状态',
  `price` int(11) NOT NULL DEFAULT 0 COMMENT '商品价格',
  `supply` bigint(20) NOT NULL DEFAULT 0 COMMENT '总供应量',
  `inventory` bigint(20) NOT NULL DEFAULT 0 COMMENT '库存',
  `goods_property` varchar(1024) NOT NULL DEFAULT '' COMMENT '商品属性',
  `create_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '创建时间',
  `update_time` datetime NOT NULL DEFAULT '0000-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `goods_category_brand_name` (`goods_category`, `brand_category`, `goods_name`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 COMMENT='商品表';

10.4.1.2 创建商品表(t_scacommerce_goods)表实体类

package com.edcode.commerce.entity;

import java.util.Date;
import javax.persistence.*;

import com.edcode.commerce.converter.BrandCategoryConverter;
import com.edcode.commerce.converter.GoodsCategoryConverter;
import com.edcode.commerce.converter.GoodsStatusConverter;
import org.hibernate.annotations.GenericGenerator;
import java.io.Serializable;

import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

/**
 * @description 商品表(t_scacommerce_goods)表实体类
 * @author eddie.lee
 * @date 2021-11-15 15:28:28
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_scacommerce_goods")
@EntityListeners(AuditingEntityListener.class) // 作用:自动更新时间, 需要配合 @EnableJpaAuditing 使用
public class ScaCommerceGoods implements Serializable {

	/**
	 * 自增主键
	 */
	@Id
	@Column(name = "id", nullable = false)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * 商品类别
	 */
	@Column(name = "goods_category", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = GoodsCategoryConverter.class)
	private String goodsCategory;

	/**
	 * 品牌分类
	 */
	@Column(name = "brand_category", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = BrandCategoryConverter.class)
	private String brandCategory;

	/**
	 * 商品名称
	 */
	@Column(name = "goods_name", nullable = false)
	private String goodsName;

	/**
	 * 商品图片
	 */
	@Column(name = "goods_pic", nullable = false)
	private String goodsPic;

	/**
	 * 商品描述信息
	 */
	@Column(name = "goods_description", nullable = false)
	private String goodsDescription;

	/**
	 * 商品状态
	 */
	@Column(name = "goods_status", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = GoodsStatusConverter.class)
	private Integer goodsStatus;

	/**
	 * 商品价格: 单位: 分、厘
	 */
	@Column(name = "price", nullable = false)
	private Integer price;

	/**
	 * 总供应量
	 */
	@Column(name = "supply", nullable = false)
	private Long supply;

	/**
	 * 库存
	 */
	@Column(name = "inventory", nullable = false)
	private Long inventory;

	/**
	 * 商品属性, json 字符串存储
	 */
	@Column(name = "goods_property", nullable = false)
	private String goodsProperty;

	/**
	 * 创建时间
	 */
	@CreatedDate
	@Column(name = "create_time", nullable = false)
	private Date createTime;

	/**
	 * 更新时间
	 */
	@LastModifiedDate
	@Column(name = "update_time", nullable = false)
	private Date updateTime;

}

10.4.1.3 ScaCommerceGoods Dao 接口定义 - (分页、排序)

package com.edcode.commerce.dao;

import com.edcode.commerce.constant.BrandCategory;
import com.edcode.commerce.constant.GoodsCategory;
import com.edcode.commerce.entity.ScaCommerceGoods;
import org.springframework.data.repository.PagingAndSortingRepository;

import java.util.Optional;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description ScaCommerceGoods Dao 接口定义
 *
 *              PagingAndSortingRepository 实现分页和排序的功能
 *
 */
public interface ScaCommerceGoodsDao extends PagingAndSortingRepository<ScaCommerceGoods, Long> {

	/**
	 * 根据查询条件查询商品表, 并限制返回结果
	 * 
	 * @param goodsCategory
	 * @param brandCategory
	 * @param goodsName
	 * @return
	 * 
	 *    select * from t_scacommerce_goods
     *      where goods_category = ? and brand_category = ? and goods_name = ?
     *    limit 1;
	 */
	Optional<ScaCommerceGoods> findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
	        GoodsCategory goodsCategory,
			BrandCategory brandCategory,
            String goodsName
    );
}

10.5.1 商品信息对象定义及转换方法

10.5.1.1 详细的商品信息

package com.edcode.commerce.goods;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 详细的商品信息
 */
@ApiModel(description = "详细的商品信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfo {

	@ApiModelProperty(value = "商品表主键 id")
	private Long id;

	@ApiModelProperty(value = "商品类别编码")
	private String goodsCategory;

	@ApiModelProperty(value = "品牌分类编码")
	private String brandCategory;

	@ApiModelProperty(value = "商品名称")
	private String goodsName;

	@ApiModelProperty(value = "商品图片")
	private String goodsPic;

	@ApiModelProperty(value = "商品描述信息")
	private String goodsDescription;

	@ApiModelProperty(value = "商品状态")
	private Integer goodsStatus;

	@ApiModelProperty(value = "商品价格, 单位: 分")
	private Integer price;

	@ApiModelProperty(value = "商品属性")
	private GoodsProperty goodsProperty;

	@ApiModelProperty(value = "供应量")
	private Long supply;

	@ApiModelProperty(value = "库存")
	private Long inventory;

	@ApiModelProperty(value = "创建时间")
	private Date createTime;

	@ApiModelProperty(value = "更新时间")
	private Date updateTime;

	/**
	 * 商品属性
	 */
	@ApiModel(description = "商品属性对象")
	@Data
	@NoArgsConstructor
	@AllArgsConstructor
	public static class GoodsProperty {

		@ApiModelProperty(value = "尺寸")
		private String size;

		@ApiModelProperty(value = "颜色")
		private String color;

		@ApiModelProperty(value = "材质")
		private String material;

		@ApiModelProperty(value = "图案")
		private String pattern;
	}

}

10.5.1.2 简单的商品信息: 封面

package com.edcode.commerce.goods;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 简单的商品信息: 封面
 */
@ApiModel(description = "简单的商品信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SimpleGoodsInfo {

	@ApiModelProperty(value = "商品表主键 id")
	private Long id;

	@ApiModelProperty(value = "商品名称")
	private String goodsName;

	@ApiModelProperty(value = "商品图片")
	private String goodsPic;

	@ApiModelProperty(value = "商品价格, 单位: 分")
	private Integer price;

	public SimpleGoodsInfo(Long id) {
		this.id = id;
	}
}

10.5.1.1 转换方法

package com.edcode.commerce.entity;

import java.util.Date;
import javax.persistence.*;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.BrandCategory;
import com.edcode.commerce.constant.GoodsCategory;
import com.edcode.commerce.constant.GoodsStatus;
import com.edcode.commerce.converter.BrandCategoryConverter;
import com.edcode.commerce.converter.GoodsCategoryConverter;
import com.edcode.commerce.converter.GoodsStatusConverter;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.goods.SimpleGoodsInfo;
import org.hibernate.annotations.GenericGenerator;
import java.io.Serializable;

import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;

/**
 * @description 商品表(t_scacommerce_goods)表实体类
 * @author eddie.lee
 * @date 2021-11-15 15:28:28
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "t_scacommerce_goods")
@EntityListeners(AuditingEntityListener.class) // 作用:自动更新时间, 需要配合 @EnableJpaAuditing 使用
public class ScaCommerceGoods implements Serializable {

	/**
	 * 自增主键
	 */
	@Id
	@Column(name = "id", nullable = false)
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * 商品类别
	 */
	@Column(name = "goods_category", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = GoodsCategoryConverter.class)
	private GoodsCategory goodsCategory;

	/**
	 * 品牌分类
	 */
	@Column(name = "brand_category", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = BrandCategoryConverter.class)
	private BrandCategory brandCategory;

	/**
	 * 商品名称
	 */
	@Column(name = "goods_name", nullable = false)
	private String goodsName;

	/**
	 * 商品图片
	 */
	@Column(name = "goods_pic", nullable = false)
	private String goodsPic;

	/**
	 * 商品描述信息
	 */
	@Column(name = "goods_description", nullable = false)
	private String goodsDescription;

	/**
	 * 商品状态
	 */
	@Column(name = "goods_status", nullable = false)
	// JPA有的,其实就是自动拆装箱的操作,可以转换类型存入DB
	@Convert(converter = GoodsStatusConverter.class)
	private GoodsStatus goodsStatus;

	/**
	 * 商品价格: 单位: 分、厘
	 */
	@Column(name = "price", nullable = false)
	private Integer price;

	/**
	 * 总供应量
	 */
	@Column(name = "supply", nullable = false)
	private Long supply;

	/**
	 * 库存
	 */
	@Column(name = "inventory", nullable = false)
	private Long inventory;

	/**
	 * 商品属性, json 字符串存储
	 */
	@Column(name = "goods_property", nullable = false)
	private String goodsProperty;

	/**
	 * 创建时间
	 */
	@CreatedDate
	@Column(name = "create_time", nullable = false)
	private Date createTime;

	/**
	 * 更新时间
	 */
	@LastModifiedDate
	@Column(name = "update_time", nullable = false)
	private Date updateTime;

	/**
	 * <h2>将 GoodsInfo 转成实体对象</h2>
	 * */
	public static ScaCommerceGoods to(GoodsInfo goodsInfo) {
		ScaCommerceGoods scaCommerceGoods = new ScaCommerceGoods();
		scaCommerceGoods.setGoodsCategory(GoodsCategory.of(goodsInfo.getGoodsCategory()));
		scaCommerceGoods.setBrandCategory(BrandCategory.of(goodsInfo.getBrandCategory()));
		scaCommerceGoods.setGoodsName(goodsInfo.getGoodsName());
		scaCommerceGoods.setGoodsPic(goodsInfo.getGoodsPic());
		scaCommerceGoods.setGoodsDescription(goodsInfo.getGoodsDescription());
		// TODO 可以增加一个审核的过程: 后续有空在搞
		scaCommerceGoods.setGoodsStatus(GoodsStatus.ONLINE);
		scaCommerceGoods.setPrice(goodsInfo.getPrice());
		scaCommerceGoods.setSupply(goodsInfo.getSupply());
		scaCommerceGoods.setInventory(goodsInfo.getSupply());
		scaCommerceGoods.setGoodsProperty(
				JSON.toJSONString(goodsInfo.getGoodsProperty())
		);

		return scaCommerceGoods;
	}

	/**
	 * <h2>将实体对象转成 GoodsInfo 对象</h2>
	 * */
	public GoodsInfo toGoodsInfo() {
		GoodsInfo goodsInfo = new GoodsInfo();
		goodsInfo.setId(this.id);
		goodsInfo.setGoodsCategory(this.goodsCategory.getCode());
		goodsInfo.setBrandCategory(this.brandCategory.getCode());
		goodsInfo.setGoodsName(this.goodsName);
		goodsInfo.setGoodsPic(this.goodsPic);
		goodsInfo.setGoodsDescription(this.goodsDescription);
		goodsInfo.setGoodsStatus(this.goodsStatus.getStatus());
		goodsInfo.setPrice(this.price);
		goodsInfo.setGoodsProperty(
				JSON.parseObject(this.goodsProperty, GoodsInfo.GoodsProperty.class)
		);
		goodsInfo.setSupply(this.supply);
		goodsInfo.setInventory(this.inventory);
		goodsInfo.setCreateTime(this.createTime);
		goodsInfo.setUpdateTime(this.updateTime);

		return goodsInfo;
	}

	/**
	 * <h2>将实体对象转成 SimpleGoodsInfo 对象</h2>
	 * */
	public SimpleGoodsInfo toSimple() {
		SimpleGoodsInfo goodsInfo = new SimpleGoodsInfo();
		goodsInfo.setId(this.id);
		goodsInfo.setGoodsName(this.goodsName);
		goodsInfo.setGoodsPic(this.goodsPic);
		goodsInfo.setPrice(this.price);
		return goodsInfo;
	}
}

10.6.1 异步任务与商品服务接口定义

10.6.1.1 创建分页商品信息对象

package com.edcode.commerce.vo;

import com.edcode.commerce.goods.SimpleGoodsInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 分页商品信息对象
 */
@ApiModel(description = "分页商品信息对象")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageSimpleGoodsInfo {

	@ApiModelProperty(value = "分页简单商品信息")
	private List<SimpleGoodsInfo> simpleGoodsInfos;

	@ApiModelProperty(value = "是否有更多的商品(分页)")
	private Boolean hasMore;
}

10.6.1.2 创建扣减商品库存对象

package com.edcode.commerce.goods;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 扣减商品库存对象
 */
@ApiModel(description = "扣减商品库存对象")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeductGoodsInventory {

    @ApiModelProperty(value = "商品主键 id")
    private Long goodsId;

    @ApiModelProperty(value = "扣减个数")
    private Integer count;
}

10.6.1.3 商品微服务相关服务接口定义

package com.edcode.commerce.service;

import com.edcode.commerce.common.TableId;
import com.edcode.commerce.goods.DeductGoodsInventory;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.goods.SimpleGoodsInfo;
import com.edcode.commerce.vo.PageSimpleGoodsInfo;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品微服务相关服务接口定义
 */
public interface IGoodsService {

	/**
	 * 根据 TableId 查询商品详细信息
	 * 
	 * @param tableId
	 * @return
	 */
	List<GoodsInfo> getGoodsInfoByTableId(TableId tableId); // GoodsInfo 是对外的 sdk 实体

	/**
	 * 获取分页的商品信息
	 * 
	 * @param page
	 * @return
	 */
	PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page);

	/**
	 * 根据 TableId 查询简单商品信息
	 * 
	 * @param tableId
	 * @return
	 */
	List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(TableId tableId);

	/**
	 * 扣减商品库存
	 * 
	 * @param deductGoodsInventories
	 * @return
	 */
	Boolean deductGoodsInventory(List<DeductGoodsInventory> deductGoodsInventories);
}

10.6.1.4 异步服务接口定义

package com.edcode.commerce.service.async;

import com.edcode.commerce.goods.GoodsInfo;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步服务接口定义
 */
public interface IAsyncService {

	/**
	 * 异步将商品信息保存下来
	 * 
	 * @param goodsInfos
	 * @param taskId
	 */
	void asyncImportGoods(List<GoodsInfo> goodsInfos, String taskId);
}

10.6.1.5 异步任务执行信息

package com.edcode.commerce.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务执行信息
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AsyncTaskInfo {

    /** 异步任务 id */
    private String taskId;

    /** 异步任务开始时间 */
    private Date startTime;

    /** 异步任务结束时间 */
    private Date endTime;

    /** 异步任务总耗时 */
    private String totalTime;

}

10.7.1 异步入库商品服务功能实现

10.7.1.1 自定义异步任务线程池, 异步任务异常捕获处理器, 邮件发送异常信息

package com.edcode.commerce.config;

import com.alibaba.fastjson.JSON;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.beans.factory.annotation.Value;

import java.lang.reflect.Method;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 自定义异步任务线程池, 异步任务异常捕获处理器
 */
@Slf4j
@EnableAsync    // 开启 Spring 异步任务支持
@Configuration
@RequiredArgsConstructor
public class AsyncPoolConfig implements AsyncConfigurer {

    private final JavaMailSender javaMailSender;

    @Value("${spring.mail.username}")
    private String username;

    /**
     * 将自定义的线程池注入到 Spring 容器中
     * */
    @Bean
    @Override
    public Executor getAsyncExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        // 核心线程数
        executor.setCorePoolSize(10);
        // 最大线程数
        executor.setMaxPoolSize(20);
        // 阻塞队列容量
        executor.setQueueCapacity(20);
        // 线程存活时间
        executor.setKeepAliveSeconds(60);
        // 这个非常重要,给线程起前缀的名称
        executor.setThreadNamePrefix("Qinyi-Async-");
        // 等待所有任务结果候再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // 设置线程池任务的等待时间,如果超过设置的时间,就会强制销毁线程池
        executor.setAwaitTerminationSeconds(60);
        // 定义拒绝策略
        executor.setRejectedExecutionHandler(
                // MaxPoolSize + QueueCapacity 满了, 就会执行拒绝策略
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
        // 初始化线程池, 初始化 core 线程
        executor.initialize();

        return executor;
    }

    /**
     * 指定系统中的异步任务在出现异常时使用到的处理器
     */
    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new AsyncExceptionHandler();
    }

    /**
     * 异步任务异常捕获处理器
     */
    @SuppressWarnings("all")
    class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

        @Override
        public void handleUncaughtException(Throwable throwable, Method method, Object... objects) {

            throwable.printStackTrace();

            log.error("Async Error: [{}], Method: [{}], Param: [{}]",
                    throwable.getMessage(),
                    method.getName(),
                    JSON.toJSONString(objects));

            // 发送邮件或者是短信, 做进一步的报警处理
            // 建立邮箱消息
            SimpleMailMessage message = new SimpleMailMessage();
            // 发送者
            message.setFrom(username);
            // 接收者
            message.setTo(username);
            // 发送标题
            message.setSubject("Async Error");
            // 发送内容
            message.setText("Async Error: " + throwable.getMessage() + " Method: " + method.getName() + " Param:" + JSON.toJSONString(objects));
            // go
            javaMailSender.send(message);
        }
    }
}

10.7.1.2 商品常量信息 - Redis key

package com.edcode.commerce.constant;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品常量信息
 */
public class GoodsConstant {

	/**
	 * redis key
	 */
	public static final String SCACOMMERCE_GOODS_DICT_KEY = "scacommerce:goods:dict:20211115";
}

10.7.1.3 异步服务接口实现

package com.edcode.commerce.service.async;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.GoodsConstant;
import com.edcode.commerce.dao.ScaCommerceGoodsDao;
import com.edcode.commerce.entity.ScaCommerceGoods;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.goods.SimpleGoodsInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步服务接口实现
 */
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class AsyncServiceImpl implements  IAsyncService{

    private final ScaCommerceGoodsDao scaCommerceGoodsDao;

    private final StringRedisTemplate redisTemplate;

    /**
     * 异步任务需要加上注解, 并指定使用的线程池
     * 异步任务处理两件事:
     *  1. 将商品信息保存到数据表
     *  2. 更新商品缓存
     * */
    @Async("getAsyncExecutor")
    @Override
    public void asyncImportGoods(List<GoodsInfo> goodsInfos, String taskId) {

        log.info("运行taskId的异步任务: [{}]", taskId);

        // StopWatch可以方便记录运行时间,主要用于单线程,单位为ms级,常用于日志记录运行时间
        // https://blog.csdn.net/weixin_45910779/article/details/119035003?spm=1001.2101.3001.6650.16&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-16.no_search_link&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromBaidu%7Edefault-16.no_search_link
        StopWatch watch = StopWatch.createStarted();

        // 1. 如果是 goodsInfo 中存在重复的商品, 不保存; 直接返回, 记录错误日志
        // 请求数据是否合法的标记
        boolean isIllegal = false;

        // 将商品信息字段 joint 在一起, 用来判断是否存在重复
        Set<String> goodsJointInfos = new HashSet<>(goodsInfos.size());
        // 过滤出来的, 可以入库的商品信息(规则按照自己的业务需求自定义即可)
        List<GoodsInfo> filteredGoodsInfo = new ArrayList<>(goodsInfos.size());

        // 走一遍循环, 过滤非法参数与判定当前请求是否合法
        for (GoodsInfo goods : goodsInfos) {

            // 基本条件不满足的, 直接过滤器
            if (goods.getPrice() <= 0 || goods.getSupply() <= 0) {
                log.info("商品信息无效: [{}]", JSON.toJSONString(goods));
                continue;
            }

            // 组合商品信息(唯一性索引)
            String jointInfo = String.format(
                    "%s,%s,%s",
                    goods.getGoodsCategory(),
                    goods.getBrandCategory(),
                    goods.getGoodsName()
            );

            // 是否包含商品信息
            if (goodsJointInfos.contains(jointInfo)) {
                isIllegal = true;
            }

            // 加入到两个容器中
            goodsJointInfos.add(jointInfo);
            filteredGoodsInfo.add(goods);
        }

        // 如果存在重复商品或者是没有需要入库的商品, 直接打印日志返回
        if (isIllegal || CollectionUtils.isEmpty(filteredGoodsInfo)) {
            watch.stop();
            log.warn("重复商品: [{}]", JSON.toJSONString(filteredGoodsInfo));
            log.info("检查和入库的商品完成: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS));
            return;
        }

        List<ScaCommerceGoods> scaCommerceGoods = filteredGoodsInfo.stream()
                .map(ScaCommerceGoods::to)
                .collect(Collectors.toList());

        List<ScaCommerceGoods> targetGoods = new ArrayList<>(scaCommerceGoods.size());

        // 2. 保存 goodsInfo 之前先判断下是否存在重复商品
        scaCommerceGoods.forEach(g -> {
            // limit 1
            if (null != scaCommerceGoodsDao.findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName(
                            g.getGoodsCategory(),
                            g.getBrandCategory(),
                            g.getGoodsName()
                    ).orElse(null)) {
                // 查询到重覆商品直接过滤掉
                return;
            }
            targetGoods.add(g);
        });

        // 商品信息入库
        List<ScaCommerceGoods> savedGoods = IterableUtils.toList(
                // 返回的是 Iterable 的迭代,所以需要 toList一次
                scaCommerceGoodsDao.saveAll(targetGoods)
        );
        // TODO 将入库商品信息同步到 Redis 中
        saveNewGoodsInfoToRedis(savedGoods);

        log.info("将商品信息保存到db和redis: [{}]", savedGoods.size());

        watch.stop();
        log.info("检查和入库商品完成: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS));
    }

    /**
     * 将保存到数据表中的数据缓存到 Redis 中
     * dict: key -> <id, SimpleGoodsInfo(json)>
     */
    private void saveNewGoodsInfoToRedis(List<ScaCommerceGoods> savedGoods) {

        // 由于 Redis 是内存存储, 只存储简单商品信息
        List<SimpleGoodsInfo> simpleGoodsInfos = savedGoods.stream()
//                .map(e -> e.toSimple())
                .map(ScaCommerceGoods::toSimple)
                .collect(Collectors.toList());

        Map<String, String> id2JsonObject = new HashMap<>(simpleGoodsInfos.size());
        simpleGoodsInfos.forEach(
                g -> id2JsonObject.put(g.getId().toString(), JSON.toJSONString(g))
        );

        // 保存到 Redis 中
        redisTemplate.opsForHash().putAll(
                GoodsConstant.SCACOMMERCE_GOODS_DICT_KEY,
                id2JsonObject
        );
    }

}

@Async(“getAsyncExecutor”) 指定 asyncImportGoods() 使用线程池

10.7.1.4 面试常问到的线程池关键属性

核心线程数 - CorePoolSize
最大线程数 - MaxPoolSize
阻塞队列容量 - QueueCapacity
线程存活时间 - KeepAliveSeconds
这个非常重要,给线程起前缀的名称 - ThreadNamePrefix
等待所有任务结果候再关闭线程池 - WaitForTasksToCompleteOnShutdown
设置线程池任务的等待时间,如果超过设置的时间,就会强制销毁线程池 - AwaitTerminationSeconds
定义拒绝策略 - RejectedExecutionHandler
初始化线程池 - initialize

10.8.1 异步任务执行管理器

10.8.1.1 异步任务状态枚举

package com.edcode.commerce.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务状态枚举
 */
@Getter
@AllArgsConstructor
public enum AsyncTaskStatusEnum {

	STARTED(0, "已经启动"),
    RUNNING(1, "正在运行"),
    SUCCESS(2, "执行成功"),
    FAILED(3, "执行失败"),

    ;

	/**
     * 执行状态编码
     */
	private final int state;

	/**
     * 执行状态描述
     */
	private final String stateInfo;
}

10.8.1.2 异步任务执行信息

package com.edcode.commerce.vo;

import com.edcode.commerce.constant.AsyncTaskStatusEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务执行信息
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AsyncTaskInfo {

	/**
	 * 异步任务 id
	 */
	private String taskId;

	/**
	 * 异步任务开始时间
	 */
	private Date startTime;

	/**
	 * 异步任务结束时间
	 */
	private Date endTime;

	/**
	 * 异步任务总耗时
	 */
	private String totalTime;

	/**
	 * 异步任务执行状态
	 */
	private AsyncTaskStatusEnum status;

}

10.8.1.3 异步任务执行管理器

对异步任务进行包装管理, 记录并塞入异步任务执行信息

package com.edcode.commerce.service.async;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.edcode.commerce.constant.AsyncTaskConstant;
import com.edcode.commerce.constant.AsyncTaskStatusEnum;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.vo.AsyncTaskInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务执行管理器
 *
 *              对异步任务进行包装管理, 记录并塞入异步任务执行信息
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncTaskManager {

    private final Map<String, AsyncTaskInfo> taskContainer = new HashMap<>(16);

    private final IAsyncService asyncService;

    /**
     * 初始化异步任务
     */
    public AsyncTaskInfo initTask() {

        AsyncTaskInfo taskInfo = new AsyncTaskInfo();
        // 设置一个唯一的异步任务 id, 只要唯一即可
        taskInfo.setTaskId(UUID.randomUUID().toString());
        taskInfo.setStatus(AsyncTaskStatusEnum.STARTED);
        taskInfo.setStartTime(new Date());

        // 初始化的时候就要把异步任务执行信息放入到存储容器中
        taskContainer.put(taskInfo.getTaskId(), taskInfo);
        return taskInfo;
    }

    /**
     * 提交异步任务
     */
    public AsyncTaskInfo submit(List<GoodsInfo> goodsInfos) {

        // 初始化一个异步任务的监控信息
        AsyncTaskInfo taskInfo = initTask();
        asyncService.asyncImportGoods(goodsInfos, taskInfo.getTaskId());
        return taskInfo;
    }

    /**
     * 设置异步任务执行状态信息
     */
    public void setTaskInfo(AsyncTaskInfo taskInfo) {
        taskContainer.put(taskInfo.getTaskId(), taskInfo);
    }
    /**
     * 获取异步任务执行状态信息
     */
    public AsyncTaskInfo getTaskInfo(String taskId) {
        return taskContainer.get(taskId);
    }
}

10.9.1 异步任务执行监控切面

10.9.1.1 AOP实现异步任务监控

package com.edcode.commerce.service.async;

import com.edcode.commerce.constant.AsyncTaskStatusEnum;
import com.edcode.commerce.vo.AsyncTaskInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务执行监控切面
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class AsyncTaskMonitor {

    /** 注入异步任务管理器 */
    private final AsyncTaskManager asyncTaskManager;

    /**
     * 异步任务执行的环绕切面
     *
     * 环绕切面让我们可以在方法执行之前和执行之后做一些 "额外" 的操作
     *
     */
    @Around("execution(* com.edcode.commerce.service.async.AsyncServiceImpl.*(..))")
    public Object taskHandle(ProceedingJoinPoint proceedingJoinPoint) {

        // 获取 taskId, 调用异步任务传入的第二个参数
        String taskId = proceedingJoinPoint.getArgs()[1].toString();

        // 获取任务信息, 在提交任务的时候就已经放入到容器中了
        AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId);
        log.info("AsyncTaskMonitor正在监视异步任务: [{}]", taskId);

        taskInfo.setStatus(AsyncTaskStatusEnum.RUNNING);
        asyncTaskManager.setTaskInfo(taskInfo); // 设置为运行状态, 并重新放入容器

        AsyncTaskStatusEnum status;
        Object result;

        try {
            // 执行异步任务
            result = proceedingJoinPoint.proceed();
            status = AsyncTaskStatusEnum.SUCCESS;
        } catch (Throwable ex) {
            // 异步任务出现了异常
            result = null;
            status = AsyncTaskStatusEnum.FAILED;
            log.error("AsyncTaskMonitor:异步任务 [{}] 失败,错误信息: [{}]", taskId, ex.getMessage(), ex);
        }

        // 设置异步任务其他的信息, 再次重新放入到容器中
        taskInfo.setEndTime(new Date());
        taskInfo.setStatus(status);
        taskInfo.setTotalTime(String.valueOf(
                taskInfo.getEndTime().getTime() - taskInfo.getStartTime().getTime()
        ));
        asyncTaskManager.setTaskInfo(taskInfo);

        return result;
    }
}

AsyncTaskInfo、AsyncTaskManager、AsyncTaskMonitor

10.10.1 商品相关服务接口实现

10.10.1.1 商品微服务相关服务功能实现

package com.edcode.commerce.service.impl;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.common.TableId;
import com.edcode.commerce.constant.GoodsConstant;
import com.edcode.commerce.dao.ScaCommerceGoodsDao;
import com.edcode.commerce.entity.ScaCommerceGoods;
import com.edcode.commerce.goods.DeductGoodsInventory;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.goods.SimpleGoodsInfo;
import com.edcode.commerce.service.IGoodsService;
import com.edcode.commerce.vo.PageSimpleGoodsInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品微服务相关服务功能实现
 */
@Slf4j
@Service
@Transactional(rollbackFor = Exception.class)
@RequiredArgsConstructor
public class GoodsServiceImpl implements IGoodsService {

    private final StringRedisTemplate redisTemplate;

    private final ScaCommerceGoodsDao scaCommerceGoodsDao;

    /**
     * 根据 TableId 查询商品详细信息
     */
    @Override
    public List<GoodsInfo> getGoodsInfoByTableId(TableId tableId) {

//        List<Long> idLong = new ArrayList<>();
//        List<TableId.Id> idIds = tableId.getIds();
//        for (TableId.Id tid: idIds) {
//            idLong.add(tid.getId());
//        }
//        log.info("通过 ids 获取商品信息: [{}]", JSON.toJSONString(idLong));

        // 详细的商品信息, 不能从 redis cache 中去拿
        List<Long> ids = tableId.getIds().stream()
//                .map(e->e.getId())
                .map(TableId.Id::getId)
                .collect(Collectors.toList());
        log.info("通过 ids 获取商品信息: [{}]", JSON.toJSONString(ids));

        // findAllById 返回类型:Iterable, 所以使用 IterableUtils.toList 转换 List
        List<ScaCommerceGoods> scaCommerceGoods = IterableUtils.toList(
                // 查数据库
                scaCommerceGoodsDao.findAllById(ids)
        );

        return scaCommerceGoods.stream()
//                .map(e -> e.toGoodsInfo())
                .map(ScaCommerceGoods::toGoodsInfo)
                .collect(Collectors.toList());
    }

    /**
     * 获取分页的商品信息
     */
    @Override
    public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page) {

        // 分页不能从 redis cache 中去拿
        if (page <= 1) {
            page = 1;   // 默认是第一页
        }

        // 这里分页的规则(你可以自由修改): 1页10调数据, 按照 id 倒序排列
        Pageable pageable = PageRequest.of(
                page - 1, 10, Sort.by("id").descending()
        );
        Page<ScaCommerceGoods> orderPage = scaCommerceGoodsDao.findAll(pageable);

        // 是否还有更多页: 总页数是否大于当前给定的页
        boolean hasMore = orderPage.getTotalPages() > page;

        return new PageSimpleGoodsInfo(
                orderPage.getContent().stream()
                        .map(ScaCommerceGoods::toSimple)
                        .collect(Collectors.toList()),
                hasMore
        );
    }

    /**
     * 根据 TableId 查询简单商品信息
     */
    @Override
    public List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(TableId tableId) {
        // 获取商品的简单信息, 可以从 redis cache 中去拿, 拿不到需要从 DB 中获取并保存到 Redis 里面
        // Redis 中的 KV 都是字符串类型
        List<Object> goodIds = tableId.getIds().stream()
                .map(i -> i.getId().toString()).collect(Collectors.toList());

        // FIXME 如果 cache 中查不到 goodsId 对应的数据, 返回的是 null, [null, null]
        List<Object> cachedSimpleGoodsInfos = redisTemplate.opsForHash()
                .multiGet(GoodsConstant.SCACOMMERCE_GOODS_DICT_KEY, goodIds)
                .stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toList());

        // 如果从 Redis 中查到了商品信息, 分两种情况去操作
        if (CollectionUtils.isNotEmpty(cachedSimpleGoodsInfos)) {
            // 1. 如果从缓存中查询出所有需要的 SimpleGoodsInfo
            if (cachedSimpleGoodsInfos.size() == goodIds.size()) {
                log.info("通过ID获取简单商品信息(从缓存): [{}]",
                        JSON.toJSONString(goodIds));
                return parseCachedGoodsInfo(cachedSimpleGoodsInfos);
            } else {
                // 2. 一半从数据表中获取 (right), 一半从 redis cache 中获取 (left)
                List<SimpleGoodsInfo> left = parseCachedGoodsInfo(cachedSimpleGoodsInfos);
                // 取差集: 传递进来的参数 - 缓存中查到的 = 缓存中没有的
                Collection<Long> subtractIds = CollectionUtils.subtract(
                        goodIds.stream()
                                .map(g -> Long.valueOf(g.toString()))
                                .collect(Collectors.toList()),
                        left.stream()
                                .map(SimpleGoodsInfo::getId)
                                .collect(Collectors.toList())
                );

                // 缓存中没有的, 查询数据表并缓存
                List<SimpleGoodsInfo> right = queryGoodsFromDBAndCacheToRedis(
                        new TableId(subtractIds.stream()
                                .map(TableId.Id::new)
                                .collect(Collectors.toList())
                        )
                );
                // 合并 left 和 right 并返回
                log.info("通过ID(从数据库和缓存)获取简单商品信息: [{}]", JSON.toJSONString(subtractIds));
                return new ArrayList<>(CollectionUtils.union(left, right));
            }
        } else {
            // 从 redis 里面什么都没有查到
            return queryGoodsFromDBAndCacheToRedis(tableId);
        }
    }

    /**
     * 将缓存中的数据反序列化成 Java Pojo 对象
     * */
    private List<SimpleGoodsInfo> parseCachedGoodsInfo(List<Object> cachedSimpleGoodsInfo) {

        return cachedSimpleGoodsInfo.stream()
                .map(s -> JSON.parseObject(s.toString(), SimpleGoodsInfo.class))
                .collect(Collectors.toList());
    }

    /**
     * 从数据表中查询数据, 并缓存到 Redis 中
     * */
    private List<SimpleGoodsInfo> queryGoodsFromDBAndCacheToRedis(TableId tableId) {

        // 从数据表中查询数据并做转换
        List<Long> ids = tableId.getIds().stream()
                .map(TableId.Id::getId).collect(Collectors.toList());
        log.info("通过ID(从数据库)获取简单商品信息: [{}]",
                JSON.toJSONString(ids));
        List<ScaCommerceGoods> scaCommerceGoods = IterableUtils.toList(
                scaCommerceGoodsDao.findAllById(ids)
        );
        List<SimpleGoodsInfo> result = scaCommerceGoods.stream()
                .map(ScaCommerceGoods::toSimple).collect(Collectors.toList());
        // 将结果缓存, 下一次可以直接从 redis cache 中查询
        log.info("缓存商品信息: [{}]", JSON.toJSONString(ids));

        Map<String, String> id2JsonObject = new HashMap<>(result.size());
        result.forEach(g -> id2JsonObject.put(
                g.getId().toString(), JSON.toJSONString(g)
        ));
        // 保存到 Redis 中
        redisTemplate.opsForHash().putAll(
                GoodsConstant.SCACOMMERCE_GOODS_DICT_KEY, id2JsonObject);
        return result;
    }

    @Override
    public Boolean deductGoodsInventory(List<DeductGoodsInventory> deductGoodsInventories) {

        // 检验下参数是否合法
        deductGoodsInventories.forEach(d -> {
            if (d.getCount() <= 0) {
                throw new RuntimeException("purchase goods count need > 0");
            }
        });

        List<ScaCommerceGoods> scaCommerceGoods = IterableUtils.toList(
                scaCommerceGoodsDao.findAllById(
                        deductGoodsInventories.stream()
//                                .map(DeductGoodsInventory::getGoodsId)
                                .map(e -> e.getGoodsId())
                                .collect(Collectors.toList())
                )
        );
        // 根据传递的 goodsIds 查询不到商品对象, 抛异常
        if (CollectionUtils.isEmpty(scaCommerceGoods)) {
            throw new RuntimeException("未按要求找到任何商品");
        }
        // 查询出来的商品数量与传递的不一致, 抛异常
        if (scaCommerceGoods.size() != deductGoodsInventories.size()) {
            throw new RuntimeException("请求无效");
        }
        // goodsId -> DeductGoodsInventory
        Map<Long, DeductGoodsInventory> goodsId2Inventory =
                deductGoodsInventories.stream().collect(
                        // 使用Stream时,要将它转换成其他容器或Map。这时候,就会使用到Function.identity()
                        Collectors.toMap(DeductGoodsInventory::getGoodsId, Function.identity())
                );

        // 检查是不是可以扣减库存, 再去扣减库存
        scaCommerceGoods.forEach(g -> {
            // 当前库存
            Long currentInventory = g.getInventory();
            // 需要扣除存货
            Integer needDeductInventory = goodsId2Inventory.get(g.getId()).getCount();
            if (currentInventory < needDeductInventory) {
                log.error("商品库存不足: [{}], [{}]", currentInventory, needDeductInventory);
                throw new RuntimeException("商品库存不足: " + g.getId());
            }
            // 扣减库存
            g.setInventory(currentInventory - needDeductInventory);
            log.info("扣除存货: [{}], [{}], [{}]", g.getId(), currentInventory, g.getInventory());
        });

        scaCommerceGoodsDao.saveAll(scaCommerceGoods);
        log.info("扣除已完成的商品库存");

        return true;
    }
}

10.13.1 异步入库商品功能可用性验证

Nacos上添加动态 goods-service 路由

在这里插入图片描述

源码文件在 gateway 里面 sca-commerce-gateway-router.json

10.13.1.1 异步任务执行管理器 - 存储方式 Redis

package com.edcode.commerce.service.async;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.edcode.commerce.constant.AsyncTaskConstant;
import com.edcode.commerce.constant.AsyncTaskStatusEnum;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.vo.AsyncTaskInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务执行管理器
 *
 *              对异步任务进行包装管理, 记录并塞入异步任务执行信息
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class AsyncTaskManager {

    private final Map<String, AsyncTaskInfo> taskContainer = new HashMap<>(16);

    private final IAsyncService asyncService;

    public final StringRedisTemplate stringRedisTemplate;

    /**
     * 初始化异步任务
     */
    public AsyncTaskInfo initTask() {

        AsyncTaskInfo taskInfo = new AsyncTaskInfo();
        // 设置一个唯一的异步任务 id, 只要唯一即可
        taskInfo.setTaskId(UUID.randomUUID().toString());
        taskInfo.setStatus(AsyncTaskStatusEnum.STARTED);
        taskInfo.setStartTime(new Date());

        // 初始化的时候就要把异步任务执行信息放入到存储容器中
//        taskContainer.put(taskInfo.getTaskId(), taskInfo);

        stringRedisTemplate.opsForValue().set(AsyncTaskConstant.SCACOMMERCE_ASYNC_TASK_KEY + taskInfo.getTaskId(), JSON.toJSONString(taskInfo), 3600, TimeUnit.SECONDS);

        return taskInfo;
    }

    /**
     * 提交异步任务
     */
    public AsyncTaskInfo submit(List<GoodsInfo> goodsInfos) {

        // 初始化一个异步任务的监控信息
        AsyncTaskInfo taskInfo = initTask();
        asyncService.asyncImportGoods(goodsInfos, taskInfo.getTaskId());
        return taskInfo;
    }

    /**
     * 设置异步任务执行状态信息
     */
    public void setTaskInfo(AsyncTaskInfo taskInfo) {
//        taskContainer.put(taskInfo.getTaskId(), taskInfo);

        stringRedisTemplate.opsForValue().set(AsyncTaskConstant.SCACOMMERCE_ASYNC_TASK_KEY + taskInfo.getTaskId(), JSON.toJSONString(taskInfo), 3600, TimeUnit.SECONDS);
    }
    /**
     * 获取异步任务执行状态信息
     */
    public AsyncTaskInfo getTaskInfo(String taskId) {
        return JSON.parseObject(
                stringRedisTemplate.opsForValue().get(AsyncTaskConstant.SCACOMMERCE_ASYNC_TASK_KEY + taskId),
                AsyncTaskInfo.class
        );
        //        return taskContainer.get(taskId);
    }
}

10.13.1.2 ScaCommerceGoods 实体类里面 to 方法

需要留意的地方

scaCommerceGoods.setSupply(goodsInfo.getSupply());
scaCommerceGoods.setInventory(goodsInfo.getInventory());

// 总供应量、 库存 初始化时候应该一样
scaCommerceGoods.setSupply(goodsInfo.getSupply());
scaCommerceGoods.setInventory(goodsInfo.getSupply());

10.13.1.3 异步任务服务对外提供的 API

package com.edcode.commerce.controller;

import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.service.async.AsyncTaskManager;
import com.edcode.commerce.vo.AsyncTaskInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import lombok.RequiredArgsConstructor;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 异步任务服务对外提供的 API
 */
@Api(tags = "商品异步入库服务")
@Slf4j
@RestController
@RequestMapping("/async-goods")
@RequiredArgsConstructor
public class AsyncGoodsController {

    /**
     * 异步任务执行管理器
     */
    private final AsyncTaskManager asyncTaskManager;

    @ApiOperation(value = "导入商品", notes = "导入商品进入到商品表", httpMethod = "POST")
    @PostMapping("/import-goods")
    public AsyncTaskInfo importGoods(@RequestBody List<GoodsInfo> goodsInfos) {
        // 提交异步任务
        return asyncTaskManager.submit(goodsInfos);
    }

    @ApiOperation(value = "查询状态", notes = "查询异步任务的执行状态", httpMethod = "GET")
    @GetMapping("/task-info")
    public AsyncTaskInfo getTaskInfo(@RequestParam String taskId) {
        // 获取异步任务执行状态信息
        return asyncTaskManager.getTaskInfo(taskId);
    }

}

10.13.1.4 创建 http 请求测试

goods-async.http

### 导入商品
POST http://127.0.0.1:9001/edcode/scacommerce-goods-service/async-goods/import-goods
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiZDM0Y2I2MjYtMGUzNS00YTFiLTllZjgtNTY1NzBhZWZhNzFkIiwiZXhwIjoxNjM3MTY0ODAwfQ.Dn-8b0rhvEAODGQDrDiNY1XXs7vUvYkOQPO7gYq_2ul7NCYSWqxSxpXjFDntY1hjDA6MYa-Xtsz0JN8Pt4bHZmkpTY4NPv6WPeVoo9EORzI8zxQB67C47HY13r-BQszMTaISTbsBo2jpj1nOAdI-p3jQ3k8R0ixeujkwHCYuF_ne9KEQtqpLBqjnsrYqVgL0ZE93n6k-DkWuM7O58miPO7ArIJpFccx-a3IABs11uF4TSJFWg0DkzX5XjpoCOMdXLBa8lhHS4vBoEgoExq9HBQxqTo-Slnlglx-NL7yM3Uz_ga5D6HzgYOtF3d0hmDfuexQ2otKOtlH2J9mmPtcavQ

[
  {
    "goodsCategory": "10001",
    "brandCategory": "20001",
    "goodsName": "iphone 11",
    "goodsPic": "",
    "goodsDescription": "苹果手机",
    "price": 100000,
    "supply": 2000000,
    "goodsProperty": {
      "size": "12cm * 6.5cm",
      "color": "绿色",
      "material": "金属机身",
      "pattern": "纯色"
    }
  },
  {
    "goodsCategory": "10001",
    "brandCategory": "20001",
    "goodsName": "iphone 12",
    "goodsPic": "",
    "goodsDescription": "苹果手机",
    "price": 150000,
    "supply": 2000000,
    "goodsProperty": {
      "size": "12cm * 6.5cm",
      "color": "绿色",
      "material": "金属机身",
      "pattern": "纯色"
    }
  },
  {
    "goodsCategory": "10001",
    "brandCategory": "20001",
    "goodsName": "iphone 13",
    "goodsPic": "",
    "goodsDescription": "苹果手机",
    "price": 160000,
    "supply": 2000000,
    "goodsProperty": {
      "size": "12cm * 6.5cm",
      "color": "绿色",
      "material": "金属机身",
      "pattern": "纯色"
    }
  }
]


### 查询导入商品状态
GET http://127.0.0.1:9001/edcode/scacommerce-goods-service/async-goods/task-info?taskId=8525a2fc-b87c-4134-a93b-a08421cff387
Accept: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiZDM0Y2I2MjYtMGUzNS00YTFiLTllZjgtNTY1NzBhZWZhNzFkIiwiZXhwIjoxNjM3MTY0ODAwfQ.Dn-8b0rhvEAODGQDrDiNY1XXs7vUvYkOQPO7gYq_2ul7NCYSWqxSxpXjFDntY1hjDA6MYa-Xtsz0JN8Pt4bHZmkpTY4NPv6WPeVoo9EORzI8zxQB67C47HY13r-BQszMTaISTbsBo2jpj1nOAdI-p3jQ3k8R0ixeujkwHCYuF_ne9KEQtqpLBqjnsrYqVgL0ZE93n6k-DkWuM7O58miPO7ArIJpFccx-a3IABs11uF4TSJFWg0DkzX5XjpoCOMdXLBa8lhHS4vBoEgoExq9HBQxqTo-Slnlglx-NL7yM3Uz_ga5D6HzgYOtF3d0hmDfuexQ2otKOtlH2J9mmPtcavQ

###

sca-commerce-user 的 token 需要请求 login.http 的登录方法获取最新 token

查询导入商品状态 里面 taskId 是根据“导入商品”返回的 taskId 赋值 <?taskId=8525a2fc-b87c-4134-a93b-a08421cff387>

如果出现线程池错误会发送邮件

在这里插入图片描述

10.14.1 商品服务接口可用性测试

10.14.1.1 商品微服务功能测试

package com.edcode.commerce.service;

import com.alibaba.fastjson.JSON;
import com.edcode.commerce.common.TableId;
import com.edcode.commerce.goods.DeductGoodsInventory;
import lombok.extern.slf4j.Slf4j;
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 java.util.stream.Collectors;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品微服务功能测试
 */
@Slf4j
@SpringBootTest
@RunWith(SpringRunner.class)
public class GoodsServiceTest {

    @Autowired
    private IGoodsService goodsService;

    @Test
    public void testGetGoodsInfoByTableId() {

        List<Long> ids = Arrays.asList(1L, 2L, 3L);
        List<TableId.Id> tIds = ids.stream()
                .map(TableId.Id::new)
                .collect(Collectors.toList());

        List<TableId.Id> tIds2 = new ArrayList<>();
        for (Long id : ids) {
            TableId.Id tid1 = new TableId.Id();
            tid1.setId(id);
            tIds2.add(tid1);
        }

        log.info("JDK8 - 测试按表id获取商品信息: [{}]",
                JSON.toJSONString(
                        goodsService.getGoodsInfoByTableId(new TableId(tIds))
                )
        );

        log.info("JDK7 - 测试按表id获取商品信息: [{}]",
                JSON.toJSONString(
                        goodsService.getGoodsInfoByTableId(new TableId(tIds2))
                )
        );
    }

    @Test
    public void testGetSimpleGoodsInfoByPage() {
        log.info("测试通过页面获取简单商品信息: [{}]", JSON.toJSONString(
                goodsService.getSimpleGoodsInfoByPage(1)
        ));
    }

    @Test
    public void testGetSimpleGoodsInfoByTableId() {
        List<Long> ids = Arrays.asList(1L, 2L, 3L);
        List<TableId.Id> tIds = ids.stream().map(TableId.Id::new).collect(Collectors.toList());
        log.info("测试通过表id获取简单商品信息: [{}]", JSON.toJSONString(
                goodsService.getSimpleGoodsInfoByTableId(new TableId(tIds))
        ));
    }

    @Test
    public void testDeductGoodsInventory() {
        List<DeductGoodsInventory> deductGoodsInventories = Arrays.asList(
                new DeductGoodsInventory(1L, 100),
                new DeductGoodsInventory(2L, 66)
        );
        log.info("测试扣除商品库存: [{}]",
                goodsService.deductGoodsInventory(deductGoodsInventories)
        );
    }
}

记得一下内部类 lambda表达式 与 jdk1.7 写法区别

在这里插入图片描述

lambda 表达式是方便快捷,但没有基础的人会比较吃力

10.15.1 商品微服务对外HTTP接口

10.15.1.1 商品微服务对外暴露的功能服务 API 接口

package com.edcode.commerce.controller;

import com.edcode.commerce.common.TableId;
import com.edcode.commerce.goods.DeductGoodsInventory;
import com.edcode.commerce.goods.GoodsInfo;
import com.edcode.commerce.goods.SimpleGoodsInfo;
import com.edcode.commerce.service.IGoodsService;
import com.edcode.commerce.vo.PageSimpleGoodsInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @author eddie.lee
 * @blog blog.eddilee.cn
 * @description 商品微服务对外暴露的功能服务 API 接口
 */
@Api(tags = "商品微服务功能接口")
@Slf4j
@RestController
@RequestMapping("/goods")
@RequiredArgsConstructor
public class GoodsController {

	private final IGoodsService goodsService;

	@ApiOperation(value = "详细商品信息", notes = "根据 TableId 查询详细商品信息", httpMethod = "POST")
	@PostMapping("/goods-info")
	public List<GoodsInfo> getGoodsInfoByTableId(@RequestBody TableId tableId) {
		return goodsService.getGoodsInfoByTableId(tableId);
	}

	@ApiOperation(value = "简单商品信息", notes = "获取分页的简单商品信息", httpMethod = "GET")
	@GetMapping("/page-simple-goods-info")
	public PageSimpleGoodsInfo getSimpleGoodsInfoByPage(@RequestParam(required = false, defaultValue = "1") int page) {
		return goodsService.getSimpleGoodsInfoByPage(page);
	}

	@ApiOperation(value = "简单商品信息", notes = "根据 TableId 查询简单商品信息", httpMethod = "POST")
	@PostMapping("/simple-goods-info")
	public List<SimpleGoodsInfo> getSimpleGoodsInfoByTableId(@RequestBody TableId tableId) {
		return goodsService.getSimpleGoodsInfoByTableId(tableId);
	}

	@ApiOperation(value = "扣减商品库存", notes = "扣减商品库存", httpMethod = "PUT")
	@PutMapping("/deduct-goods-inventory")
	public Boolean deductGoodsInventory(@RequestBody List<DeductGoodsInventory> deductGoodsInventories) {
		return goodsService.deductGoodsInventory(deductGoodsInventories);
	}
}

10.16.1 验证商品微服务功能可用性

10.16.1.1 goods-goods.http

### 登录
POST 127.0.0.1:9001/edcode/sca-commerce/login
Content-Type: application/json

{
  "username": "eddie@qq.com",
  "password": "25d55ad283aa400af464c76d713c07ad"
}


### 根据 TableId 查询详细商品信息
POST http://127.0.0.1:9001/edcode/scacommerce-goods-service/goods/goods-info
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiYTdhMjU4OTUtZmRjYi00ZWQ3LTg2MDEtZGMzZmY5Y2FlMDg4IiwiZXhwIjoxNjM3MTY0ODAwfQ.ihMZ14UdfTPMPyln8zOyP-_pM3X-VySZF6b0YKkbLmgEGbqc6zVS8E1NxqQ_XfSIaqbCb0a6GQnviBi1tEEwZL7U7Yyq4SsvuA4iGWpz2bgd-DS0qyCFvCxMeH6Q6yphYmej4IGF7EJ_EGIa0RfSuySa_tXrcKK6Xv5B0x4kkuWu57E61j3e1q1gqq_Ozn8NuB-iTzvxcepjczmLLkcuMqJ8EDHMd8j54CzUCbHb8it_2bZ6DnOc7pP2aZBxl7HaE7eo-LoaO_4QECuX2PcvGEe34hjTkT6_ahRBiWZZqxb15wjkTfP-rwHwFuCqHx0Ysou_4l766PxXVkP35lQlpQ

{
  "ids": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}


### 根据分页查询简单商品信息
GET http://127.0.0.1:9001/edcode/scacommerce-goods-service/goods/page-simple-goods-info?page=1
Accept: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiYTdhMjU4OTUtZmRjYi00ZWQ3LTg2MDEtZGMzZmY5Y2FlMDg4IiwiZXhwIjoxNjM3MTY0ODAwfQ.ihMZ14UdfTPMPyln8zOyP-_pM3X-VySZF6b0YKkbLmgEGbqc6zVS8E1NxqQ_XfSIaqbCb0a6GQnviBi1tEEwZL7U7Yyq4SsvuA4iGWpz2bgd-DS0qyCFvCxMeH6Q6yphYmej4IGF7EJ_EGIa0RfSuySa_tXrcKK6Xv5B0x4kkuWu57E61j3e1q1gqq_Ozn8NuB-iTzvxcepjczmLLkcuMqJ8EDHMd8j54CzUCbHb8it_2bZ6DnOc7pP2aZBxl7HaE7eo-LoaO_4QECuX2PcvGEe34hjTkT6_ahRBiWZZqxb15wjkTfP-rwHwFuCqHx0Ysou_4l766PxXVkP35lQlpQ


### 根据 TableId 查询简单商品信息: 完整的 goods cache
### 第二步验证, 删掉 cache
### 第三步验证, 删除 cache 中其中一个商品
POST http://127.0.0.1:9001/edcode/scacommerce-goods-service/goods/simple-goods-info
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiYTdhMjU4OTUtZmRjYi00ZWQ3LTg2MDEtZGMzZmY5Y2FlMDg4IiwiZXhwIjoxNjM3MTY0ODAwfQ.ihMZ14UdfTPMPyln8zOyP-_pM3X-VySZF6b0YKkbLmgEGbqc6zVS8E1NxqQ_XfSIaqbCb0a6GQnviBi1tEEwZL7U7Yyq4SsvuA4iGWpz2bgd-DS0qyCFvCxMeH6Q6yphYmej4IGF7EJ_EGIa0RfSuySa_tXrcKK6Xv5B0x4kkuWu57E61j3e1q1gqq_Ozn8NuB-iTzvxcepjczmLLkcuMqJ8EDHMd8j54CzUCbHb8it_2bZ6DnOc7pP2aZBxl7HaE7eo-LoaO_4QECuX2PcvGEe34hjTkT6_ahRBiWZZqxb15wjkTfP-rwHwFuCqHx0Ysou_4l766PxXVkP35lQlpQ

{
  "ids": [
    {
      "id": 1
    },
    {
      "id": 2
    }
  ]
}


### 扣减商品库存
PUT http://127.0.0.1:9001/edcode/scacommerce-goods-service/goods/deduct-goods-inventory
Content-Type: application/json
sca-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJzY2EtY29tbWVyY2UtdXNlciI6IntcImlkXCI6MTEsXCJ1c2VybmFtZVwiOlwiZWRkaWVAcXEuY29tXCJ9IiwianRpIjoiYTdhMjU4OTUtZmRjYi00ZWQ3LTg2MDEtZGMzZmY5Y2FlMDg4IiwiZXhwIjoxNjM3MTY0ODAwfQ.ihMZ14UdfTPMPyln8zOyP-_pM3X-VySZF6b0YKkbLmgEGbqc6zVS8E1NxqQ_XfSIaqbCb0a6GQnviBi1tEEwZL7U7Yyq4SsvuA4iGWpz2bgd-DS0qyCFvCxMeH6Q6yphYmej4IGF7EJ_EGIa0RfSuySa_tXrcKK6Xv5B0x4kkuWu57E61j3e1q1gqq_Ozn8NuB-iTzvxcepjczmLLkcuMqJ8EDHMd8j54CzUCbHb8it_2bZ6DnOc7pP2aZBxl7HaE7eo-LoaO_4QECuX2PcvGEe34hjTkT6_ahRBiWZZqxb15wjkTfP-rwHwFuCqHx0Ysou_4l766PxXVkP35lQlpQ

[
  {
    "goodsId": 1,
    "count": 100
  },
  {
    "goodsId": 2,
    "count": 34
  }
]

###

10.17.1 商品微服务总结

10.17.1 商品微服务总结

  • 商品微服务提供的功能
    • 商品微服务的功能以及在业务中的位置

在这里插入图片描述

10.17.2 异步任务总结

  • SpringBoot(Spring)异步任务应该如何写
    • 注解 @EnableAsync 开启异步任务支持
    • 自定义异步任务线程池(线程池配置根据业务需要、一定要指定线程名称前缀
    • 异步任务异常捕获处理器(发送邮件、短信报警等等)

Tips:异步任务一定要有状态监控


# SpringCloud