From b76b683b66db4649bcb7a9c3d4aadb70817e1314 Mon Sep 17 00:00:00 2001 From: harper <35358294+yooyouny@users.noreply.github.com> Date: Tue, 15 Oct 2024 22:24:02 +0900 Subject: [PATCH] =?UTF-8?q?=EC=82=AC=EC=A0=84=EC=98=88=EC=95=BD=EC=A3=BC?= =?UTF-8?q?=EB=AC=B8=20API=20=EA=B5=AC=ED=98=84=20=20(#127)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#100): 카프카에서 사용하는 토픽들을 관리하는 문자열 상수 클래스를 commons에 정의 * feat(#22): Dto 모듈에 OrderDto 생성 * chore(#100): 카프카 설정 및 PreOrderProducer 등록 - 카프카 의존성 및 설정 - 오더서버로 사전예약상품 정보를 전송하는 카프카 빈 추가 * rename(#100): ProductSearchDto를 utils 패키지로 이동 * feat(#100): 사전예약주문 API 구현 - Redission 으로 분산락 구현 - validate로 사전예약주문기간, 중복여부, 사전예약수량 여부 확인 --------- Co-authored-by: linavell --- .../domain/entity/KafkaTopicConstant.java | 7 ++++ .../order/dto/src/main/java/dto/OrderDto.java | 1 - service/product/server/build.gradle | 3 ++ .../preorder/DistributedLockComponent.java | 36 +++++++++++++++++ .../preorder/PreOrderCacheService.java | 19 +++++++++ .../preorder/PreOrderFacadeService.java | 39 +++++++++++++++++++ .../preorder/PreOrderLockService.java | 28 +++++++++++++ .../application/preorder/PreOrderMapper.java | 8 ++++ .../preorder/PreOrderRedisService.java | 37 ++++++++++++++++++ .../application/preorder/PreOrderService.java | 2 +- .../product/ElasticsearchService.java | 2 +- .../repository/ElasticSearchRepository.java | 2 +- .../ElasticsearchCustomRepository.java | 2 +- .../repository/redis/RedisRepository.java | 23 +++++++++++ .../configuration/KafkaConfig.java | 20 ++++++++++ .../configuration/RedisConfig.java | 25 ++++++------ .../configuration/RedissonConfig.java | 26 +++++++++++++ .../messaging/PreOrderProducer.java | 17 ++++++++ .../utils/PreOrderRedisDto.java | 38 ++++++++++++++++++ .../dto => utils}/ProductSearchDto.java | 2 +- .../infrastructure/utils/RedisUtils.java | 7 ++++ .../controller/PreOrderController.java | 15 +++++++ .../controller/ProductSearchController.java | 2 +- .../exception/ProductErrorCode.java | 7 ++++ .../src/main/resources/application-local.yml | 22 ++++++++++- 25 files changed, 368 insertions(+), 22 deletions(-) create mode 100644 common/domain/src/main/java/com/sparta/common/domain/entity/KafkaTopicConstant.java create mode 100644 service/product/server/src/main/java/com/sparta/product/application/preorder/DistributedLockComponent.java create mode 100644 service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderCacheService.java create mode 100644 service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderFacadeService.java create mode 100644 service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderLockService.java create mode 100644 service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderRedisService.java create mode 100644 service/product/server/src/main/java/com/sparta/product/domain/repository/redis/RedisRepository.java create mode 100644 service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/KafkaConfig.java create mode 100644 service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedissonConfig.java create mode 100644 service/product/server/src/main/java/com/sparta/product/infrastructure/messaging/PreOrderProducer.java create mode 100644 service/product/server/src/main/java/com/sparta/product/infrastructure/utils/PreOrderRedisDto.java rename service/product/server/src/main/java/com/sparta/product/infrastructure/{elasticsearch/dto => utils}/ProductSearchDto.java (98%) create mode 100644 service/product/server/src/main/java/com/sparta/product/infrastructure/utils/RedisUtils.java diff --git a/common/domain/src/main/java/com/sparta/common/domain/entity/KafkaTopicConstant.java b/common/domain/src/main/java/com/sparta/common/domain/entity/KafkaTopicConstant.java new file mode 100644 index 00000000..b5cf1bef --- /dev/null +++ b/common/domain/src/main/java/com/sparta/common/domain/entity/KafkaTopicConstant.java @@ -0,0 +1,7 @@ +package com.sparta.common.domain.entity; + +public class KafkaTopicConstant { + public static final String PROCESS_PREORDER = "process_preorder"; + public static final String ERROR_IN_PROCESS_PREORDER = "error_in_create_delivery"; + public static final String PAYMENT_COMPLETED = "payment-completed-topic"; +} diff --git a/service/order/dto/src/main/java/dto/OrderDto.java b/service/order/dto/src/main/java/dto/OrderDto.java index 5251e47f..d85d3a5e 100644 --- a/service/order/dto/src/main/java/dto/OrderDto.java +++ b/service/order/dto/src/main/java/dto/OrderDto.java @@ -18,7 +18,6 @@ public static class OrderCreateRequest { private List orderProductInfos = new ArrayList<>(); private BigDecimal pointPrice; private Long addressId; - } @Getter diff --git a/service/product/server/build.gradle b/service/product/server/build.gradle index eda4b9a8..e63b98ee 100644 --- a/service/product/server/build.gradle +++ b/service/product/server/build.gradle @@ -31,10 +31,13 @@ dependencies { implementation project(':common:domain') implementation project(':service:product:product_dto') implementation project(':service:auth:auth_dto') + implementation project(':service:order:dto') implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-data-cassandra' implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.redisson:redisson:3.35.0' + implementation 'org.springframework.kafka:spring-kafka' implementation 'org.springframework.data:spring-data-elasticsearch:5.3.0' implementation 'co.elastic.clients:elasticsearch-java:8.6.0' implementation 'org.springframework.boot:spring-boot-starter-json:3.3.4' diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/DistributedLockComponent.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/DistributedLockComponent.java new file mode 100644 index 00000000..62632e3a --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/DistributedLockComponent.java @@ -0,0 +1,36 @@ +package com.sparta.product.application.preorder; + +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.annotation.Aspect; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@RequiredArgsConstructor +@Slf4j(topic = "DistributedLockComponent") +public class DistributedLockComponent { + private final RedissonClient redissonClient; + + public void execute( + String lockName, long waitMilliSecond, long leaseMilliSecond, Runnable logic) { + RLock lock = redissonClient.getLock(lockName); + try { + boolean isLocked = lock.tryLock(waitMilliSecond, leaseMilliSecond, TimeUnit.MILLISECONDS); + if (!isLocked) { + throw new IllegalStateException("[" + lockName + "] lock 획득 실패"); + } + logic.run(); + } catch (InterruptedException e) { + log.error(e.getMessage(), e); + throw new RuntimeException(e); + } finally { + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderCacheService.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderCacheService.java new file mode 100644 index 00000000..cb75370e --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderCacheService.java @@ -0,0 +1,19 @@ +package com.sparta.product.application.preorder; + +import com.sparta.product.domain.model.PreOrder; +import com.sparta.product.infrastructure.utils.PreOrderRedisDto; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PreOrderCacheService { + private final PreOrderService preOrderService; + + @Cacheable(cacheNames = "preOrder", key = "#preOrderId") + public PreOrderRedisDto getPreOrderCache(Long preOrderId) { + PreOrder preOrder = preOrderService.findPreOrderByPreOrderId(preOrderId); + return new PreOrderRedisDto(preOrder); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderFacadeService.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderFacadeService.java new file mode 100644 index 00000000..0f998445 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderFacadeService.java @@ -0,0 +1,39 @@ +package com.sparta.product.application.preorder; + +import com.sparta.common.domain.entity.KafkaTopicConstant; +import com.sparta.product.domain.model.PreOrder; +import com.sparta.product.domain.model.PreOrderState; +import com.sparta.product.infrastructure.messaging.PreOrderProducer; +import com.sparta.product.presentation.exception.ProductErrorCode; +import com.sparta.product.presentation.exception.ProductServerException; +import dto.OrderDto.OrderCreateRequest; +import java.time.LocalDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class PreOrderFacadeService { + private final PreOrderService preOrderService; + private final PreOrderProducer preOrderProducer; + private final PreOrderLockService preOrderLockService; + + @Transactional + public void preOrder(Long preOrderId, Long addressId, Long userId) { + PreOrder preOrder = preOrderService.findPreOrderByPreOrderId(preOrderId); + preOrderLockService.reservation(preOrderId, userId); + OrderCreateRequest createRequest = PreOrderMapper.toDto(preOrderId, addressId); + preOrderProducer.send(KafkaTopicConstant.PROCESS_PREORDER, preOrderId, createRequest); + } + + private void validatePreOrder(PreOrder preOrder) { + if (preOrder.getState() != PreOrderState.OPEN_FOR_ORDER) + throw new ProductServerException(ProductErrorCode.NOT_OPEN_FOR_PREORDER); + + LocalDateTime now = LocalDateTime.now(); + if (now.isBefore(preOrder.getStartDateTime()) || now.isAfter(preOrder.getEndDateTime())) { + throw new ProductServerException(ProductErrorCode.CLOSED_PREORDER); + } + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderLockService.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderLockService.java new file mode 100644 index 00000000..e85428b6 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderLockService.java @@ -0,0 +1,28 @@ +package com.sparta.product.application.preorder; + +import static com.sparta.product.infrastructure.utils.RedisUtils.getRedisKeyOfPreOrder; + +import com.sparta.product.infrastructure.utils.PreOrderRedisDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PreOrderLockService { + private final PreOrderRedisService redisService; + private final PreOrderCacheService cacheService; + private final DistributedLockComponent lockComponent; + + public void reservation(long preOrderId, long userId) { + PreOrderRedisDto cachedPreOrder = cacheService.getPreOrderCache(preOrderId); + cachedPreOrder.validateReservationDate(); // 사전예약기간인지 검증 + lockComponent.execute( // 락을 걸고 + "preOrderLock_%s".formatted(preOrderId), + 3000, + 3000, + () -> { + redisService.validateQuantity(cachedPreOrder, userId); // 이미 예약에 성공한 유저인지, 수량안에 든 유저인지 검증 + }); + redisService.preOrder(getRedisKeyOfPreOrder(preOrderId), userId); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderMapper.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderMapper.java index 53bdfc0d..08b82002 100644 --- a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderMapper.java +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderMapper.java @@ -2,6 +2,9 @@ import com.sparta.product.domain.model.PreOrder; import com.sparta.product.presentation.request.PreOrderCreateRequest; +import dto.OrderDto.OrderCreateRequest; +import dto.OrderDto.OrderProductInfo; +import java.util.List; public class PreOrderMapper { public static PreOrder toEntity(PreOrderCreateRequest request) { @@ -14,4 +17,9 @@ public static PreOrder toEntity(PreOrderCreateRequest request) { .availableQuantity(request.availableQuantity()) .build(); } + + public static OrderCreateRequest toDto(Long preOrderId, Long addressId) { + OrderProductInfo orderProduct = new OrderProductInfo(preOrderId.toString(), 1, null); + return new OrderCreateRequest("PREORDER", List.of(orderProduct), null, addressId); + } } diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderRedisService.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderRedisService.java new file mode 100644 index 00000000..52fc667e --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderRedisService.java @@ -0,0 +1,37 @@ +package com.sparta.product.application.preorder; + +import static com.sparta.product.infrastructure.utils.RedisUtils.getRedisKeyOfPreOrder; + +import com.sparta.product.domain.repository.redis.RedisRepository; +import com.sparta.product.infrastructure.utils.PreOrderRedisDto; +import com.sparta.product.presentation.exception.ProductErrorCode; +import com.sparta.product.presentation.exception.ProductServerException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class PreOrderRedisService { + private final RedisRepository redisRepository; + + public void validateQuantity(PreOrderRedisDto cache, long userId) { + if (!availableUser(cache.preOrderId(), userId)) + throw new ProductServerException(ProductErrorCode.ALREADY_PREORDER); + if (!availableQuantity(cache.availableQuantity(), cache.preOrderId())) + throw new ProductServerException(ProductErrorCode.EXCEED_PREORDER_QUANTITY); + } + + public void preOrder(String key, long userId) { + redisRepository.sAdd(key, Long.toString(userId)); + } + + public boolean availableUser(long preOrderId, long userId) { // 중복 요청 확인 + String key = getRedisKeyOfPreOrder(preOrderId); + return !redisRepository.sIsMember(key, String.valueOf(userId)); + } + + public boolean availableQuantity(int availableQuantity, long preOrderId) { // 수량 검증 + String key = getRedisKeyOfPreOrder(preOrderId); + return availableQuantity > redisRepository.sCard(key); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderService.java b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderService.java index 5302e475..108f4838 100644 --- a/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderService.java +++ b/service/product/server/src/main/java/com/sparta/product/application/preorder/PreOrderService.java @@ -75,7 +75,7 @@ public void deletePreOrder(Long preOrderId) { preOrderRepository.delete(preOrder); } - private PreOrder findPreOrderByPreOrderId(Long preOrderId) { + public PreOrder findPreOrderByPreOrderId(Long preOrderId) { return preOrderRepository .findByPreOrderIdAndIsPublicTrue(preOrderId) .orElseThrow(() -> new ProductServerException(ProductErrorCode.NOT_FOUND_PREORDER)); diff --git a/service/product/server/src/main/java/com/sparta/product/application/product/ElasticsearchService.java b/service/product/server/src/main/java/com/sparta/product/application/product/ElasticsearchService.java index 32095832..a5c631e6 100644 --- a/service/product/server/src/main/java/com/sparta/product/application/product/ElasticsearchService.java +++ b/service/product/server/src/main/java/com/sparta/product/application/product/ElasticsearchService.java @@ -3,7 +3,7 @@ import com.sparta.product.domain.model.SortOption; import com.sparta.product.domain.repository.ElasticSearchRepository; import com.sparta.product.domain.repository.ElasticsearchCustomRepository; -import com.sparta.product.infrastructure.elasticsearch.dto.ProductSearchDto; +import com.sparta.product.infrastructure.utils.ProductSearchDto; import com.sparta.product.presentation.response.ProductResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; diff --git a/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticSearchRepository.java b/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticSearchRepository.java index a74d3128..5cc035c1 100644 --- a/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticSearchRepository.java +++ b/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticSearchRepository.java @@ -1,6 +1,6 @@ package com.sparta.product.domain.repository; -import com.sparta.product.infrastructure.elasticsearch.dto.ProductSearchDto; +import com.sparta.product.infrastructure.utils.ProductSearchDto; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; public interface ElasticSearchRepository diff --git a/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticsearchCustomRepository.java b/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticsearchCustomRepository.java index 64180c41..d5bc5a7c 100644 --- a/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticsearchCustomRepository.java +++ b/service/product/server/src/main/java/com/sparta/product/domain/repository/ElasticsearchCustomRepository.java @@ -9,7 +9,7 @@ import co.elastic.clients.elasticsearch.core.search.TotalHits; import co.elastic.clients.json.JsonData; import com.sparta.product.domain.model.SortOption; -import com.sparta.product.infrastructure.elasticsearch.dto.ProductSearchDto; +import com.sparta.product.infrastructure.utils.ProductSearchDto; import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; diff --git a/service/product/server/src/main/java/com/sparta/product/domain/repository/redis/RedisRepository.java b/service/product/server/src/main/java/com/sparta/product/domain/repository/redis/RedisRepository.java new file mode 100644 index 00000000..f1d61f76 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/domain/repository/redis/RedisRepository.java @@ -0,0 +1,23 @@ +package com.sparta.product.domain.repository.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class RedisRepository { + private final RedisTemplate redisTemplate; + + public Long sAdd(String key, String value) { + return redisTemplate.opsForSet().add(key, value); + } + + public Long sCard(String key) { + return redisTemplate.opsForSet().size(key); + } + + public Boolean sIsMember(String key, String value) { + return redisTemplate.opsForSet().isMember(key, value); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/KafkaConfig.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/KafkaConfig.java new file mode 100644 index 00000000..706861e3 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/KafkaConfig.java @@ -0,0 +1,20 @@ +package com.sparta.product.infrastructure.configuration; + +import com.sparta.product.infrastructure.messaging.PreOrderProducer; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.KafkaTemplate; + +@ConditionalOnProperty(value = "kafka.enabled", matchIfMissing = true) +@RequiredArgsConstructor +@Configuration +public class KafkaConfig { + private final KafkaTemplate kafkaTemplate; + + @Bean + public PreOrderProducer preOrderProducer() { + return new PreOrderProducer(kafkaTemplate); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedisConfig.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedisConfig.java index 85981e17..44a1f105 100644 --- a/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedisConfig.java +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedisConfig.java @@ -4,30 +4,29 @@ import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.CacheKeyPrefix; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration @EnableCaching public class RedisConfig { @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { - Jackson2JsonRedisSerializer jsonRedisSerializer = - new Jackson2JsonRedisSerializer<>(Object.class); - - RedisCacheConfiguration configuration = + RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() - .disableCachingNullValues() - .entryTtl(Duration.ofDays(7)) - .computePrefixWith(CacheKeyPrefix.simple()) + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer( + new StringRedisSerializer())) .serializeValuesWith( - RedisSerializationContext.SerializationPair.fromSerializer(jsonRedisSerializer)); - ; - - return RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(configuration).build(); + RedisSerializationContext.SerializationPair.fromSerializer( + new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofMinutes(30)); + return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration) + .build(); } } diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedissonConfig.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedissonConfig.java new file mode 100644 index 00000000..e40cfee0 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/configuration/RedissonConfig.java @@ -0,0 +1,26 @@ +package com.sparta.product.infrastructure.configuration; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RedissonConfig { + private static final String REDIS_URL_PREFIX = "redis://"; + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer().setAddress(REDIS_URL_PREFIX + host + ":" + port); + return Redisson.create(config); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/messaging/PreOrderProducer.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/messaging/PreOrderProducer.java new file mode 100644 index 00000000..334a2946 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/messaging/PreOrderProducer.java @@ -0,0 +1,17 @@ +package com.sparta.product.infrastructure.messaging; + +import dto.OrderDto.OrderCreateRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; + +@RequiredArgsConstructor +@Slf4j(topic = "PreOrderProducer in Product server") +public class PreOrderProducer { + private final KafkaTemplate kafkaTemplate; + + public void send(String topic, Long preOrderId, OrderCreateRequest request) { + kafkaTemplate.send(topic, preOrderId.toString(), request); + log.info("send preorderRequest of {} to order server", preOrderId); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/PreOrderRedisDto.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/PreOrderRedisDto.java new file mode 100644 index 00000000..3b4f6c68 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/PreOrderRedisDto.java @@ -0,0 +1,38 @@ +package com.sparta.product.infrastructure.utils; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; +import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; +import com.sparta.product.domain.model.PreOrder; +import com.sparta.product.presentation.exception.ProductErrorCode; +import com.sparta.product.presentation.exception.ProductServerException; +import java.time.LocalDateTime; + +public record PreOrderRedisDto( + Long preOrderId, + Integer availableQuantity, + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + LocalDateTime startDateTime, + @JsonSerialize(using = LocalDateTimeSerializer.class) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) + LocalDateTime endDateTime) { + public PreOrderRedisDto(PreOrder preOrder) { + this( + preOrder.getPreOrderId(), + preOrder.getAvailableQuantity(), + preOrder.getStartDateTime(), + preOrder.getEndDateTime()); + } + + private boolean isReservation() { + LocalDateTime now = LocalDateTime.now(); + return startDateTime.isBefore(now) && endDateTime.isAfter(now); + } + + public void validateReservationDate() { + if (!isReservation()) + throw new ProductServerException(ProductErrorCode.INVALID_PREORDER_DATETIME); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/elasticsearch/dto/ProductSearchDto.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/ProductSearchDto.java similarity index 98% rename from service/product/server/src/main/java/com/sparta/product/infrastructure/elasticsearch/dto/ProductSearchDto.java rename to service/product/server/src/main/java/com/sparta/product/infrastructure/utils/ProductSearchDto.java index 214e08f5..949d2e2f 100644 --- a/service/product/server/src/main/java/com/sparta/product/infrastructure/elasticsearch/dto/ProductSearchDto.java +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/ProductSearchDto.java @@ -1,4 +1,4 @@ -package com.sparta.product.infrastructure.elasticsearch.dto; +package com.sparta.product.infrastructure.utils; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.sparta.product.presentation.response.ProductResponse; diff --git a/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/RedisUtils.java b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/RedisUtils.java new file mode 100644 index 00000000..9c29ff27 --- /dev/null +++ b/service/product/server/src/main/java/com/sparta/product/infrastructure/utils/RedisUtils.java @@ -0,0 +1,7 @@ +package com.sparta.product.infrastructure.utils; + +public class RedisUtils { + public static String getRedisKeyOfPreOrder(long preOrderId) { + return "preorder.request.%s".formatted(preOrderId); + } +} diff --git a/service/product/server/src/main/java/com/sparta/product/presentation/controller/PreOrderController.java b/service/product/server/src/main/java/com/sparta/product/presentation/controller/PreOrderController.java index 71aec3f9..46af5f01 100644 --- a/service/product/server/src/main/java/com/sparta/product/presentation/controller/PreOrderController.java +++ b/service/product/server/src/main/java/com/sparta/product/presentation/controller/PreOrderController.java @@ -1,6 +1,8 @@ package com.sparta.product.presentation.controller; +import com.sparta.auth.auth_dto.jwt.JwtClaim; import com.sparta.common.domain.response.ApiResponse; +import com.sparta.product.application.preorder.PreOrderFacadeService; import com.sparta.product.application.preorder.PreOrderService; import com.sparta.product.domain.model.PreOrderState; import com.sparta.product.presentation.request.PreOrderCreateRequest; @@ -10,7 +12,9 @@ import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -26,8 +30,19 @@ @RequestMapping("/api/preorders") @RequiredArgsConstructor @Validated +@Slf4j public class PreOrderController { private final PreOrderService preOrderService; + private final PreOrderFacadeService preOrderFacadeService; + + @PostMapping("/{preOrderId}/order") + public ApiResponse preOrder( + @NotNull @PathVariable("preOrderId") Long preOrderId, + @NotNull @RequestParam("addressId") Long addressId, + @AuthenticationPrincipal JwtClaim jwtClaim) { + preOrderFacadeService.preOrder(preOrderId, addressId, jwtClaim.getUserId()); + return ApiResponse.ok(); + } @PostMapping public ApiResponse createPreOrder(@RequestBody @Valid PreOrderCreateRequest request) { diff --git a/service/product/server/src/main/java/com/sparta/product/presentation/controller/ProductSearchController.java b/service/product/server/src/main/java/com/sparta/product/presentation/controller/ProductSearchController.java index ccb80a14..45287dfb 100644 --- a/service/product/server/src/main/java/com/sparta/product/presentation/controller/ProductSearchController.java +++ b/service/product/server/src/main/java/com/sparta/product/presentation/controller/ProductSearchController.java @@ -3,7 +3,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import com.sparta.common.domain.response.ApiResponse; import com.sparta.product.application.product.ElasticsearchService; -import com.sparta.product.infrastructure.elasticsearch.dto.ProductSearchDto; +import com.sparta.product.infrastructure.utils.ProductSearchDto; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import java.io.IOException; diff --git a/service/product/server/src/main/java/com/sparta/product/presentation/exception/ProductErrorCode.java b/service/product/server/src/main/java/com/sparta/product/presentation/exception/ProductErrorCode.java index 45647a8f..360f5496 100644 --- a/service/product/server/src/main/java/com/sparta/product/presentation/exception/ProductErrorCode.java +++ b/service/product/server/src/main/java/com/sparta/product/presentation/exception/ProductErrorCode.java @@ -10,7 +10,14 @@ public enum ProductErrorCode { NOT_FOUND_PRODUCT(HttpStatus.NOT_FOUND, "상품이 존재하지 않습니다"), NOT_FOUND_CATEGORY(HttpStatus.NOT_FOUND, "카테고리가 존재하지 않습니다"), NOT_FOUND_PREORDER(HttpStatus.NOT_FOUND, "사전예약정보가 존재하지 않습니다"), + PREORDER_QUANTITY_CONFLICT(HttpStatus.CONFLICT, "예약가능수량이 재고량보다 많습니다"), + NOT_OPEN_FOR_PREORDER(HttpStatus.CONFLICT, "오픈된 사전예약건이 아닙니다"), + CLOSED_PREORDER(HttpStatus.GONE, "예약기간이 종료되었습니다"), + + INVALID_PREORDER_DATETIME(HttpStatus.FORBIDDEN, "예약가능한 시간이 아닙니다"), + ALREADY_PREORDER(HttpStatus.CONFLICT, "이미 해당 사전예약 주문이 완료되었습니다"), + EXCEED_PREORDER_QUANTITY(HttpStatus.CONFLICT, "사전예약가능 수량을 초과하였습니다"), INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "내부 서버 오류입니다"); diff --git a/service/product/server/src/main/resources/application-local.yml b/service/product/server/src/main/resources/application-local.yml index eef9d9d4..1e7bca29 100644 --- a/service/product/server/src/main/resources/application-local.yml +++ b/service/product/server/src/main/resources/application-local.yml @@ -30,8 +30,26 @@ spring: data: redis: - host: product-cache - port: 6380 + host: localhost + port: 6379 + + kafka: + bootstrap-servers: localhost:9092 + listener: + ack-mode: MANUAL + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: product + auto-offset-reset: latest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + trusted: + packages: "*" product: search-index: "sasaping-ecommerce-products"