From 6d01440d3f066f651729341ac06ff564b8d95f53 Mon Sep 17 00:00:00 2001 From: qyx <565485304@qq.com> Date: Fri, 30 Aug 2024 17:36:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(master):=E5=95=86=E5=93=81=E5=BE=AE?= =?UTF-8?q?=E6=9C=8D=E5=8A=A1=E5=AE=8C=E7=BB=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 商品微服务, 完成开发 --- dev-protocol-springcloud/NetflixHystrix.md | 29 +++ .../pom.xml | 6 + .../pom.xml | 99 ++++++++ .../java/org/example/GoodsApplication.java | 22 ++ .../org/example/config/AsyncPoolConfig.java | 78 ++++++ .../example/constant/AsyncTaskStatusEnum.java | 24 ++ .../org/example/constant/BrandCategory.java | 43 ++++ .../org/example/constant/GoodsCategory.java | 45 ++++ .../org/example/constant/GoodsConstant.java | 11 + .../org/example/constant/GoodsStatus.java | 41 ++++ .../controller/AsyncGoodsController.java | 44 ++++ .../example/controller/GoodsController.java | 64 +++++ .../converter/BrandCategoryConverter.java | 22 ++ .../converter/GoodsCategoryConverter.java | 22 ++ .../converter/GoodsStatusConverter.java | 28 +++ .../org/example/dao/EcommerceGoodsDao.java | 25 ++ .../org/example/entity/EcommerceGoods.java | 162 +++++++++++++ .../org/example/service/IGoodsService.java | 36 +++ .../service/async/AsyncServiceImpl.java | 155 ++++++++++++ .../service/async/AsyncTaskManager.java | 73 ++++++ .../service/async/AsyncTaskMonitor.java | 70 ++++++ .../example/service/async/IAsyncService.java | 17 ++ .../service/impl/GoodsServiceImpl.java | 229 ++++++++++++++++++ .../java/org/example/vo/AsyncTaskInfo.java | 32 +++ .../org/example/vo/PageSimpleGoodsInfo.java | 26 ++ .../src/main/resources/bootstrap.yml | 75 ++++++ .../src/main/resources/http/goods-async.http | 58 +++++ .../src/main/resources/http/goods-goods.http | 57 +++++ .../main/resources/sql/t_ecommerce_goods.sql | 18 ++ .../org/example/GoodsApplicationTest.java | 19 ++ .../org/example/service/GoodsServiceTest.java | 67 +++++ .../商品微服务.md | 19 ++ pom.xml | 1 + 33 files changed, 1717 insertions(+) create mode 100644 dev-protocol-springcloud/NetflixHystrix.md create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/pom.xml create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/GoodsApplication.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/config/AsyncPoolConfig.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/AsyncTaskStatusEnum.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/BrandCategory.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsCategory.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsConstant.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsStatus.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/AsyncGoodsController.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/GoodsController.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/BrandCategoryConverter.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsCategoryConverter.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsStatusConverter.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/dao/EcommerceGoodsDao.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/entity/EcommerceGoods.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/IGoodsService.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncServiceImpl.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskManager.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskMonitor.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/IAsyncService.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/impl/GoodsServiceImpl.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/AsyncTaskInfo.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/PageSimpleGoodsInfo.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/bootstrap.yml create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-async.http create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-goods.http create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/sql/t_ecommerce_goods.sql create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/GoodsApplicationTest.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/service/GoodsServiceTest.java create mode 100644 dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/商品微服务.md diff --git a/dev-protocol-springcloud/NetflixHystrix.md b/dev-protocol-springcloud/NetflixHystrix.md new file mode 100644 index 0000000..1609f5e --- /dev/null +++ b/dev-protocol-springcloud/NetflixHystrix.md @@ -0,0 +1,29 @@ +## SpringCloud Netflix Hystrix + + +### SpringCloud Netflix Hystrix 概览 + + +### 使用注解方式实现服务的容错、降级 + + +### 使用编程方式实现服务的容错、降级 + + +### 编程方式开启 Hystrix 请求缓存 + + +### 注解方式开启 Hystrix 请求缓存 + + +### 编程方式应用 Hystrix 请求合并 + + +### 注解方式应用 Hystrix 请求合并 + +### OpenFeign 集成 Hystrix 开启后备模式 + + +### 使用 Hystrix 监控面板监测客户端容错 + +### SpringCloud Netflix Hystrix 容错组件总结 \ No newline at end of file diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-common/pom.xml b/dev-protocol-springcloud/dev-protocol-springcloud-project-common/pom.xml index a3c6db4..feb826b 100644 --- a/dev-protocol-springcloud/dev-protocol-springcloud-project-common/pom.xml +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-common/pom.xml @@ -44,12 +44,18 @@ hutool-all 5.7.22 + + org.apache.commons + commons-collections4 + 4.4 + com.alibaba.fastjson2 fastjson2 2.0.18 + \ No newline at end of file diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/pom.xml b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/pom.xml new file mode 100644 index 0000000..8b5cd7c --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + org.example + dev-protocol + 1.0-SNAPSHOT + ../../pom.xml + + + dev-protocol-springcloud-project-goods-service + 1.0-SNAPSHOT + jar + + + dev-protocol-springcloud-project-goods-service + 商品服务 + + + 8 + 8 + UTF-8 + + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + org.springframework.cloud + spring-cloud-starter-zipkin + + + org.springframework.kafka + spring-kafka + 2.5.0.RELEASE + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + mysql + mysql-connector-java + 8.0.12 + runtime + + + + org.springframework.boot + spring-boot-starter-aop + + + + org.example + dev-protocol-springcloud-project-service-config + 1.0-SNAPSHOT + + + org.example + dev-protocol-springcloud-project-service-sdk + 1.0-SNAPSHOT + + + + + + ${artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/GoodsApplication.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/GoodsApplication.java new file mode 100644 index 0000000..9d32063 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/GoodsApplication.java @@ -0,0 +1,22 @@ +package org.example; + +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; + +/** + *

商品微服务启动入口

+ * 启动依赖组件/中间件: Redis + MySQL + Nacos + Kafka + Zipkin + * http://127.0.0.1:8001/dev-protocol-springcloud-project-goods-service/doc.html + * */ +@EnableJpaAuditing +@EnableDiscoveryClient +@SpringBootApplication +public class GoodsApplication { + + public static void main(String[] args) { + + SpringApplication.run(GoodsApplication.class, args); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/config/AsyncPoolConfig.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/config/AsyncPoolConfig.java new file mode 100644 index 0000000..442ab70 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/config/AsyncPoolConfig.java @@ -0,0 +1,78 @@ +package org.example.config; + +import com.alibaba.fastjson2.JSON; +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.scheduling.annotation.AsyncConfigurer; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.lang.reflect.Method; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; + +/** + *

自定义异步任务线程池, 异步任务异常捕获处理器

+ * */ +@Slf4j +@EnableAsync // 开启 Spring 异步任务支持 +@Configuration +public class AsyncPoolConfig implements AsyncConfigurer { + + /** + *

将自定义的线程池注入到 Spring 容器中

+ * */ + @Bean + @Override + public Executor getAsyncExecutor() { + + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + + executor.setCorePoolSize(10); + executor.setMaxPoolSize(20); + executor.setQueueCapacity(20); + executor.setKeepAliveSeconds(60); + executor.setThreadNamePrefix("Q-Async-"); // 这个非常重要 + + // 等待所有任务结果候再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(60); + // 定义拒绝策略 + executor.setRejectedExecutionHandler( + 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)); + + // TODO 发送邮件或者是短信, 做进一步的报警处理 + } + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/AsyncTaskStatusEnum.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/AsyncTaskStatusEnum.java new file mode 100644 index 0000000..1598bd1 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/AsyncTaskStatusEnum.java @@ -0,0 +1,24 @@ +package org.example.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + *

异步任务状态枚举

+ * */ +@Getter +@AllArgsConstructor +public enum AsyncTaskStatusEnum { + + STARTED(0, "已经启动"), + RUNNING(1, "正在运行"), + SUCCESS(2, "执行成功"), + FAILED(3, "执行失败"), + ; + + /** 执行状态编码 */ + private final int state; + + /** 执行状态描述 */ + private final String stateInfo; +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/BrandCategory.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/BrandCategory.java new file mode 100644 index 0000000..3966072 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/BrandCategory.java @@ -0,0 +1,43 @@ +package org.example.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + *

品牌分类

+ * */ +@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") + ); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsCategory.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsCategory.java new file mode 100644 index 0000000..8474484 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsCategory.java @@ -0,0 +1,45 @@ +package org.example.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + *

商品类别

+ * 电器 -> 手机、电脑 + * */ +@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") + ); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsConstant.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsConstant.java new file mode 100644 index 0000000..0d4b9b1 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsConstant.java @@ -0,0 +1,11 @@ +package org.example.constant; + +/** + *

商品常量信息

+ * */ +public class GoodsConstant { + + /** redis key */ + public static final String ECOMMERCE_GOODS_DICT_KEY = + "ecommerce:goods:dict:20210101"; +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsStatus.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsStatus.java new file mode 100644 index 0000000..52f5ae5 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/constant/GoodsStatus.java @@ -0,0 +1,41 @@ +package org.example.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Objects; +import java.util.stream.Stream; + +/** + *

商品状态枚举类

+ * */ +@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") + ); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/AsyncGoodsController.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/AsyncGoodsController.java new file mode 100644 index 0000000..9baaa4f --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/AsyncGoodsController.java @@ -0,0 +1,44 @@ +package org.example.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.example.goods.GoodsInfo; +import org.example.service.async.AsyncTaskManager; +import org.example.vo.AsyncTaskInfo; +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; + +/** + *

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

+ * */ +@Api(tags = "商品异步入库服务") +@Slf4j +@RestController +@RequestMapping("/async-goods") +public class AsyncGoodsController { + + private final AsyncTaskManager asyncTaskManager; + + public AsyncGoodsController(AsyncTaskManager asyncTaskManager) { + this.asyncTaskManager = asyncTaskManager; + } + + @ApiOperation(value = "导入商品", notes = "导入商品进入到商品表", httpMethod = "POST") + @PostMapping("/import-goods") + public AsyncTaskInfo importGoods(@RequestBody List goodsInfos) { + return asyncTaskManager.submit(goodsInfos); + } + + @ApiOperation(value = "查询状态", notes = "查询异步任务的执行状态", httpMethod = "GET") + @GetMapping("/task-info") + public AsyncTaskInfo getTaskInfo(@RequestParam String taskId) { + return asyncTaskManager.getTaskInfo(taskId); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/GoodsController.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/GoodsController.java new file mode 100644 index 0000000..df2965d --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/controller/GoodsController.java @@ -0,0 +1,64 @@ +package org.example.controller; + +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.example.common.TableId; +import org.example.goods.DeductGoodsInventory; +import org.example.goods.GoodsInfo; +import org.example.goods.SimpleGoodsInfo; +import org.example.service.IGoodsService; +import org.example.vo.PageSimpleGoodsInfo; +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; + +/** + *

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

+ * */ +@Api(tags = "商品微服务功能接口") +@Slf4j +@RestController +@RequestMapping("/goods") +public class GoodsController { + + private final IGoodsService goodsService; + + public GoodsController(IGoodsService goodsService) { + this.goodsService = goodsService; + } + + @ApiOperation(value = "详细商品信息", notes = "根据 TableId 查询详细商品信息", + httpMethod = "POST") + @PostMapping("/goods-info") + public List 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 getSimpleGoodsInfoByTableId(@RequestBody TableId tableId) { + return goodsService.getSimpleGoodsInfoByTableId(tableId); + } + + @ApiOperation(value = "扣减商品库存", notes = "扣减商品库存", httpMethod = "PUT") + @PutMapping("/deduct-goods-inventory") + public Boolean deductGoodsInventory( + @RequestBody List deductGoodsInventories) { + return goodsService.deductGoodsInventory(deductGoodsInventories); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/BrandCategoryConverter.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/BrandCategoryConverter.java new file mode 100644 index 0000000..bb42d24 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/BrandCategoryConverter.java @@ -0,0 +1,22 @@ +package org.example.converter; + + +import org.example.constant.BrandCategory; + +import javax.persistence.AttributeConverter; + +/** + *

品牌分类枚举属性转换器

+ * */ +public class BrandCategoryConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(BrandCategory brandCategory) { + return brandCategory.getCode(); + } + + @Override + public BrandCategory convertToEntityAttribute(String code) { + return BrandCategory.of(code); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsCategoryConverter.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsCategoryConverter.java new file mode 100644 index 0000000..edd3538 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsCategoryConverter.java @@ -0,0 +1,22 @@ +package org.example.converter; + + +import org.example.constant.GoodsCategory; + +import javax.persistence.AttributeConverter; + +/** + *

商品类别枚举属性转换器

+ * */ +public class GoodsCategoryConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(GoodsCategory goodsCategory) { + return goodsCategory.getCode(); + } + + @Override + public GoodsCategory convertToEntityAttribute(String code) { + return GoodsCategory.of(code); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsStatusConverter.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsStatusConverter.java new file mode 100644 index 0000000..6997296 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/converter/GoodsStatusConverter.java @@ -0,0 +1,28 @@ +package org.example.converter; + + +import org.example.constant.GoodsStatus; + +import javax.persistence.AttributeConverter; + +/** + *

商品状态枚举属性转换器

+ * */ +public class GoodsStatusConverter implements AttributeConverter { + + /** + *

转换成可以存入数据表的基本类型

+ * */ + @Override + public Integer convertToDatabaseColumn(GoodsStatus goodsStatus) { + return goodsStatus.getStatus(); + } + + /** + *

还原数据表中的字段值到 Java 数据类型

+ * */ + @Override + public GoodsStatus convertToEntityAttribute(Integer status) { + return GoodsStatus.of(status); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/dao/EcommerceGoodsDao.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/dao/EcommerceGoodsDao.java new file mode 100644 index 0000000..8ed1fa7 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/dao/EcommerceGoodsDao.java @@ -0,0 +1,25 @@ +package org.example.dao; + + +import org.example.constant.BrandCategory; +import org.example.constant.GoodsCategory; +import org.example.entity.EcommerceGoods; +import org.springframework.data.repository.PagingAndSortingRepository; + +import java.util.Optional; + +/** + *

EcommerceGoods Dao 接口定义

+ * */ +public interface EcommerceGoodsDao extends PagingAndSortingRepository { + + /** + *

根据查询条件查询商品表, 并限制返回结果

+ * select * from t_dev_protocol_cloud_goods where goods_category = ? and brand_category = ? + * and goods_name = ? limit 1; + * */ + Optional findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName( + GoodsCategory goodsCategory, BrandCategory brandCategory, + String goodsName + ); +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/entity/EcommerceGoods.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/entity/EcommerceGoods.java new file mode 100644 index 0000000..ec0871c --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/entity/EcommerceGoods.java @@ -0,0 +1,162 @@ +package org.example.entity; + +import com.alibaba.fastjson2.JSON; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.example.constant.BrandCategory; +import org.example.constant.GoodsCategory; +import org.example.constant.GoodsStatus; +import org.example.converter.BrandCategoryConverter; +import org.example.converter.GoodsCategoryConverter; +import org.example.converter.GoodsStatusConverter; +import org.example.goods.GoodsInfo; +import org.example.goods.SimpleGoodsInfo; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Entity; +import javax.persistence.EntityListeners; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import java.util.Date; + +/** + *

商品表实体类定义

+ * */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Entity +@EntityListeners(AuditingEntityListener.class) +@Table(name = "t_dev_protocol_cloud_goods") +public class EcommerceGoods { + + /** 自增主键 */ + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id", nullable = false) + private Long id; + + /** 商品类型 */ + @Column(name = "goods_category", nullable = false) + @Convert(converter = GoodsCategoryConverter.class) + private GoodsCategory goodsCategory; + + /** 品牌分类 */ + @Column(name = "brand_category", nullable = false) + @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) + @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; + + /** + *

将 GoodsInfo 转成实体对象

+ * */ + public static EcommerceGoods to(GoodsInfo goodsInfo) { + + EcommerceGoods ecommerceGoods = new EcommerceGoods(); + + ecommerceGoods.setGoodsCategory(GoodsCategory.of(goodsInfo.getGoodsCategory())); + ecommerceGoods.setBrandCategory(BrandCategory.of(goodsInfo.getBrandCategory())); + ecommerceGoods.setGoodsName(goodsInfo.getGoodsName()); + ecommerceGoods.setGoodsPic(goodsInfo.getGoodsPic()); + ecommerceGoods.setGoodsDescription(goodsInfo.getGoodsDescription()); + ecommerceGoods.setGoodsStatus(GoodsStatus.ONLINE); // 可以增加一个审核的过程 + ecommerceGoods.setPrice(goodsInfo.getPrice()); + ecommerceGoods.setSupply(goodsInfo.getSupply()); + ecommerceGoods.setInventory(goodsInfo.getSupply()); + ecommerceGoods.setGoodsProperty( + JSON.toJSONString(goodsInfo.getGoodsProperty()) + ); + + return ecommerceGoods; + } + + /** + *

将实体对象转成 GoodsInfo 对象

+ * */ + 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; + } + + /** + *

将实体对象转成 SimpleGoodsInfo 对象

+ * */ + 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; + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/IGoodsService.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/IGoodsService.java new file mode 100644 index 0000000..afb63b9 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/IGoodsService.java @@ -0,0 +1,36 @@ +package org.example.service; + + +import org.example.common.TableId; +import org.example.goods.DeductGoodsInventory; +import org.example.goods.GoodsInfo; +import org.example.goods.SimpleGoodsInfo; +import org.example.vo.PageSimpleGoodsInfo; + +import java.util.List; + +/** + *

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

+ * */ +public interface IGoodsService { + + /** + *

根据 TableId 查询商品详细信息

+ * */ + List getGoodsInfoByTableId(TableId tableId); + + /** + *

获取分页的商品信息

+ * */ + PageSimpleGoodsInfo getSimpleGoodsInfoByPage(int page); + + /** + *

根据 TableId 查询简单商品信息

+ * */ + List getSimpleGoodsInfoByTableId(TableId tableId); + + /** + *

扣减商品库存

+ * */ + Boolean deductGoodsInventory(List deductGoodsInventories); +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncServiceImpl.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncServiceImpl.java new file mode 100644 index 0000000..2b5be27 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncServiceImpl.java @@ -0,0 +1,155 @@ +package org.example.service.async; + +import com.alibaba.fastjson2.JSON; +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.example.constant.GoodsConstant; +import org.example.dao.EcommerceGoodsDao; +import org.example.entity.EcommerceGoods; +import org.example.goods.GoodsInfo; +import org.example.goods.SimpleGoodsInfo; +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; + +/** + *

异步服务接口实现

+ * */ +@Slf4j +@Service +@Transactional +public class AsyncServiceImpl implements IAsyncService { + + private final EcommerceGoodsDao ecommerceGoodsDao; + private final StringRedisTemplate redisTemplate; + + public AsyncServiceImpl(EcommerceGoodsDao ecommerceGoodsDao, + StringRedisTemplate redisTemplate) { + this.ecommerceGoodsDao = ecommerceGoodsDao; + this.redisTemplate = redisTemplate; + } + + /** + *

异步任务需要加上注解, 并指定使用的线程池

+ * 异步任务处理两件事: + * 1. 将商品信息保存到数据表 + * 2. 更新商品缓存 + * */ + @Async("getAsyncExecutor") + @Override + public void asyncImportGoods(List goodsInfos, String taskId) { + + log.info("async task running taskId: [{}]", taskId); + + StopWatch watch = StopWatch.createStarted(); + + // 1. 如果是 goodsInfo 中存在重复的商品, 不保存; 直接返回, 记录错误日志 + // 请求数据是否合法的标记 + boolean isIllegal = false; + + // 将商品信息字段 joint 在一起, 用来判断是否存在重复 + Set goodsJointInfos = new HashSet<>(goodsInfos.size()); + // 过滤出来的, 可以入库的商品信息(规则按照自己的业务需求自定义即可) + List filteredGoodsInfo = new ArrayList<>(goodsInfos.size()); + + // 走一遍循环, 过滤非法参数与判定当前请求是否合法 + for (GoodsInfo goods : goodsInfos) { + + // 基本条件不满足的, 直接过滤器 + if (goods.getPrice() <= 0 || goods.getSupply() <= 0) { + log.info("goods info is invalid: [{}]", 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("import nothing: [{}]", JSON.toJSONString(filteredGoodsInfo)); + log.info("check and import goods done: [{}ms]", watch.getTime(TimeUnit.MILLISECONDS)); + return; + } + + List ecommerceGoods = filteredGoodsInfo.stream() + .map(EcommerceGoods::to) + .collect(Collectors.toList()); + List targetGoods = new ArrayList<>(ecommerceGoods.size()); + + // 2. 保存 goodsInfo 之前先判断下是否存在重复商品 + ecommerceGoods.forEach(g -> { + + // limit 1 + if (null != ecommerceGoodsDao + .findFirst1ByGoodsCategoryAndBrandCategoryAndGoodsName( + g.getGoodsCategory(), g.getBrandCategory(), + g.getGoodsName() + ).orElse(null)) { + return; + } + + targetGoods.add(g); + }); + + // 商品信息入库 + List savedGoods = IterableUtils.toList( + ecommerceGoodsDao.saveAll(targetGoods) + ); + // 将入库商品信息同步到 Redis 中 + saveNewGoodsInfoToRedis(savedGoods); + + log.info("save goods info to db and redis: [{}]", savedGoods.size()); + + watch.stop(); + log.info("check and import goods success: [{}ms]", + watch.getTime(TimeUnit.MILLISECONDS)); + } + + /** + *

将保存到数据表中的数据缓存到 Redis 中

+ * dict: key -> + * */ + private void saveNewGoodsInfoToRedis(List savedGoods) { + + // 由于 Redis 是内存存储, 只存储简单商品信息 + List simpleGoodsInfos = savedGoods.stream() + .map(EcommerceGoods::toSimple) + .collect(Collectors.toList()); + + Map id2JsonObject = new HashMap<>(simpleGoodsInfos.size()); + simpleGoodsInfos.forEach( + g -> id2JsonObject.put(g.getId().toString(), JSON.toJSONString(g)) + ); + + // 保存到 Redis 中 + redisTemplate.opsForHash().putAll( + GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, + id2JsonObject + ); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskManager.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskManager.java new file mode 100644 index 0000000..d51952e --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskManager.java @@ -0,0 +1,73 @@ +package org.example.service.async; + +import lombok.extern.slf4j.Slf4j; +import org.example.constant.AsyncTaskStatusEnum; +import org.example.goods.GoodsInfo; +import org.example.vo.AsyncTaskInfo; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +/** + *

异步任务执行管理器

+ * 对异步任务进行包装管理, 记录并塞入异步任务执行信息 + * */ +@Slf4j +@Component +public class AsyncTaskManager { + + /** 异步任务执行信息容器 */ + private final Map taskContainer = + new HashMap<>(16); + + private final IAsyncService asyncService; + + public AsyncTaskManager(IAsyncService asyncService) { + this.asyncService = 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 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); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskMonitor.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskMonitor.java new file mode 100644 index 0000000..f60c22a --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/AsyncTaskMonitor.java @@ -0,0 +1,70 @@ +package org.example.service.async; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.example.constant.AsyncTaskStatusEnum; +import org.example.vo.AsyncTaskInfo; +import org.springframework.stereotype.Component; + +import java.util.Date; + +/** + *

异步任务执行监控切面

+ * */ +@Slf4j +@Aspect +@Component +public class AsyncTaskMonitor { + + /** 注入异步任务管理器 */ + private final AsyncTaskManager asyncTaskManager; + + public AsyncTaskMonitor(AsyncTaskManager asyncTaskManager) { + this.asyncTaskManager = asyncTaskManager; + } + + /** + *

异步任务执行的环绕切面

+ * 环绕切面让我们可以在方法执行之前和执行之后做一些 "额外" 的操作 + * */ + @Around("execution(* org.example.service.async.AsyncServiceImpl.*(..))") + public Object taskHandle(ProceedingJoinPoint proceedingJoinPoint) { + + // 获取 taskId, 调用异步任务传入的第二个参数 + String taskId = proceedingJoinPoint.getArgs()[1].toString(); + + // 获取任务信息, 在提交任务的时候就已经放入到容器中了 + AsyncTaskInfo taskInfo = asyncTaskManager.getTaskInfo(taskId); + log.info("AsyncTaskMonitor is monitoring async task: [{}]", 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: async task [{}] is failed, Error Info: [{}]", + 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; + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/IAsyncService.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/IAsyncService.java new file mode 100644 index 0000000..b29cc53 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/async/IAsyncService.java @@ -0,0 +1,17 @@ +package org.example.service.async; + + +import org.example.goods.GoodsInfo; + +import java.util.List; + +/** + *

异步服务接口定义

+ * */ +public interface IAsyncService { + + /** + *

异步将商品信息保存下来

+ * */ + void asyncImportGoods(List goodsInfos, String taskId); +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/impl/GoodsServiceImpl.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/impl/GoodsServiceImpl.java new file mode 100644 index 0000000..b08e606 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/service/impl/GoodsServiceImpl.java @@ -0,0 +1,229 @@ +package org.example.service.impl; + +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.IterableUtils; +import org.example.common.TableId; +import org.example.constant.GoodsConstant; +import org.example.dao.EcommerceGoodsDao; +import org.example.entity.EcommerceGoods; +import org.example.goods.DeductGoodsInventory; +import org.example.goods.GoodsInfo; +import org.example.goods.SimpleGoodsInfo; +import org.example.service.IGoodsService; +import org.example.vo.PageSimpleGoodsInfo; +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.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + *

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

+ * */ +@Slf4j +@Service +@Transactional(rollbackFor = Exception.class) +public class GoodsServiceImpl implements IGoodsService { + + private final StringRedisTemplate redisTemplate; + private final EcommerceGoodsDao ecommerceGoodsDao; + + public GoodsServiceImpl(StringRedisTemplate redisTemplate, + EcommerceGoodsDao ecommerceGoodsDao) { + this.redisTemplate = redisTemplate; + this.ecommerceGoodsDao = ecommerceGoodsDao; + } + + @Override + public List getGoodsInfoByTableId(TableId tableId) { + + // 详细的商品信息, 不能从 redis cache 中去拿 + List ids = tableId.getIds().stream() + .map(TableId.Id::getId) + .collect(Collectors.toList()); + log.info("get goods info by ids: [{}]", JSON.toJSONString(ids)); + + List ecommerceGoods = IterableUtils.toList( + ecommerceGoodsDao.findAllById(ids) + ); + + return ecommerceGoods.stream() + .map(EcommerceGoods::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 orderPage = ecommerceGoodsDao.findAll(pageable); + + // 是否还有更多页: 总页数是否大于当前给定的页 + boolean hasMore = orderPage.getTotalPages() > page; + + return new PageSimpleGoodsInfo( + orderPage.getContent().stream() + .map(EcommerceGoods::toSimple).collect(Collectors.toList()), + hasMore + ); + } + + @Override + public List getSimpleGoodsInfoByTableId(TableId tableId) { + + // 获取商品的简单信息, 可以从 redis cache 中去拿, 拿不到需要从 DB 中获取并保存到 Redis 里面 + // Redis 中的 KV 都是字符串类型 + List goodIds = tableId.getIds().stream() + .map(i -> i.getId().toString()).collect(Collectors.toList()); + + // FIXME 如果 cache 中查不到 goodsId 对应的数据, 返回的是 null, [null, null] + List cachedSimpleGoodsInfos = redisTemplate.opsForHash() + .multiGet(GoodsConstant.ECOMMERCE_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("get simple goods info by ids (from cache): [{}]", + JSON.toJSONString(goodIds)); + return parseCachedGoodsInfo(cachedSimpleGoodsInfos); + } else { + // 2. 一半从数据表中获取 (right), 一半从 redis cache 中获取 (left) + List left = parseCachedGoodsInfo(cachedSimpleGoodsInfos); + // 取差集: 传递进来的参数 - 缓存中查到的 = 缓存中没有的 + Collection subtractIds = com.alibaba.nacos.client.naming.utils.CollectionUtils.subtract( + goodIds.stream() + .map(g -> Long.valueOf(g.toString())).collect(Collectors.toList()), + left.stream() + .map(SimpleGoodsInfo::getId).collect(Collectors.toList()) + ); + // 缓存中没有的, 查询数据表并缓存 + List right = queryGoodsFromDBAndCacheToRedis( + new TableId(subtractIds.stream().map(TableId.Id::new) + .collect(Collectors.toList())) + ); + // 合并 left 和 right 并返回 + log.info("get simple goods info by ids (from db and cache): [{}]", + JSON.toJSONString(subtractIds)); + return new ArrayList<>(CollectionUtils.union(left, right)); + } + } else { + // 从 redis 里面什么都没有查到 + return queryGoodsFromDBAndCacheToRedis(tableId); + } + } + + /** + *

将缓存中的数据反序列化成 Java Pojo 对象

+ * */ + private List parseCachedGoodsInfo(List cachedSimpleGoodsInfo) { + + return cachedSimpleGoodsInfo.stream() + .map(s -> JSON.parseObject(s.toString(), SimpleGoodsInfo.class)) + .collect(Collectors.toList()); + } + + /** + *

从数据表中查询数据, 并缓存到 Redis 中

+ * */ + private List queryGoodsFromDBAndCacheToRedis(TableId tableId) { + + // 从数据表中查询数据并做转换 + List ids = tableId.getIds().stream() + .map(TableId.Id::getId).collect(Collectors.toList()); + log.info("get simple goods info by ids (from db): [{}]", + JSON.toJSONString(ids)); + List ecommerceGoods = IterableUtils.toList( + ecommerceGoodsDao.findAllById(ids) + ); + List result = ecommerceGoods.stream() + .map(EcommerceGoods::toSimple).collect(Collectors.toList()); + // 将结果缓存, 下一次可以直接从 redis cache 中查询 + log.info("cache goods info: [{}]", JSON.toJSONString(ids)); + + Map id2JsonObject = new HashMap<>(result.size()); + result.forEach(g -> id2JsonObject.put( + g.getId().toString(), JSON.toJSONString(g) + )); + // 保存到 Redis 中 + redisTemplate.opsForHash().putAll( + GoodsConstant.ECOMMERCE_GOODS_DICT_KEY, id2JsonObject); + return result; + } + + @Override + public Boolean deductGoodsInventory(List deductGoodsInventories) { + + // 检验下参数是否合法 + deductGoodsInventories.forEach(d -> { + if (d.getCount() <= 0) { + throw new RuntimeException("purchase goods count need > 0"); + } + }); + + List ecommerceGoods = IterableUtils.toList( + ecommerceGoodsDao.findAllById( + deductGoodsInventories.stream() + .map(DeductGoodsInventory::getGoodsId) + .collect(Collectors.toList()) + ) + ); + // 根据传递的 goodsIds 查询不到商品对象, 抛异常 + if (CollectionUtils.isEmpty(ecommerceGoods)) { + throw new RuntimeException("can not found any goods by request"); + } + // 查询出来的商品数量与传递的不一致, 抛异常 + if (ecommerceGoods.size() != deductGoodsInventories.size()) { + throw new RuntimeException("request is not valid"); + } + // goodsId -> DeductGoodsInventory + Map goodsId2Inventory = + deductGoodsInventories.stream().collect( + Collectors.toMap(DeductGoodsInventory::getGoodsId, + Function.identity()) + ); + + // 检查是不是可以扣减库存, 再去扣减库存 + ecommerceGoods.forEach(g -> { + Long currentInventory = g.getInventory(); + Integer needDeductInventory = goodsId2Inventory.get(g.getId()).getCount(); + if (currentInventory < needDeductInventory) { + log.error("goods inventory is not enough: [{}], [{}]", + currentInventory, needDeductInventory); + throw new RuntimeException("goods inventory is not enough: " + g.getId()); + } + // 扣减库存 + g.setInventory(currentInventory - needDeductInventory); + log.info("deduct goods inventory: [{}], [{}], [{}]", g.getId(), + currentInventory, g.getInventory()); + }); + + ecommerceGoodsDao.saveAll(ecommerceGoods); + log.info("deduct goods inventory done"); + + return true; + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/AsyncTaskInfo.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/AsyncTaskInfo.java new file mode 100644 index 0000000..f08c274 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/AsyncTaskInfo.java @@ -0,0 +1,32 @@ +package org.example.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.example.constant.AsyncTaskStatusEnum; + +import java.util.Date; + +/** + *

异步任务执行信息

+ * */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AsyncTaskInfo { + + /** 异步任务 id */ + private String taskId; + + /** 异步任务执行状态 */ + private AsyncTaskStatusEnum status; + + /** 异步任务开始时间 */ + private Date startTime; + + /** 异步任务结束时间 */ + private Date endTime; + + /** 异步任务总耗时 */ + private String totalTime; +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/PageSimpleGoodsInfo.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/PageSimpleGoodsInfo.java new file mode 100644 index 0000000..8569372 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/java/org/example/vo/PageSimpleGoodsInfo.java @@ -0,0 +1,26 @@ +package org.example.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.example.goods.SimpleGoodsInfo; + +import java.util.List; + +/** + *

分页商品信息

+ * */ +@ApiModel(description = "分页商品信息对象") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PageSimpleGoodsInfo { + + @ApiModelProperty(value = "分页简单商品信息") + private List simpleGoodsInfos; + + @ApiModelProperty(value = "是否有更多的商品(分页)") + private Boolean hasMore; +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/bootstrap.yml b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/bootstrap.yml new file mode 100644 index 0000000..37b83cc --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/bootstrap.yml @@ -0,0 +1,75 @@ +server: + port: 8001 + servlet: + context-path: /dev-protocol-springcloud-project-goods-service + +spring: + application: + name: dev-protocol-springcloud-project-goods-service + cloud: + nacos: + discovery: + enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可 + server-addr: 127.0.0.1:8848 + # server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850 # Nacos 服务器地址 + namespace: 1ccc74ae-9398-4dbe-b9d7-4f9addf9f40c + metadata: + management: + context-path: ${server.servlet.context-path}/actuator + kafka: + bootstrap-servers: 127.0.0.1:9092 + producer: + retries: 3 + consumer: + auto-offset-reset: latest + sleuth: + sampler: + probability: 1.0 # 采样比例, 1.0 表示 100%, 默认是 0.1 + zipkin: + sender: + type: kafka # 默认是 http + base-url: http://localhost:9411/ + jpa: + show-sql: true + hibernate: + ddl-auto: none + + properties: + hibernate.show_sql: true + hibernate.format_sql: true + hibernate: + dialect: org.hibernate.dialect.MySQLDialect + open-in-view: false + datasource: + # 数据源 + url: jdbc:mysql://127.0.0.1:3306/dev_protocol_springcloud_project?autoReconnect=true&useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8 + username: root + password: root + 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: devProtocolSpringcloudHikariCP + redis: + database: 0 + host: 127.0.0.1 + port: 6379 + # password: + # 连接超时时间 + timeout: 10000 + +# 暴露端点 +management: + endpoints: + web: + exposure: + include: '*' + endpoint: + health: + show-details: always diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-async.http b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-async.http new file mode 100644 index 0000000..83e776c --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-async.http @@ -0,0 +1,58 @@ +### 导入商品 +POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/async-goods/import-goods +Content-Type: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjAyYTRiZDcxLWUyMTgtNGZmYS1hYzQ3LWE5MGQxNWIzYmEwYSIsImV4cCI6MTYyNDgwOTYwMH0.UWbvqkIq5b5bb-WomLziZyCmjqCqsdeU1EZ0TfWrloRoY7WwqmYGDsf2GnE7JBgVLM0DibhSkkrkXu-wdjzWnqtxLkQ5UgON9BdPm1ZYLvllLcbAMv8KAdbXiC1_FiZ9q1tM6vGXlKU4-G1t88cUUP1_xXOGY9PvC5yGr31lQXCc0Nni4Ds4WwDPHvOq9YBVILdaWYeFsxIWi0pTGwcAxaCkp3BdsvPkJ3uXmrmzuLgkorkfITsmJqdaBuiSCD74LK0F-CvvCv09qizij627O3RuTrpbBfdFjDXT5xyRcKXxAR-n6oFGZdG-JUqh3iXWv_JdsyW-d8wPk3-DZ5zufA + +[ + { + "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/imooc/ecommerce-goods-service/async-goods/task-info?taskId=f5c1c6ff-4efb-45e5-a8c9-9f3d4515228a +Accept: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjAyYTRiZDcxLWUyMTgtNGZmYS1hYzQ3LWE5MGQxNWIzYmEwYSIsImV4cCI6MTYyNDgwOTYwMH0.UWbvqkIq5b5bb-WomLziZyCmjqCqsdeU1EZ0TfWrloRoY7WwqmYGDsf2GnE7JBgVLM0DibhSkkrkXu-wdjzWnqtxLkQ5UgON9BdPm1ZYLvllLcbAMv8KAdbXiC1_FiZ9q1tM6vGXlKU4-G1t88cUUP1_xXOGY9PvC5yGr31lQXCc0Nni4Ds4WwDPHvOq9YBVILdaWYeFsxIWi0pTGwcAxaCkp3BdsvPkJ3uXmrmzuLgkorkfITsmJqdaBuiSCD74LK0F-CvvCv09qizij627O3RuTrpbBfdFjDXT5xyRcKXxAR-n6oFGZdG-JUqh3iXWv_JdsyW-d8wPk3-DZ5zufA diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-goods.http b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-goods.http new file mode 100644 index 0000000..77436e3 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/http/goods-goods.http @@ -0,0 +1,57 @@ +### 根据 TableId 查询详细商品信息 +POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/goods-info +Content-Type: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A + +{ + "ids": [ + { + "id": 1 + }, + { + "id": 2 + } + ] +} + + +### 根据分页查询简单商品信息 +GET http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/page-simple-goods-info?page=2 +Accept: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A + + +### 根据 TableId 查询简单商品信息: 完整的 goods cache +### 第二步验证, 删掉 cache +### 第三步验证, 删除 cache 中其中一个商品 +POST http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/simple-goods-info +Content-Type: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A + +{ + "ids": [ + { + "id": 1 + }, + { + "id": 2 + } + ] +} + + +### 扣减商品库存 +PUT http://127.0.0.1:9001/imooc/ecommerce-goods-service/goods/deduct-goods-inventory +Content-Type: application/json +e-commerce-user: eyJhbGciOiJSUzI1NiJ9.eyJlLWNvbW1lcmNlLXVzZXIiOiJ7XCJpZFwiOjEwLFwidXNlcm5hbWVcIjpcIlFpbnlpQGltb29jLmNvbVwifSIsImp0aSI6IjI3NGUzYzQ3LTRmNTQtNDdlYy05MGNhLTcxNzYyMjcyN2EzYyIsImV4cCI6MTYyNDk4MjQwMH0.TUy1C-9FkpyGkTxjyAKP9tX4mFzdZ22RWYvtKOOUUwjFefHSESamFWTJ2l0PcJJp07EIpzKgk9sNnVRZ5NmW6_Beo2AQgPOMWbYHiJg7eiR0bVC2CK6Tw8rUwgpkoWSXePgUM_3kntvXc19mgzO1NLVPNw5gahkBigzDffrXVUuXyc6kAf6L-y37hCytqfUwpgwQYm4Z2G7tUmF0_BsnQR4qHuWHrEdHm3_8Y8V38Ph_1VAlcJGvNXZS3bqtBxWHa2Wf7WksVA-H3dO_7xO7AlGJvUNOyiMGOjvMiwXc5mbqqqe6KXnvr9W1CvAPFmR-nlmc81wiCqW5Yfwo2Rh_5A + +[ + { + "goodsId": 1, + "count": 100 + }, + { + "goodsId": 2, + "count": 34 + } +] diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/sql/t_ecommerce_goods.sql b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/sql/t_ecommerce_goods.sql new file mode 100644 index 0000000..8562d57 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/main/resources/sql/t_ecommerce_goods.sql @@ -0,0 +1,18 @@ +-- 创建 t_ecommerce_goods 数据表 +CREATE TABLE IF NOT EXISTS `dev_protocol_springcloud_project`.`t_dev_protocol_cloud_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=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/GoodsApplicationTest.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/GoodsApplicationTest.java new file mode 100644 index 0000000..baa2e88 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/GoodsApplicationTest.java @@ -0,0 +1,19 @@ +package org.example; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + *

商品微服务环境可用性校验

+ * */ +@SpringBootTest +@RunWith(SpringRunner.class) +public class GoodsApplicationTest { + + @Test + public void contextLoad() { + + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/service/GoodsServiceTest.java b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/service/GoodsServiceTest.java new file mode 100644 index 0000000..5e7d35a --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/src/test/java/org/example/service/GoodsServiceTest.java @@ -0,0 +1,67 @@ +package org.example.service; + +import com.alibaba.fastjson2.JSON; +import lombok.extern.slf4j.Slf4j; +import org.example.common.TableId; +import org.example.goods.DeductGoodsInventory; +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.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + *

商品微服务功能测试

+ * */ +@Slf4j +@SpringBootTest +@RunWith(SpringRunner.class) +public class GoodsServiceTest { + + @Autowired + private IGoodsService goodsService; + + @Test + public void testGetGoodsInfoByTableId() { + + List ids = Arrays.asList(1L, 2L, 3L); + List tIds = ids.stream() + .map(TableId.Id::new).collect(Collectors.toList()); + log.info("test get goods info by table id: [{}]", + JSON.toJSONString(goodsService.getGoodsInfoByTableId(new TableId(tIds)))); + } + + @Test + public void testGetSimpleGoodsInfoByPage() { + + log.info("test get simple goods info by page: [{}]", JSON.toJSONString( + goodsService.getSimpleGoodsInfoByPage(1) + )); + } + + @Test + public void testGetSimpleGoodsInfoByTableId() { + + List ids = Arrays.asList(1L, 2L, 3L); + List tIds = ids.stream() + .map(TableId.Id::new).collect(Collectors.toList()); + log.info("test get simple goods info by table id: [{}]", JSON.toJSONString( + goodsService.getSimpleGoodsInfoByTableId(new TableId(tIds)) + )); + } + + @Test + public void testDeductGoodsInventory() { + + List deductGoodsInventories = Arrays.asList( + new DeductGoodsInventory(1L, 100), + new DeductGoodsInventory(2L, 66) + ); + log.info("test deduct goods inventory: [{}]", + goodsService.deductGoodsInventory(deductGoodsInventories)); + } +} diff --git a/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/商品微服务.md b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/商品微服务.md new file mode 100644 index 0000000..f717677 --- /dev/null +++ b/dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service/商品微服务.md @@ -0,0 +1,19 @@ +## 商品微服务 + + +### 商品微服务功能设计 + + + +### 商品属性枚举类及转换器定义 + + + +### 数据表及 ORM 过程 + + +### 商品信息对象定义及转换方法 + + + +### 异步任务与商品服务接口定义 \ No newline at end of file diff --git a/pom.xml b/pom.xml index da3e3d8..5a04683 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,7 @@ dev-protocol-springcloud/dev-protocol-springcloud-project-account-service dev-protocol-springcloud/dev-protocol-springcloud-project-service-config dev-protocol-springcloud/dev-protocol-springcloud-project-service-sdk + dev-protocol-springcloud/dev-protocol-springcloud-project-goods-service