diff --git a/common/core/src/main/resources/application-eureka.yml b/common/core/src/main/resources/application-eureka.yml index f109d302..6b705140 100644 --- a/common/core/src/main/resources/application-eureka.yml +++ b/common/core/src/main/resources/application-eureka.yml @@ -1,4 +1,10 @@ eureka: client: service-url: - defaultZone: ${EUREKA_SERVER} \ No newline at end of file + defaultZone: ${EUREKA_SERVER} + +spring: + cloud: + loadbalancer: + ribbon: + enabled: true \ No newline at end of file diff --git a/common/messaging/src/main/java/messaging/events/OrderFailedEvent.java b/common/messaging/src/main/java/messaging/events/OrderFailedEvent.java new file mode 100644 index 00000000..c505d177 --- /dev/null +++ b/common/messaging/src/main/java/messaging/events/OrderFailedEvent.java @@ -0,0 +1,15 @@ +package messaging.events; + +import java.util.UUID; + +public record OrderFailedEvent( + UUID scheduleId, + UUID seatId +) { + public static OrderFailedEvent create(UUID scheduleId, UUID seatId) { + return new OrderFailedEvent( + scheduleId, + seatId + ); + } +} diff --git a/common/messaging/src/main/java/messaging/events/SeatPreReserveExpiredEvent.java b/common/messaging/src/main/java/messaging/events/SeatPreReserveExpiredEvent.java new file mode 100644 index 00000000..20cea7a4 --- /dev/null +++ b/common/messaging/src/main/java/messaging/events/SeatPreReserveExpiredEvent.java @@ -0,0 +1,17 @@ +package messaging.events; + +import java.util.UUID; + +public record SeatPreReserveExpiredEvent( + UUID scheduleId, + UUID seatId +) { + + public static SeatPreReserveExpiredEvent create(UUID scheduleId, UUID seatId) { + return new SeatPreReserveExpiredEvent( + scheduleId, + seatId + ); + } + +} diff --git a/common/messaging/src/main/java/messaging/topics/OrderTopic.java b/common/messaging/src/main/java/messaging/topics/OrderTopic.java index 95bf6522..5d257fe3 100644 --- a/common/messaging/src/main/java/messaging/topics/OrderTopic.java +++ b/common/messaging/src/main/java/messaging/topics/OrderTopic.java @@ -8,7 +8,8 @@ public enum OrderTopic { COMPLETED_FOR_SEAT_RESERVATION("order-completed-for-seat-reservation"), - COMPLETED_FOR_QUEUE_TOKEN_REMOVAL("order-completed-for-queue-token-removal"); + COMPLETED_FOR_QUEUE_TOKEN_REMOVAL("order-completed-for-queue-token-removal"), + FAILED("order-failed"); private final String topic; diff --git a/common/messaging/src/main/java/messaging/topics/SeatTopic.java b/common/messaging/src/main/java/messaging/topics/SeatTopic.java new file mode 100644 index 00000000..9cfa6fb1 --- /dev/null +++ b/common/messaging/src/main/java/messaging/topics/SeatTopic.java @@ -0,0 +1,13 @@ +package messaging.topics; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum SeatTopic { + + PRE_RESERVE_EXPIRED("pre-reserve-expired"); + + private final String topic; +} diff --git a/services/order/build.gradle b/services/order/build.gradle index b85c0997..aacced62 100644 --- a/services/order/build.gradle +++ b/services/order/build.gradle @@ -38,7 +38,6 @@ dependencies { implementation project(':common:core') implementation project(':common:dtos') implementation project(':common:rdb') - implementation project(':common:caching') implementation project(':common:messaging') implementation project(':common:monitoring') diff --git a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java index ebafa1de..7beeb7f0 100644 --- a/services/order/src/main/java/com/ticketPing/order/OrderApplication.java +++ b/services/order/src/main/java/com/ticketPing/order/OrderApplication.java @@ -7,7 +7,7 @@ @SpringBootApplication @EnableFeignClients -@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "caching", "messaging"}) +@ComponentScan(basePackages = {"com.ticketPing.order", "aop", "exception", "audit", "messaging"}) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); diff --git a/services/order/src/main/java/com/ticketPing/order/application/client/PaymentClient.java b/services/order/src/main/java/com/ticketPing/order/application/client/PaymentClient.java new file mode 100644 index 00000000..5a3063d5 --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/application/client/PaymentClient.java @@ -0,0 +1,12 @@ +package com.ticketPing.order.application.client; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; +import payment.PaymentResponse; +import response.CommonResponse; + +import java.util.UUID; + +public interface PaymentClient { + ResponseEntity> getCompletedPaymentByOrderId(@RequestParam("orderId") UUID orderId); +} diff --git a/services/order/src/main/java/com/ticketPing/order/application/dtos/temp/SeatResponse.java b/services/order/src/main/java/com/ticketPing/order/application/dtos/temp/SeatResponse.java deleted file mode 100644 index eee8a845..00000000 --- a/services/order/src/main/java/com/ticketPing/order/application/dtos/temp/SeatResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.ticketPing.order.application.dtos.temp; - -import com.ticketPing.order.application.enums.SeatStatus; -import lombok.Data; - -import java.util.UUID; - -@Data -public class SeatResponse { - UUID seatId; - Integer row; - Integer col; - String seatState; - String seatRate; - Integer cost; - - public void updateSeatState(SeatStatus seatState) { - this.seatState = seatState.getValue(); - } -} - diff --git a/services/order/src/main/java/com/ticketPing/order/application/enums/SeatStatus.java b/services/order/src/main/java/com/ticketPing/order/application/enums/SeatStatus.java deleted file mode 100644 index 19ed7f40..00000000 --- a/services/order/src/main/java/com/ticketPing/order/application/enums/SeatStatus.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.ticketPing.order.application.enums; - -import com.ticketPing.order.common.exception.OrderExceptionCase; -import exception.ApplicationException; -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -import java.util.Arrays; - -@Getter -@RequiredArgsConstructor -public enum SeatStatus { - AVAILABLE("AVAILABLE"), - HELD("HELD"), - RESERVED("RESERVED"); - - private final String value; - - public static SeatStatus getSeatStatus(final String value) { - return Arrays.stream(SeatStatus.values()) - .filter(t -> t.getValue().equals(value)) - .findAny().orElseThrow(() -> new ApplicationException(OrderExceptionCase.INVALID_SEAT_STATUS)); - } -} diff --git a/services/order/src/main/java/com/ticketPing/order/application/service/EventApplicationService.java b/services/order/src/main/java/com/ticketPing/order/application/service/EventApplicationService.java index 3b883b4d..eea4946f 100644 --- a/services/order/src/main/java/com/ticketPing/order/application/service/EventApplicationService.java +++ b/services/order/src/main/java/com/ticketPing/order/application/service/EventApplicationService.java @@ -2,6 +2,7 @@ import messaging.events.OrderCompletedForQueueTokenRemovalEvent; import messaging.events.OrderCompletedForSeatReservationEvent; +import messaging.events.OrderFailedEvent; import messaging.utils.EventSerializer; import lombok.RequiredArgsConstructor; import org.springframework.kafka.core.KafkaTemplate; @@ -24,4 +25,9 @@ public void publishForQueueTokenRemoval(OrderCompletedForQueueTokenRemovalEvent kafkaTemplate.send(OrderTopic.COMPLETED_FOR_QUEUE_TOKEN_REMOVAL.getTopic(), message); } + public void publishOrderFailed(OrderFailedEvent event) { + String message = EventSerializer.serialize(event); + kafkaTemplate.send(OrderTopic.FAILED.getTopic(), message); + } + } diff --git a/services/order/src/main/java/com/ticketPing/order/application/service/OrderService.java b/services/order/src/main/java/com/ticketPing/order/application/service/OrderService.java index d88beaa9..02622b16 100644 --- a/services/order/src/main/java/com/ticketPing/order/application/service/OrderService.java +++ b/services/order/src/main/java/com/ticketPing/order/application/service/OrderService.java @@ -1,5 +1,6 @@ package com.ticketPing.order.application.service; +import com.ticketPing.order.application.client.PaymentClient; import com.ticketPing.order.application.client.PerformanceClient; import com.ticketPing.order.application.dtos.OrderResponse; import com.ticketPing.order.common.exception.OrderExceptionCase; @@ -9,12 +10,13 @@ import com.ticketPing.order.domain.repository.OrderRepository; import com.ticketPing.order.presentation.request.CreateOrderRequest; import exception.ApplicationException; +import feign.FeignException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; import messaging.events.OrderCompletedForQueueTokenRemovalEvent; import messaging.events.OrderCompletedForSeatReservationEvent; -import org.springframework.data.domain.Page; +import messaging.events.OrderFailedEvent; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; @@ -22,6 +24,7 @@ import performance.OrderSeatResponse; import java.util.List; +import java.util.Optional; import java.util.UUID; import static com.ticketPing.order.common.exception.OrderExceptionCase.DUPLICATED_ORDER; @@ -35,6 +38,7 @@ public class OrderService { private final OrderRepository orderRepository; private final EventApplicationService eventApplicationService; private final PerformanceClient performanceClient; + private final PaymentClient paymentClient; @Transactional public OrderResponse createOrder(CreateOrderRequest createOrderRequest, UUID userId) { @@ -58,6 +62,23 @@ public void validateOrderAndExtendTTL(UUID orderId, UUID userId) { performanceClient.extendPreReserveTTL(order.getScheduleId(), order.getOrderSeat().getSeatId()); } + @Transactional + public void failOrder(UUID scheduleId, UUID seatId) { + Optional optionalOrder = orderRepository.findByOrderSeatSeatIdAndOrderStatus(seatId, OrderStatus.PENDING); + + if (optionalOrder.isPresent()) { + Order order = optionalOrder.get(); + try { + paymentClient.getCompletedPaymentByOrderId(order.getId()); + } catch (FeignException.NotFound e) { + order.fail(); + publishOrderFailed(scheduleId, seatId); + } + } else { + publishOrderFailed(scheduleId, seatId); + } + } + @Transactional public void completeOrder(UUID orderId, UUID paymentId) { Order order = findOrderById(orderId); @@ -112,4 +133,9 @@ private void publishForQueueTokenRemoval(UUID userId, UUID performanceId) { val event = OrderCompletedForQueueTokenRemovalEvent.create(userId, performanceId); eventApplicationService.publishForQueueTokenRemoval(event); } + + private void publishOrderFailed(UUID scheduleId, UUID seatId) { + val event = OrderFailedEvent.create(scheduleId, seatId); + eventApplicationService.publishOrderFailed(event); + } } diff --git a/services/order/src/main/java/com/ticketPing/order/domain/model/entity/Order.java b/services/order/src/main/java/com/ticketPing/order/domain/model/entity/Order.java index cb3292ca..d5fcd10b 100644 --- a/services/order/src/main/java/com/ticketPing/order/domain/model/entity/Order.java +++ b/services/order/src/main/java/com/ticketPing/order/domain/model/entity/Order.java @@ -63,4 +63,7 @@ public void complete(UUID paymentId){ this.paymentId = paymentId; } + public void fail() { + this.orderStatus = OrderStatus.FAIL; + } } diff --git a/services/order/src/main/java/com/ticketPing/order/domain/repository/OrderRepository.java b/services/order/src/main/java/com/ticketPing/order/domain/repository/OrderRepository.java index 064179cf..1f8a5187 100644 --- a/services/order/src/main/java/com/ticketPing/order/domain/repository/OrderRepository.java +++ b/services/order/src/main/java/com/ticketPing/order/domain/repository/OrderRepository.java @@ -14,6 +14,8 @@ public interface OrderRepository { Optional findById(UUID orderId); + Optional findByOrderSeatSeatIdAndOrderStatus(UUID seatId, OrderStatus orderStatus); + Optional findByIdAndOrderStatus(UUID orderId, OrderStatus orderStatus); boolean existsByOrderSeatSeatIdAndOrderStatusIn(UUID seatId, List statuses); diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java new file mode 100644 index 00000000..07a90da0 --- /dev/null +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PaymentFeignClient.java @@ -0,0 +1,18 @@ +package com.ticketPing.order.infrastructure.client; + +import com.ticketPing.order.application.client.PaymentClient; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import payment.PaymentResponse; +import response.CommonResponse; + +import java.util.UUID; + +@FeignClient(name = "payment") +public interface PaymentFeignClient extends PaymentClient { + @GetMapping("/api/v1/payments/completed") + ResponseEntity> getCompletedPaymentByOrderId(@RequestParam("orderId") UUID orderId); + +} diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/config/RedisMessageListenerConfig.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/config/RedisMessageListenerConfig.java deleted file mode 100644 index db9a4482..00000000 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/config/RedisMessageListenerConfig.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.ticketPing.order.infrastructure.config; - -import com.ticketPing.order.infrastructure.listener.RedisKeyExpiredListener; -import java.util.concurrent.Executor; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.listener.ChannelTopic; -import org.springframework.data.redis.listener.RedisMessageListenerContainer; -import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; - -@Configuration -public class RedisMessageListenerConfig { - - @Bean(name = "redisMessageTaskExecutor") - public Executor redisMessageTaskExecutor() { - ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); - threadPoolTaskExecutor.setCorePoolSize(2); - threadPoolTaskExecutor.setMaxPoolSize(4); - return threadPoolTaskExecutor; - } - - @Bean - public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, - RedisKeyExpiredListener redisKeyExpiredListener) { - RedisMessageListenerContainer container = new RedisMessageListenerContainer(); - container.setConnectionFactory(connectionFactory); - container.addMessageListener(redisKeyExpiredListener, new ChannelTopic("__keyevent@0__:expired")); - container.setTaskExecutor(redisMessageTaskExecutor()); - return container; - } - -} \ No newline at end of file diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/EventConsumer.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/EventConsumer.java index 02a7b190..74ed7799 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/EventConsumer.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/EventConsumer.java @@ -3,6 +3,7 @@ import com.ticketPing.order.application.service.OrderService; import messaging.events.PaymentCompletedEvent; import lombok.RequiredArgsConstructor; +import messaging.events.SeatPreReserveExpiredEvent; import messaging.utils.EventLogger; import messaging.utils.EventSerializer; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -24,4 +25,12 @@ public void handlePaymentCompletedEvent(ConsumerRecord record, A acknowledgment.acknowledge(); } + @KafkaListener(topics = "pre-reserve-expired", groupId = "order-group") + public void handleSeatPreReserveExpiredEvent(ConsumerRecord record, Acknowledgment acknowledgment) { + EventLogger.logReceivedMessage(record); + SeatPreReserveExpiredEvent event = EventSerializer.deserialize(record.value(), SeatPreReserveExpiredEvent.class); + orderService.failOrder(event.scheduleId(), event.seatId()); + acknowledgment.acknowledge(); + } + } \ No newline at end of file diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/RedisKeyExpiredListener.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/RedisKeyExpiredListener.java deleted file mode 100644 index cb988e7c..00000000 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/listener/RedisKeyExpiredListener.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.ticketPing.order.infrastructure.listener; - -import caching.repository.RedisRepository; -import com.ticketPing.order.application.dtos.temp.SeatResponse; -import com.ticketPing.order.application.enums.SeatStatus; -import com.ticketPing.order.domain.model.entity.Order; -import com.ticketPing.order.domain.model.enums.OrderStatus; -import com.ticketPing.order.infrastructure.repository.OrderRepository; -import exception.ApplicationException; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.redis.connection.Message; -import org.springframework.data.redis.connection.MessageListener; -import org.springframework.stereotype.Component; - -import java.util.UUID; - -import static com.ticketPing.order.common.exception.OrderExceptionCase.INVALID_TTL_NAME; -import static com.ticketPing.order.common.exception.OrderExceptionCase.NOT_FOUND_ORDER_ID_IN_TTL; - -@Slf4j -@Component -@RequiredArgsConstructor -public class RedisKeyExpiredListener implements MessageListener { - private final RedisRepository redisRepository; - private final OrderRepository orderRepository; - - @Override - public void onMessage(Message message, byte[] pattern) { - String expiredKey = message.toString(); // 만료된 키 - log.info("Expired key: {}", expiredKey); - - String[] parts = expiredKey.split(":"); - if (parts.length != 5) - throw new ApplicationException(INVALID_TTL_NAME); - - String scheduleId = parts[2]; - String seatId = parts[3]; - String orderId = parts[4]; - - updateRedisSeatState(scheduleId, seatId); - updateOrderStatus(orderId); - } - - private void updateRedisSeatState(String scheduleId, String seatId) { - String key = "seat:" + scheduleId + ":" + seatId; - SeatResponse seatResponse = redisRepository.getValueAsClass(key, SeatResponse.class); - seatResponse.updateSeatState(SeatStatus.AVAILABLE); - redisRepository.setValue(key, seatResponse); - } - - private void updateOrderStatus(String orderId) { - Order order = orderRepository.findById(UUID.fromString(orderId)) - .orElseThrow(() -> new ApplicationException(NOT_FOUND_ORDER_ID_IN_TTL)); - - order.updateOrderStatus(OrderStatus.FAIL); - orderRepository.save(order); - } -} diff --git a/services/order/src/main/resources/application.yml b/services/order/src/main/resources/application.yml index 640a8479..22ffeb74 100644 --- a/services/order/src/main/resources/application.yml +++ b/services/order/src/main/resources/application.yml @@ -12,7 +12,6 @@ spring: import: - "classpath:application-eureka.yml" - "classpath:application-jpa.yml" - - "classpath:application-redis.yml" - "classpath:application-kafka.yml" - "classpath:application-monitoring.yml" diff --git a/services/payment/src/main/java/com/ticketPing/payment/application/service/PaymentApplicationService.java b/services/payment/src/main/java/com/ticketPing/payment/application/service/PaymentApplicationService.java index a9ea9568..4f7a13b4 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/application/service/PaymentApplicationService.java +++ b/services/payment/src/main/java/com/ticketPing/payment/application/service/PaymentApplicationService.java @@ -80,5 +80,9 @@ public PaymentResponse getPayment(UUID paymentId) { return PaymentResponse.from(payment); } + public PaymentResponse getCompletedPaymentByOrderId(UUID orderId) { + Payment payment = paymentDomainService.getCompletedPaymentByOrderId(orderId); + return PaymentResponse.from(payment); + } } diff --git a/services/payment/src/main/java/com/ticketPing/payment/domain/repository/PaymentRepository.java b/services/payment/src/main/java/com/ticketPing/payment/domain/repository/PaymentRepository.java index d71ddb6a..d3ad7006 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/domain/repository/PaymentRepository.java +++ b/services/payment/src/main/java/com/ticketPing/payment/domain/repository/PaymentRepository.java @@ -1,10 +1,13 @@ package com.ticketPing.payment.domain.repository; import com.ticketPing.payment.domain.model.entity.Payment; +import com.ticketPing.payment.domain.model.enums.PaymentStatus; + import java.util.Optional; import java.util.UUID; public interface PaymentRepository { void save(Payment payment); Optional findById(UUID paymentId); + Optional findByOrderIdAndStatus(UUID orderId, PaymentStatus status); } diff --git a/services/payment/src/main/java/com/ticketPing/payment/domain/service/PaymentDomainService.java b/services/payment/src/main/java/com/ticketPing/payment/domain/service/PaymentDomainService.java index 0aea3aeb..17af2ac9 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/domain/service/PaymentDomainService.java +++ b/services/payment/src/main/java/com/ticketPing/payment/domain/service/PaymentDomainService.java @@ -3,6 +3,7 @@ import static com.ticketPing.payment.common.exception.PaymentErrorCase.PAYMENT_NOT_FOUND; import com.ticketPing.payment.domain.model.entity.Payment; +import com.ticketPing.payment.domain.model.enums.PaymentStatus; import com.ticketPing.payment.domain.repository.PaymentRepository; import exception.ApplicationException; import java.util.UUID; @@ -27,4 +28,8 @@ public Payment findPayment(UUID paymentId) { .orElseThrow(() -> new ApplicationException(PAYMENT_NOT_FOUND)); } + public Payment getCompletedPaymentByOrderId(UUID orderId) { + return paymentRepository.findByOrderIdAndStatus(orderId, PaymentStatus.COMPLETED) + .orElseThrow(() -> new ApplicationException(PAYMENT_NOT_FOUND)); + } } \ No newline at end of file diff --git a/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentJpaRepository.java b/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentJpaRepository.java index 81aa2ec7..2f6f1f5e 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentJpaRepository.java +++ b/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentJpaRepository.java @@ -1,6 +1,7 @@ package com.ticketPing.payment.infrastructure.repository; import com.ticketPing.payment.domain.model.entity.Payment; +import com.ticketPing.payment.domain.model.enums.PaymentStatus; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; @@ -8,4 +9,5 @@ public interface PaymentJpaRepository extends JpaRepository { Optional findById(UUID paymentId); + Optional findByOrderIdAndStatus(UUID orderId, PaymentStatus status); } diff --git a/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentRepositoryImpl.java b/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentRepositoryImpl.java index fa5f23d3..0fcca9ba 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentRepositoryImpl.java +++ b/services/payment/src/main/java/com/ticketPing/payment/infrastructure/repository/PaymentRepositoryImpl.java @@ -1,6 +1,7 @@ package com.ticketPing.payment.infrastructure.repository; import com.ticketPing.payment.domain.model.entity.Payment; +import com.ticketPing.payment.domain.model.enums.PaymentStatus; import com.ticketPing.payment.domain.repository.PaymentRepository; import java.util.Optional; import java.util.UUID; @@ -23,4 +24,9 @@ public Optional findById(UUID paymentId) { return paymentJpaRepository.findById(paymentId); } + @Override + public Optional findByOrderIdAndStatus(UUID orderId, PaymentStatus status) { + return paymentJpaRepository.findByOrderIdAndStatus(orderId, status); + } + } diff --git a/services/payment/src/main/java/com/ticketPing/payment/presentation/controller/PaymentController.java b/services/payment/src/main/java/com/ticketPing/payment/presentation/controller/PaymentController.java index 1357d6ce..2c8e33ad 100644 --- a/services/payment/src/main/java/com/ticketPing/payment/presentation/controller/PaymentController.java +++ b/services/payment/src/main/java/com/ticketPing/payment/presentation/controller/PaymentController.java @@ -39,4 +39,13 @@ public ResponseEntity> getPayment( .body(success(paymentApplicationService.getPayment(paymentId))); } + @Operation(summary = "성공 예매 확인") + @GetMapping("/completed") + public ResponseEntity> getCompletedPaymentByOrderId( + @Valid @RequestParam("orderId") UUID orderId) { + return ResponseEntity + .status(200) + .body(success(paymentApplicationService.getCompletedPaymentByOrderId(orderId))); + } + } \ No newline at end of file diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java b/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java index 345ef60a..b6ca6eee 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/scheduler/SeatCacheScheduler.java @@ -9,22 +9,24 @@ import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import static com.ticketPing.performance.common.constants.SeatConstants.CACHE_SCHEDULER_LOCK_KEY; + @Slf4j @Component @RequiredArgsConstructor public class SeatCacheScheduler { + private final PerformanceService performanceService; private final DistributedLockService lockService; private final NotificationService notificationService; - private static final String LOCK_KEY = "SchedulerLock"; private static final int LOCK_TIMEOUT = 300; @Scheduled(cron = "0 0/10 * * * *") public void runScheduler() { log.info("Scheduler triggered"); try { - boolean executed = lockService.executeWithLock(LOCK_KEY, 0, LOCK_TIMEOUT, this::cacheSeatsForUpcomingPerformance); + boolean executed = lockService.executeWithLock(CACHE_SCHEDULER_LOCK_KEY, 0, LOCK_TIMEOUT, this::cacheSeatsForUpcomingPerformance); if (!executed) { log.warn("Another server is running the scheduler"); } diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/EventApplicationService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/EventApplicationService.java new file mode 100644 index 00000000..6daedc14 --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/EventApplicationService.java @@ -0,0 +1,19 @@ +package com.ticketPing.performance.application.service; + +import lombok.RequiredArgsConstructor; +import messaging.events.SeatPreReserveExpiredEvent; +import messaging.topics.SeatTopic; +import messaging.utils.EventSerializer; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class EventApplicationService { + private final KafkaTemplate kafkaTemplate; + + public void publishSeatPreReserveExpiredEvent(SeatPreReserveExpiredEvent event) { + String message = EventSerializer.serialize(event); + kafkaTemplate.send(SeatTopic.PRE_RESERVE_EXPIRED.getTopic(), message); + } +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java index 441c6aee..277fc9a8 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/PerformanceService.java @@ -33,7 +33,7 @@ public PerformanceResponse getPerformance(UUID performanceId) { } public Slice getAllPerformances(Pageable pageable) { - return performanceRepository.findAll(pageable) + return performanceRepository.findAllWithPerformanceHall(pageable) .map(PerformanceListResponse::of); } diff --git a/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java b/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java index 6544ee03..68687cb0 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/application/service/SeatService.java @@ -36,9 +36,16 @@ public void preReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { public void cancelPreReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { validatePreserve(scheduleId, seatId, userId); + cacheRepository.deletePreReserveTTL(scheduleId, seatId); cancelPreReserveSeatInCache(scheduleId, seatId); } + public void cancelPreReserveSeatInCache(UUID scheduleId, UUID seatId) { + SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId); + seatCache.cancelPreReserveSeat(); + cacheRepository.putSeatCache(seatCache, scheduleId, seatId); + } + @Transactional public void reserveSeat(String scheduleId, String seatId) { reserveSeatInDB(seatId); @@ -87,13 +94,6 @@ private void validatePreserve(UUID scheduleId, UUID seatId, UUID userId) { throw new ApplicationException(SeatExceptionCase.USER_NOT_MATCH); } - private void cancelPreReserveSeatInCache(UUID scheduleId, UUID seatId) { - cacheRepository.deletePreReserveTTL(scheduleId, seatId); - SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId); - seatCache.cancelPreReserveSeat(); - cacheRepository.putSeatCache(seatCache, scheduleId, seatId); - } - private void reserveSeatInCache(UUID scheduleId, UUID seatId) { cacheRepository.deletePreReserveTTL(scheduleId, seatId); SeatCache seatCache = cacheRepository.getSeatCache(scheduleId, seatId); diff --git a/services/performance/src/main/java/com/ticketPing/performance/common/constants/SeatConstants.java b/services/performance/src/main/java/com/ticketPing/performance/common/constants/SeatConstants.java index 62f763e7..d80373e5 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/common/constants/SeatConstants.java +++ b/services/performance/src/main/java/com/ticketPing/performance/common/constants/SeatConstants.java @@ -6,6 +6,12 @@ @Component public class SeatConstants { + public static final String SEAT_CACHE_KEY = "seat"; + public static final String PRE_RESERVE_SEAT_KEY = "seat-ttl"; + public static final String CACHE_SCHEDULER_LOCK_KEY = "SchedulerLock"; + public final static String PRE_RESERVE_EXPIRE_LOCK_KEY = "PreReserveLock:"; + + public static int PRE_RESERVE_TTL; private SeatConstants(@Value("${seat.pre-reserve-ttl}") int preReserveTtl) { diff --git a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java index f130ed62..9116979c 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/domain/repository/PerformanceRepository.java @@ -11,7 +11,7 @@ public interface PerformanceRepository { Performance save(Performance performance); - Slice findAll(Pageable pageable); + Slice findAllWithPerformanceHall(Pageable pageable); Performance findByName(String name); diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/KafkaTopicConfig.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/KafkaTopicConfig.java new file mode 100644 index 00000000..2b0ce4ba --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/KafkaTopicConfig.java @@ -0,0 +1,20 @@ +package com.ticketPing.performance.infrastructure.config; + +import messaging.topics.SeatTopic; +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; + +@Configuration +public class KafkaTopicConfig { + + @Bean + public NewTopic preReserveExpiredTopic() { + return TopicBuilder.name(SeatTopic.PRE_RESERVE_EXPIRED.getTopic()) + .partitions(3) + .replicas(3) + .build(); + } + +} \ No newline at end of file diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/RedissonMessageListenerConfig.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/RedissonMessageListenerConfig.java new file mode 100644 index 00000000..ffd8dd6a --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/config/RedissonMessageListenerConfig.java @@ -0,0 +1,24 @@ +package com.ticketPing.performance.infrastructure.config; + +import com.ticketPing.performance.infrastructure.listener.RedisKeyExpiredListener; +import lombok.RequiredArgsConstructor; +import org.redisson.api.RTopic; +import org.redisson.api.RedissonClient; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.event.EventListener; + +@Configuration +@RequiredArgsConstructor +public class RedissonMessageListenerConfig { + + private final RedissonClient redissonClient; + private final RedisKeyExpiredListener redisKeyExpiredListener; + + @EventListener(ApplicationReadyEvent.class) + public void addMessageListener() { + RTopic topic = redissonClient.getTopic("__keyevent@0__:expired"); + topic.addListener(String.class, redisKeyExpiredListener); + } + +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/EventConsumer.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/EventConsumer.java index 0c1abcad..2914ed28 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/EventConsumer.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/EventConsumer.java @@ -3,6 +3,7 @@ import com.ticketPing.performance.application.service.SeatService; import lombok.RequiredArgsConstructor; import messaging.events.OrderCompletedForSeatReservationEvent; +import messaging.events.OrderFailedEvent; import messaging.utils.EventLogger; import messaging.utils.EventSerializer; import org.apache.kafka.clients.consumer.ConsumerRecord; @@ -24,4 +25,12 @@ public void handleOrderCompletedForSeatReservationEvent(ConsumerRecord record, Acknowledgment acknowledgment) { + EventLogger.logReceivedMessage(record); + OrderFailedEvent event = EventSerializer.deserialize(record.value(), OrderFailedEvent.class); + seatService.cancelPreReserveSeatInCache(event.scheduleId(), event.seatId()); + acknowledgment.acknowledge(); + } + } diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/RedisKeyExpiredListener.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/RedisKeyExpiredListener.java new file mode 100644 index 00000000..72323476 --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/listener/RedisKeyExpiredListener.java @@ -0,0 +1,57 @@ +package com.ticketPing.performance.infrastructure.listener; + +import com.ticketPing.performance.application.service.EventApplicationService; +import com.ticketPing.performance.infrastructure.service.DistributedLockService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import messaging.events.SeatPreReserveExpiredEvent; +import org.redisson.api.listener.MessageListener; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +import static com.ticketPing.performance.common.constants.SeatConstants.*; + +@Slf4j +@Component +@RequiredArgsConstructor +public class RedisKeyExpiredListener implements MessageListener { + + private final DistributedLockService lockService; + private final EventApplicationService eventApplicationService; + + private static final int LOCK_TIMEOUT = 60; + + @Override + public void onMessage(CharSequence channel, String expiredKey) { + if(expiredKey.startsWith(PRE_RESERVE_SEAT_KEY) && expiredKey.split(":").length == 3) { + log.info("Seat ttl key has expired: {}", expiredKey); + + String scheduleId = expiredKey.split(":")[1].replaceAll("[{}]", ""); + String seatId = expiredKey.split(":")[2]; + String lockKey = CACHE_SCHEDULER_LOCK_KEY + ":" + seatId; + + try { + boolean executed = lockService.executeWithLock(lockKey, 0, LOCK_TIMEOUT, () -> { + publishPreReserveExpire(UUID.fromString(scheduleId), UUID.fromString(seatId)); + }); + + if (!executed) { + log.warn("Another server is running"); + } + + log.info("Successfully handle expired seat TTL key: {}", expiredKey); + } catch (Exception e) { + log.error("Error occurred while handling expired seat TTL key [{}]: {}", expiredKey, e.getMessage(), e); + } + + } + } + + private void publishPreReserveExpire(UUID scheduleId, UUID seatId) { + val event = SeatPreReserveExpiredEvent.create(scheduleId, seatId); + eventApplicationService.publishSeatPreReserveExpiredEvent(event); + } + +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java index 6ba9b68b..154f1ef8 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/CacheRepositoryImpl.java @@ -18,6 +18,7 @@ import java.util.UUID; import static caching.enums.RedisKeyPrefix.AVAILABLE_SEATS; +import static com.ticketPing.performance.common.constants.SeatConstants.*; @Service @RequiredArgsConstructor @@ -27,27 +28,27 @@ public class CacheRepositoryImpl implements CacheRepository { private final LuaScriptService luaScriptService; public void cacheSeats(UUID scheduleId, Map seatMap, Duration ttl) { - String key = "seat:{" + scheduleId + "}"; + String key = SEAT_CACHE_KEY +":{" + scheduleId + "}"; RMap seatCache = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); seatCache.putAll(seatMap); seatCache.expire(ttl); } public Map getSeatCaches(UUID scheduleId) { - String key = "seat:{" + scheduleId + "}"; + String key = SEAT_CACHE_KEY +":{" + scheduleId + "}"; RMap seatCacheRMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); return seatCacheRMap.readAllMap(); } public SeatCache getSeatCache(UUID scheduleId, UUID seatId) { - String seatKey = "seat:{" + scheduleId + "}"; + String seatKey = SEAT_CACHE_KEY +":{" + scheduleId + "}"; RMap seatCacheMap = redissonClient.getMap(seatKey, JsonJacksonCodec.INSTANCE); return Optional.ofNullable(seatCacheMap.get(seatId.toString())) .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_CACHE_NOT_FOUND)); } public void putSeatCache(SeatCache seatCache, UUID scheduleId, UUID seatId) { - String seatKey = "seat:{" + scheduleId + "}"; + String seatKey = SEAT_CACHE_KEY +":{" + scheduleId + "}"; RMap seatCacheMap = redissonClient.getMap(seatKey, JsonJacksonCodec.INSTANCE); seatCacheMap.put(seatId.toString(), seatCache); } @@ -57,14 +58,14 @@ public void preReserveSeatCache(UUID scheduleId, UUID seatId, UUID userId) { } public String getPreReservTTL(UUID scheduleId, UUID seatId) { - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + String ttlKey = PRE_RESERVE_SEAT_KEY + ":{" + scheduleId + "}:" + seatId; RBucket bucket = redissonClient.getBucket(ttlKey); return Optional.ofNullable(bucket.get()) .orElseThrow(() -> new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST)); } public void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl) { - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + String ttlKey = PRE_RESERVE_SEAT_KEY + ":{" + scheduleId + "}:" + seatId; RBucket bucket = redissonClient.getBucket(ttlKey); boolean success = bucket.expire(ttl); @@ -74,7 +75,7 @@ public void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl) { } public void deletePreReserveTTL(UUID scheduleId, UUID seatId) { - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + String ttlKey = PRE_RESERVE_SEAT_KEY + ":{" + scheduleId + "}:" + seatId; RBucket bucket = redissonClient.getBucket(ttlKey); boolean deleted = bucket.delete(); diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/PerformanceJpaRepository.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/PerformanceJpaRepository.java index 8e75cd2f..24187daf 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/PerformanceJpaRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/PerformanceJpaRepository.java @@ -7,6 +7,8 @@ import java.util.UUID; import com.ticketPing.performance.domain.repository.PerformanceRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -14,6 +16,11 @@ @Repository public interface PerformanceJpaRepository extends PerformanceRepository, JpaRepository { + + @Query(value = "SELECT p FROM Performance p LEFT JOIN FETCH p.performanceHall ph", + countQuery = "SELECT count(p) FROM Performance p") + Slice findAllWithPerformanceHall(Pageable pageable); + @Query("SELECT p FROM Performance p " + "LEFT JOIN FETCH p.schedules s " + "WHERE p.id = :id ") diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java index dd95bf7e..b858ed47 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/repository/SeatJpaRepository.java @@ -21,6 +21,7 @@ public interface SeatJpaRepository extends SeatRepository, JpaRepository findByIdWithAll(UUID seatId); } diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/LuaScriptService.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/LuaScriptService.java index 08be2fa6..41a1ae7e 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/LuaScriptService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/LuaScriptService.java @@ -11,7 +11,7 @@ import java.util.Arrays; import java.util.UUID; -import static com.ticketPing.performance.common.constants.SeatConstants.PRE_RESERVE_TTL; +import static com.ticketPing.performance.common.constants.SeatConstants.*; @Service @RequiredArgsConstructor @@ -20,8 +20,8 @@ public class LuaScriptService { private final String preReserveScript; public void preReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { - String hashKey = "seat:{" + scheduleId + "}"; - String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + String hashKey = SEAT_CACHE_KEY +":{" + scheduleId + "}"; + String ttlKey = PRE_RESERVE_SEAT_KEY + ":{" + scheduleId + "}:" + seatId; String response = redissonClient.getScript(StringCodec.INSTANCE) .evalSha(