Skip to content

Commit e3cfba1

Browse files
authored
Merge pull request #218 from eureca-final-capstone-project/develop
메인 배포
2 parents 1ee46a7 + bbccdd7 commit e3cfba1

4 files changed

Lines changed: 206 additions & 0 deletions

File tree

src/main/java/eureca/capstone/project/orchestrator/transaction_feed/controller/BidController.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import eureca.capstone.project.orchestrator.transaction_feed.dto.request.PlaceBidRequestDto;
66
import eureca.capstone.project.orchestrator.transaction_feed.dto.response.GetBidHistoryResponseDto;
77
import eureca.capstone.project.orchestrator.transaction_feed.service.BidService;
8+
import eureca.capstone.project.orchestrator.transaction_feed.service.impl.BidServiceWithLock;
89
import io.swagger.v3.oas.annotations.Operation;
910
import io.swagger.v3.oas.annotations.Parameter;
1011
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -24,6 +25,7 @@
2425
@RequiredArgsConstructor
2526
public class BidController {
2627
private final BidService bidService;
28+
private final BidServiceWithLock bidServiceWithLock;
2729

2830
@Operation(summary = "입찰 내역 조회 API", description = """
2931
## 특정 입찰 판매글의 전체 입찰 내역을 조회합니다.
@@ -98,4 +100,25 @@ public BaseResponseDto<Void> placeBid(
98100
bidService.placeBid(customUserDetailsDto.getEmail(), placeBidRequestDto);
99101
return BaseResponseDto.voidSuccess();
100102
}
103+
104+
@Operation(summary = "[DB Lock] 입찰 참여 API (성능 테스트용)", description = """
105+
## Pessimistic Lock 을 이용한 입찰 API 입니다.
106+
Redis 버전과의 성능 비교를 위해 사용됩니다. 로직은 동일하나 동시성 제어 방식에 차이가 있습니다.
107+
108+
***
109+
110+
### 🔑 권한
111+
* `ROLE_USER` (사용자 로그인 필요)
112+
113+
### ❌ 주요 실패 코드
114+
* Redis 버전과 동일한 실패 코드를 반환합니다. (단, `LUA_SCRIPT_ERROR`는 발생하지 않음)
115+
""")
116+
@PostMapping("/db-lock")
117+
public BaseResponseDto<Void> placeBidWithDbLock(
118+
@AuthenticationPrincipal CustomUserDetailsDto customUserDetailsDto,
119+
@RequestBody PlaceBidRequestDto placeBidRequestDto
120+
) {
121+
bidServiceWithLock.placeBidWithDbLock(customUserDetailsDto.getEmail(), placeBidRequestDto);
122+
return BaseResponseDto.voidSuccess();
123+
}
101124
}

src/main/java/eureca/capstone/project/orchestrator/transaction_feed/repository/custom/BidsRepositoryCustom.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import eureca.capstone.project.orchestrator.transaction_feed.entity.Bids;
44
import eureca.capstone.project.orchestrator.transaction_feed.entity.TransactionFeed;
55
import java.util.List;
6+
import java.util.Optional;
67

78
public interface BidsRepositoryCustom {
89
List<Bids> findBidsWithUserByTransactionFeed(TransactionFeed transactionFeed);
10+
Optional<Bids> findHighestBidByFeed(TransactionFeed feed);
911
}

src/main/java/eureca/capstone/project/orchestrator/transaction_feed/repository/impl/BidsRepositoryImpl.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import eureca.capstone.project.orchestrator.transaction_feed.entity.TransactionFeed;
99
import eureca.capstone.project.orchestrator.transaction_feed.repository.custom.BidsRepositoryCustom;
1010
import java.util.List;
11+
import java.util.Optional;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.stereotype.Repository;
1314

@@ -25,4 +26,16 @@ public List<Bids> findBidsWithUserByTransactionFeed(TransactionFeed transactionF
2526
.orderBy(bids.bidTime.desc())
2627
.fetch();
2728
}
29+
30+
@Override
31+
public Optional<Bids> findHighestBidByFeed(TransactionFeed feed) {
32+
Bids result = queryFactory
33+
.selectFrom(bids)
34+
.where(bids.transactionFeed.eq(feed))
35+
.orderBy(bids.bidAmount.desc())
36+
.limit(1)
37+
.fetchOne();
38+
39+
return Optional.ofNullable(result);
40+
}
2841
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package eureca.capstone.project.orchestrator.transaction_feed.service.impl;
2+
3+
import eureca.capstone.project.orchestrator.alarm.dto.AlarmCreationDto;
4+
import eureca.capstone.project.orchestrator.alarm.service.impl.NotificationProducer;
5+
import eureca.capstone.project.orchestrator.common.entity.Status;
6+
import eureca.capstone.project.orchestrator.common.exception.code.ErrorCode;
7+
import eureca.capstone.project.orchestrator.common.exception.custom.BidException;
8+
import eureca.capstone.project.orchestrator.common.exception.custom.TransactionFeedNotFoundException;
9+
import eureca.capstone.project.orchestrator.common.exception.custom.UserNotFoundException;
10+
import eureca.capstone.project.orchestrator.common.util.SalesTypeManager;
11+
import eureca.capstone.project.orchestrator.common.util.StatusManager;
12+
import eureca.capstone.project.orchestrator.pay.service.UserPayService;
13+
import eureca.capstone.project.orchestrator.transaction_feed.document.TransactionFeedDocument;
14+
import eureca.capstone.project.orchestrator.transaction_feed.dto.request.PlaceBidRequestDto;
15+
import eureca.capstone.project.orchestrator.transaction_feed.entity.Bids;
16+
import eureca.capstone.project.orchestrator.transaction_feed.entity.SalesType;
17+
import eureca.capstone.project.orchestrator.transaction_feed.entity.TransactionFeed;
18+
import eureca.capstone.project.orchestrator.transaction_feed.repository.BidsRepository;
19+
import eureca.capstone.project.orchestrator.transaction_feed.repository.TransactionFeedRepository;
20+
import eureca.capstone.project.orchestrator.transaction_feed.repository.TransactionFeedSearchRepository;
21+
import eureca.capstone.project.orchestrator.user.entity.User;
22+
import eureca.capstone.project.orchestrator.user.repository.UserRepository;
23+
import java.time.LocalDateTime;
24+
import java.util.List;
25+
import java.util.Optional;
26+
import lombok.RequiredArgsConstructor;
27+
import lombok.extern.slf4j.Slf4j;
28+
import org.springframework.stereotype.Service;
29+
import org.springframework.transaction.annotation.Transactional;
30+
31+
@Slf4j
32+
@Service
33+
@RequiredArgsConstructor
34+
public class BidServiceWithLock {
35+
private final UserRepository userRepository;
36+
private final TransactionFeedRepository transactionFeedRepository;
37+
private final TransactionFeedSearchRepository transactionFeedSearchRepository;
38+
private final BidsRepository bidsRepository;
39+
private final UserPayService userPayService;
40+
private final StatusManager statusManager;
41+
private final SalesTypeManager salesTypeManager;
42+
private final NotificationProducer notificationProducer;
43+
44+
@Transactional
45+
public void placeBidWithDbLock(String email, PlaceBidRequestDto request) {
46+
TransactionFeed feed = transactionFeedRepository.findByIdWithLock(request.getTransactionFeedId())
47+
.orElseThrow(TransactionFeedNotFoundException::new);
48+
log.info("[placeBidWithDbLock] 입찰 판매글 비관적 락 적용 ID: {}", feed.getTransactionFeedId());
49+
50+
User bidder = findUserByEmail(email);
51+
log.info("[placeBidWithDbLock] 사용자 ID: {}, 판매글 ID: {}, 입찰금액: {}", bidder.getUserId(), feed.getTransactionFeedId(), request.getBidAmount());
52+
53+
validateBidPrecondition(bidder, feed, request.getBidAmount());
54+
log.info("[placeBidWithDbLock] 입찰 사전 검증 통과");
55+
56+
Optional<Bids> highestBidOptional = bidsRepository.findHighestBidByFeed(feed);
57+
log.info("[placeBidWithDbLock] 입찰 최고가 조회");
58+
59+
if (highestBidOptional.isPresent()) {
60+
Bids highestBid = highestBidOptional.get();
61+
62+
if (request.getBidAmount() <= highestBid.getBidAmount()) {
63+
throw new BidException(ErrorCode.BID_AMOUNT_TOO_LOW);
64+
}
65+
66+
if (highestBid.getUser().getUserId().equals(bidder.getUserId())) {
67+
throw new BidException(ErrorCode.CANNOT_BID_ON_OWN_HIGHEST);
68+
}
69+
} else {
70+
if (request.getBidAmount() < feed.getSalesPrice()) {
71+
throw new BidException(ErrorCode.BID_AMOUNT_TOO_LOW);
72+
}
73+
}
74+
75+
LocalDateTime bidTimeStamp = LocalDateTime.now();
76+
77+
if (highestBidOptional.isPresent()) {
78+
Bids prevHighestBid = highestBidOptional.get();
79+
userPayService.refundPay(prevHighestBid.getUser(), prevHighestBid.getBidAmount());
80+
log.info("[placeBidWithDbLock] 이전 입찰자 페이 환불 완료. 사용자 ID: {}, 환불 금액: {}", prevHighestBid.getUser().getUserId(), prevHighestBid.getBidAmount());
81+
}
82+
83+
userPayService.usePay(bidder, request.getBidAmount());
84+
log.info("[placeBidWithDbLock] 새로운 입찰자 페이 사용 완료. 사용자 ID: {}, 사용 금액: {}", bidder.getUserId(), request.getBidAmount());
85+
86+
saveBidHistory(feed, bidder, request.getBidAmount(), bidTimeStamp);
87+
log.info("입찰 성공 - 사용자 ID: {}, 게시글 ID: {}, 입찰가: {}", bidder.getUserId(), feed.getTransactionFeedId(), request.getBidAmount());
88+
89+
updateFeedDocumentHighestPrice(feed.getTransactionFeedId(), request.getBidAmount());
90+
91+
List<User> participants = bidsRepository.findBidsWithUserByTransactionFeed(feed)
92+
.stream()
93+
.map(Bids::getUser)
94+
.distinct()
95+
.toList();
96+
log.info("[handleBidResult] 입찰 참여자 {}명 조회 완료", participants.size());
97+
98+
for (User participant : participants) {
99+
log.info("transaction_feed_id: {}", feed.getTransactionFeedId());
100+
if (participant.getUserId().equals(bidder.getUserId())) {
101+
notificationProducer.send(AlarmCreationDto.builder()
102+
.userId(participant.getUserId())
103+
.alarmType("입찰 성공")
104+
.transactionFeedId(feed.getTransactionFeedId())
105+
.content("'" + feed.getTitle() + "'를(을) (다챠페이)" + request.getBidAmount() + "원에 입찰했습니다.")
106+
.build());
107+
} else {
108+
notificationProducer.send(AlarmCreationDto.builder()
109+
.userId(participant.getUserId())
110+
.alarmType("입찰 갱신")
111+
.transactionFeedId(feed.getTransactionFeedId())
112+
.content(bidder + "님이 '" + feed.getTitle() + "'를(을) (다챠페이)" + request.getBidAmount() + "원에 입찰했습니다.")
113+
.build());
114+
}
115+
}
116+
log.info("[placeBidWithDbLock] 입찰 알림 전송 완료");
117+
}
118+
119+
private User findUserByEmail(String email) {
120+
return userRepository.findByEmail(email)
121+
.orElseThrow(UserNotFoundException::new);
122+
}
123+
124+
private void validateBidPrecondition(User bidder, TransactionFeed feed, Long bidAmount) {
125+
Status salesStatus = statusManager.getStatus("FEED", "ON_SALE");
126+
SalesType bidSalesType = salesTypeManager.getBidSaleType();
127+
128+
if (feed.getUser().getUserId().equals(bidder.getUserId())) {
129+
throw new BidException(ErrorCode.SELLER_CANNOT_BID);
130+
}
131+
if (!feed.getTelecomCompany().getTelecomCompanyId().equals(bidder.getTelecomCompany().getTelecomCompanyId())) {
132+
throw new BidException(ErrorCode.INVALID_TELECOM_COMPANY);
133+
}
134+
if (!feed.getStatus().equals(salesStatus)) {
135+
throw new BidException(ErrorCode.AUCTION_NOT_ON_SALE);
136+
}
137+
if (!feed.getSalesType().equals(bidSalesType)) {
138+
throw new BidException(ErrorCode.FEED_NOT_AUCTION);
139+
}
140+
if (feed.getExpiresAt().isBefore(LocalDateTime.now())) {
141+
throw new BidException(ErrorCode.AUCTION_EXPIRED);
142+
}
143+
if (bidAmount % 100 != 0) {
144+
throw new BidException(ErrorCode.BID_AMOUNT_100_DIVISIBLE);
145+
}
146+
}
147+
148+
private void saveBidHistory(TransactionFeed feed, User bidder, Long bidAmount, LocalDateTime bidTimeStamp) {
149+
bidsRepository.save(Bids.builder()
150+
.transactionFeed(feed)
151+
.user(bidder)
152+
.bidAmount(bidAmount)
153+
.bidTime(bidTimeStamp)
154+
.build());
155+
}
156+
157+
158+
private void updateFeedDocumentHighestPrice(Long feedId, Long highestPrice) {
159+
try {
160+
TransactionFeedDocument document = transactionFeedSearchRepository.findById(feedId)
161+
.orElseThrow(TransactionFeedNotFoundException::new);
162+
document.updateHighestPrice(highestPrice);
163+
transactionFeedSearchRepository.save(document);
164+
} catch (Exception e) {
165+
log.error("[updateFeedDocumentHighestPrice] Elasticsearch 문서 업데이트 실패. Document ID: {}. Error: {}", feedId, e.getMessage());
166+
}
167+
}
168+
}

0 commit comments

Comments
 (0)