diff --git a/services/order/src/main/java/com/ticketPing/order/application/client/PerformanceClient.java b/services/order/src/main/java/com/ticketPing/order/application/client/PerformanceClient.java index ad876c3f..8a0c1888 100644 --- a/services/order/src/main/java/com/ticketPing/order/application/client/PerformanceClient.java +++ b/services/order/src/main/java/com/ticketPing/order/application/client/PerformanceClient.java @@ -1,6 +1,8 @@ package com.ticketPing.order.application.client; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestParam; import performance.OrderSeatResponse; import performance.SeatResponse; import response.CommonResponse; @@ -10,5 +12,7 @@ public interface PerformanceClient { ResponseEntity> getOrderInfo(UUID userId, UUID scheduleId, UUID seatId); + ResponseEntity> extendPreReserveTTL(UUID scheduleId, UUID seatId); + ResponseEntity> updateSeatState(UUID seatId, Boolean seatState); } 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 d7bda828..33bf3cb4 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 @@ -2,6 +2,7 @@ import com.ticketPing.order.application.client.PerformanceClient; import com.ticketPing.order.application.dtos.OrderResponse; +import com.ticketPing.order.common.exception.OrderExceptionCase; import com.ticketPing.order.domain.model.entity.Order; import com.ticketPing.order.domain.model.entity.OrderSeat; import com.ticketPing.order.domain.model.enums.OrderStatus; @@ -42,18 +43,17 @@ public OrderResponse createOrder(UUID scheduleId, UUID seatId, UUID userId) { return OrderResponse.from(order); } - @Transactional(readOnly = true) - public OrderResponse validateOrder(UUID orderId, UUID userId) { - Order order = findOrderById(orderId); - // TODO: 본인 좌석 인지 확인, 좌석 선점 TTL 갱신 - return OrderResponse.from(order); - } - public List getUserOrders(UUID userId) { List orders = orderRepository.findByUserId(userId); return orders.stream().map(OrderResponse::from).toList(); } + public OrderResponse validateOrderAndExtendTTL(UUID orderId, UUID userId) { + Order order = validateOrder(orderId, userId); + performanceClient.extendPreReserveTTL(order.getScheduleId(), order.getOrderSeat().getSeatId()); + return OrderResponse.from(order); + } + @Transactional public void updateOrderStatus(UUID orderId, UUID paymentId) { Order order = findOrderById(orderId); @@ -63,7 +63,7 @@ public void updateOrderStatus(UUID orderId, UUID paymentId) { UUID scheduleId = order.getScheduleId(); UUID seatId = order.getOrderSeat().getSeatId(); - performanceClient.updateSeatState(order.getOrderSeat().getSeatId(), true); // 1. 좌석 db 업데이트 (kafka로 변경?) + performanceClient.updateSeatState(order.getOrderSeat().getSeatId(), true); String ttlKey = TTL_PREFIX + scheduleId + ":" + seatId + ":" + orderId; redisRepository.deleteKey(ttlKey); @@ -74,11 +74,6 @@ public void updateOrderStatus(UUID orderId, UUID paymentId) { publishOrderCompletedEvent(order.getUserId(), performanceId); } - private void publishOrderCompletedEvent(UUID userId, UUID performanceId) { - val event = OrderCompletedEvent.create(userId, performanceId); - eventApplicationService.publishOrderCompletedEvent(event); - } - @Transactional private Order saveOrderWithOrderSeat(UUID userId, OrderSeatResponse orderData) { Order order = Order.from(userId, orderData); @@ -98,12 +93,23 @@ private Order findOrderById(UUID orderId){ private void validateDuplicateOrder(UUID seatId) { List duplicateOrders = orderRepository.findByOrderSeatSeatId(seatId) .stream() - .filter(o -> o.getOrderStatus().equals(OrderStatus.PENDING) || - o.getOrderStatus().equals(OrderStatus.COMPLETED)) + .filter(o -> o.getOrderStatus().equals(OrderStatus.PENDING) || o.getOrderStatus().equals(OrderStatus.COMPLETED)) .toList(); if(!duplicateOrders.isEmpty()) throw new ApplicationException(SEAT_ALREADY_TAKEN); } + private Order validateOrder(UUID orderId, UUID userId) { + Order order = findOrderById(orderId); + if(!order.getOrderStatus().equals(OrderStatus.PENDING) || !order.getUserId().equals(userId)) + throw new ApplicationException(OrderExceptionCase.INVALID_ORDER); + return order; + } + + private void publishOrderCompletedEvent(UUID userId, UUID performanceId) { + val event = OrderCompletedEvent.create(userId, performanceId); + eventApplicationService.publishOrderCompletedEvent(event); + } + } diff --git a/services/order/src/main/java/com/ticketPing/order/common/exception/OrderExceptionCase.java b/services/order/src/main/java/com/ticketPing/order/common/exception/OrderExceptionCase.java index 3745e1b1..f2b4c138 100644 --- a/services/order/src/main/java/com/ticketPing/order/common/exception/OrderExceptionCase.java +++ b/services/order/src/main/java/com/ticketPing/order/common/exception/OrderExceptionCase.java @@ -13,6 +13,7 @@ public enum OrderExceptionCase implements ErrorCase { INVALID_TTL_NAME(HttpStatus.BAD_REQUEST, "유효하지 않은 TTL 명입니다."), NOT_FOUND_ORDER_ID_IN_TTL(HttpStatus.NOT_FOUND,"TTL 정보에서 order_id를 찾을 수 없습니다."), SEAT_ALREADY_TAKEN(HttpStatus.BAD_REQUEST, "중복된 주문입니다."), + INVALID_ORDER(HttpStatus.BAD_REQUEST, "유효하지 않은 주문입니다."), SEAT_CACHE_NOT_FOUND(HttpStatus.NOT_FOUND, "레디스에 공연관련 정보가 캐싱되어 있지 않습니다."), ORDER_STATUS_UNKNOWN(HttpStatus.CONFLICT,"저장된 enum 상태값을 사용해야 합니다."), INVALID_SEAT_STATUS(HttpStatus.BAD_REQUEST, "유효하지 않은 좌석 상태를 사용했습니다."); diff --git a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java index 4188afe5..1cf34662 100644 --- a/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java +++ b/services/order/src/main/java/com/ticketPing/order/infrastructure/client/PerformanceFeignClient.java @@ -13,11 +13,15 @@ @FeignClient(name = "performance") public interface PerformanceFeignClient extends PerformanceClient { - @GetMapping("/api/v1/seats/{seatId}/order-info") + @GetMapping("/api/v1/client/seats/{seatId}/order-info") ResponseEntity> getOrderInfo(@RequestHeader("X_USER_ID") UUID userId, @RequestParam("scheduleId") UUID scheduleId, @PathVariable("seatId") UUID seatId); + @PostMapping("/api/v1/client/seats/{seatId}/extend-ttl") + ResponseEntity> extendPreReserveTTL(@RequestParam("scheduleId") UUID scheduleId, + @PathVariable("seatId") UUID seatId); + @PutMapping("/api/v1/seats/{seatId}") ResponseEntity> updateSeatState(@PathVariable("seatId") UUID seatId, @RequestParam("seatState") Boolean seatState); diff --git a/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java b/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java index 18fd8ca8..e7b4b304 100644 --- a/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java +++ b/services/order/src/main/java/com/ticketPing/order/presentation/controller/OrderController.java @@ -39,7 +39,7 @@ public CommonResponse> getUserReservation(@RequestHeader("X_ @PostMapping("/{orderId}/validate") public CommonResponse validateOrder(@RequestHeader("X_USER_ID") UUID userId, @PathVariable("orderId") UUID orderId) { - OrderResponse orderResponse = orderService.validateOrder(orderId, userId); + OrderResponse orderResponse = orderService.validateOrderAndExtendTTL(orderId, userId); return CommonResponse.success(orderResponse); } 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 4ce4dec3..96164e8c 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 @@ -12,6 +12,7 @@ import com.ticketPing.performance.infrastructure.service.LuaScriptService; import exception.ApplicationException; import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.time.Duration; @@ -29,6 +30,9 @@ public class SeatService { private final CacheService cacheService; private final LuaScriptService luaScriptService; + @Value("${seat.pre-reserve-ttl}") + private int PRE_RESERVE_TTL; + public SeatResponse getSeat(UUID id) { Seat seat = seatRepository.findByIdWithSeatCost(id) .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_NOT_FOUND)); @@ -44,7 +48,7 @@ public void cancelPreReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { cacheService.canclePreReserveSeat(scheduleId, seatId); } - public OrderSeatResponse getOrderInfo(UUID scheduleId, UUID seatId, UUID userId) { + public OrderSeatResponse getOrderSeatInfo(UUID scheduleId, UUID seatId, UUID userId) { validatePreserve(scheduleId, seatId, userId); Seat seat = seatRepository.findByIdWithAll(seatId) @@ -53,6 +57,10 @@ public OrderSeatResponse getOrderInfo(UUID scheduleId, UUID seatId, UUID userId) return OrderSeatResponse.of(seat); } + public void extendPreReserveTTL(UUID scheduleId, UUID seatId) { + cacheService.extendPreReserveTTL(scheduleId, seatId, Duration.ofSeconds(PRE_RESERVE_TTL)); + } + public long cacheSeatsForSchedule(Schedule schedule) { List seats = seatRepository.findByScheduleWithSeatCost(schedule); 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 new file mode 100644 index 00000000..62f763e7 --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/common/constants/SeatConstants.java @@ -0,0 +1,14 @@ +package com.ticketPing.performance.common.constants; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +public class SeatConstants { + + public static int PRE_RESERVE_TTL; + + private SeatConstants(@Value("${seat.pre-reserve-ttl}") int preReserveTtl) { + PRE_RESERVE_TTL = preReserveTtl; + } +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/common/exception/SeatExceptionCase.java b/services/performance/src/main/java/com/ticketPing/performance/common/exception/SeatExceptionCase.java index af658a86..569f81b9 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/common/exception/SeatExceptionCase.java +++ b/services/performance/src/main/java/com/ticketPing/performance/common/exception/SeatExceptionCase.java @@ -15,7 +15,8 @@ public enum SeatExceptionCase implements ErrorCase { PRE_RESERVE_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "좌석 선점 과정에서 오류가 발생했습니다."), SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류가 발생했습니다."), USER_NOT_MATCH(HttpStatus.BAD_REQUEST, "본인이 선점한 좌석이 아닙니다."), - SEAT_NOT_PRE_RESERVED(HttpStatus.BAD_REQUEST, "좌석이 선점 상태가 아닙니다."); + SEAT_NOT_PRE_RESERVED(HttpStatus.BAD_REQUEST, "좌석이 선점 상태가 아닙니다."), + TTL_NOT_EXIST(HttpStatus.BAD_REQUEST, "좌석 선점 상태가 아닙니다."); private final HttpStatus httpStatus; private final String message; diff --git a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java index 19105e79..a5a7889a 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java +++ b/services/performance/src/main/java/com/ticketPing/performance/infrastructure/service/CacheService.java @@ -4,6 +4,7 @@ import com.ticketPing.performance.domain.model.entity.SeatCache; import exception.ApplicationException; import lombok.RequiredArgsConstructor; +import org.redisson.api.RBucket; import org.redisson.api.RMap; import org.redisson.api.RedissonClient; import org.redisson.codec.JsonJacksonCodec; @@ -44,6 +45,15 @@ public SeatCache getSeatFromCache(UUID scheduleId, UUID seatId) { .orElseThrow(() -> new ApplicationException(SeatExceptionCase.SEAT_CACHE_NOT_FOUND)); } + public void extendPreReserveTTL(UUID scheduleId, UUID seatId, Duration ttl) { + String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; + RBucket bucket = redissonClient.getBucket(ttlKey); + if (!bucket.isExists()) { + throw new ApplicationException(SeatExceptionCase.TTL_NOT_EXIST); + } + bucket.expire(ttl); + } + public void canclePreReserveSeat(UUID scheduleId, UUID seatId) { String key = "seat:{" + scheduleId + "}"; RMap seatCacheMap = redissonClient.getMap(key, JsonJacksonCodec.INSTANCE); 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 2367dc4c..08be2fa6 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 @@ -6,21 +6,19 @@ import org.redisson.api.RScript; import org.redisson.api.RedissonClient; import org.redisson.client.codec.StringCodec; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.UUID; +import static com.ticketPing.performance.common.constants.SeatConstants.PRE_RESERVE_TTL; + @Service @RequiredArgsConstructor public class LuaScriptService { private final RedissonClient redissonClient; private final String preReserveScript; - @Value("${seat.pre-reserve-ttl}") - private int PRE_RESERVE_TTL; - public void preReserveSeat(UUID scheduleId, UUID seatId, UUID userId) { String hashKey = "seat:{" + scheduleId + "}"; String ttlKey = "ttl:{" + scheduleId + "}:" + seatId; diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java new file mode 100644 index 00000000..e1ae2c0a --- /dev/null +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatClientController.java @@ -0,0 +1,40 @@ +package com.ticketPing.performance.presentation.controller; + +import com.ticketPing.performance.application.dtos.OrderSeatResponse; +import com.ticketPing.performance.application.service.SeatService; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import response.CommonResponse; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/client/seats") +@RequiredArgsConstructor +public class SeatClientController { + + private final SeatService seatService; + + @Operation(summary = "좌석 주문 정보 조회 (order 서비스에서 호출용)") + @GetMapping("/{seatId}/order-info") + public ResponseEntity> getOrderSeatInfo(@RequestHeader("X_USER_ID") UUID userId, + @RequestParam("scheduleId") UUID scheduleId, + @PathVariable("seatId") UUID seatId) { + OrderSeatResponse orderSeatResponse = seatService.getOrderSeatInfo(scheduleId, seatId, userId); + return ResponseEntity + .status(200) + .body(CommonResponse.success(orderSeatResponse)); + } + + @Operation(summary = "좌석 선점 ttl 연강 (order 서비스 호출용)") + @PostMapping("/{seatId}/extend-ttl") + ResponseEntity> extendPreReserveTTL (@RequestParam("scheduleId") UUID scheduleId, + @PathVariable("seatId") UUID seatId) { + seatService.extendPreReserveTTL(scheduleId, seatId); + return ResponseEntity + .status(200) + .body(CommonResponse.success()); + } +} diff --git a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java index d3a5497c..2c6b695f 100644 --- a/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java +++ b/services/performance/src/main/java/com/ticketPing/performance/presentation/controller/SeatController.java @@ -49,15 +49,4 @@ public ResponseEntity> cancelPreReserveSeat(@RequestHeade .status(200) .body(CommonResponse.success()); } - - @Operation(summary = "좌석 주문 정보 조회 (order 서비스에서 호출용)") - @GetMapping("/{seatId}/order-info") - public ResponseEntity> getOrderInfo(@RequestHeader("X_USER_ID") UUID userId, - @RequestParam("scheduleId") UUID scheduleId, - @PathVariable("seatId") UUID seatId) { - OrderSeatResponse orderSeatResponse = seatService.getOrderInfo(scheduleId, seatId, userId); - return ResponseEntity - .status(200) - .body(CommonResponse.success(orderSeatResponse)); - } }