Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,41 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import tictoc.annotation.DistributedLock;
import tictoc.auction.model.Auction;
import tictoc.auction.port.AuctionRepositoryPort;
import tictoc.bid.dto.request.BidUseCaseReqDTO;
import tictoc.constants.RedisConstants;
import tictoc.bid.exception.BidException;
import tictoc.bid.model.Bid;
import tictoc.bid.port.BidCommandUseCase;
import tictoc.bid.port.BidRepositoryPort;
import tictoc.profile.port.ProfileRepositoryPort;
import static tictoc.error.ErrorCode.BID_FAIL;
import static tictoc.error.ErrorCode.INVALID_PROFILE_MONEY;

@Service
@Transactional
@RequiredArgsConstructor
public class BidCommandService implements BidCommandUseCase {
private final ProfileRepositoryPort profileRepositoryPort;
private final AuctionRepositoryPort auctionRepositoryPort;
private final BidRepositoryPort bidRepositoryPort;

@Override
@DistributedLock(key = "#requestDTO.auctionId", delayTime = RedisConstants.LOCK_LEASE_TIME)
public void bid(final Long userId, BidUseCaseReqDTO.Bid requestDTO) {
var findAuction = auctionRepositoryPort.findAuctionById(requestDTO.auctionId());
bidRepositoryPort.checkBeforeBid(findAuction);
findAuction.startBid(userId);
Integer beforePrice = findAuction.getCurrentPrice();
validateBid(userId, requestDTO.price(), findAuction);
executeAtomicBidUpdate(requestDTO);
bidRepositoryPort.saveBid(Bid.of(userId, requestDTO, beforePrice));
bidRepositoryPort.saveBid(Bid.of(userId, requestDTO, findAuction.getCurrentPrice()));
}

private void validateBid(Long userId, Integer price, Auction auction) {
if (!profileRepositoryPort.checkMoney(userId, price)) {
throw new BidException(INVALID_PROFILE_MONEY);
}
bidRepositoryPort.checkBeforeBid(auction);
auction.validateBeforeBid(userId);
}

private void executeAtomicBidUpdate(BidUseCaseReqDTO.Bid requestDTO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import tictoc.TicTocApiApplication;
Expand All @@ -18,6 +19,8 @@
import tictoc.bid.port.BidCommandUseCase;
import tictoc.bid.port.BidRepositoryPort;
import tictoc.error.ErrorCode;
import tictoc.profile.port.ProfileRepositoryPort;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
Expand All @@ -26,6 +29,9 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.Mockito.when;

@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = TicTocApiApplication.class)
Expand All @@ -38,6 +44,9 @@ public class BidCommandServiceTest {
@Autowired
private BidRepositoryPort bidRepositoryPort;

@MockBean
private ProfileRepositoryPort profileRepositoryPort;

private static final Integer BID_PRICE = 1500;
private static final int NUM_USERS = 5000;
private Auction auction;
Expand All @@ -48,6 +57,7 @@ public void setup() {
LocalDateTime sellStartTime = now.plusMinutes(10);
LocalDateTime sellEndTime = now.plusHours(2);
LocalDateTime auctionCloseTime = now.plusHours(3);
when(profileRepositoryPort.checkMoney(anyLong(), anyInt())).thenReturn(true);

AuctionUseCaseReqDTO.Register requestDTO = new AuctionUseCaseReqDTO.Register(
"Test Auction",
Expand Down
3 changes: 3 additions & 0 deletions tictoc-common/src/main/java/tictoc/error/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ public enum ErrorCode {
CONFLICT_AUCTION_DELETE(HttpStatus.BAD_REQUEST,"경매 삭제가 충돌나서 할 수 없습니다."),
AUCTION_TIME_OVER(HttpStatus.BAD_REQUEST,"경매 시간이 종료되었습니다."),
INVALID_AUCTION_TIME_RANGE(HttpStatus.BAD_REQUEST,"sellStartTime은 sellEndTime보다 이전이어야 합니다."),
CLOSE_AUCTION_ERROR_FROM_BIDDER(HttpStatus.BAD_REQUEST,"경매 종료 실패입니다. (찾을 수 없는 사용거나 보유 금액이 적습니다.)"),
CLOSE_AUCTION_ERROR_FROM_AUCTIONEER(HttpStatus.BAD_REQUEST,"경매 종료 실패입니다. (찾을 수 없는 사용거나 보유 금액이 적습니다.)"),

// Bid
AUCTION_ALREADY_FINISHED(HttpStatus.BAD_REQUEST, "이미 경매가 종료되었습니다."),
BID_NO_ACCESS(HttpStatus.FORBIDDEN,"입찰에 대한 접근 권한이 없습니다."),
INVALID_BID_PRICE(HttpStatus.BAD_REQUEST,"현재 경매가보다 낮은 입찰가를 입력했습니다."),
INVALID_PROFILE_MONEY(HttpStatus.BAD_REQUEST,"현재 경매가보다 낮은 금액을 소유하고 있습니다."),
BID_NOT_FOUND(HttpStatus.BAD_REQUEST,"찾을 수 없는 입찰입니다."),
BID_FAIL(HttpStatus.BAD_REQUEST,"이미 입찰되었습니다."),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import tictoc.bid.port.BidRepositoryPort;
import tictoc.bid.port.WinningBidRepositoryPort;
import tictoc.constants.RedisConstants;
import tictoc.profile.port.ProfileRepositoryPort;
import tictoc.redis.auction.port.out.CloseAuctionUseCase;
import tictoc.user.model.UserSchedule;
import tictoc.user.port.UserScheduleRepositoryPort;
Expand All @@ -20,6 +21,7 @@
@Event("Listener")
@RequiredArgsConstructor
public class CloseAuctionEventListener implements MessageListener {
private final ProfileRepositoryPort profileRepositoryPort;
private final AuctionRepositoryPort auctionRepositoryPort;
private final BidRepositoryPort bidRepositoryPort;
private final WinningBidRepositoryPort winningBidRepositoryPort;
Expand All @@ -30,29 +32,35 @@ public class CloseAuctionEventListener implements MessageListener {
public void onMessage(Message message, byte[] pattern) {
String expiredKey = new String(message.getBody(), StandardCharsets.UTF_8);
if (expiredKey.startsWith(RedisConstants.AUCTION_CLOSE_KEY_PREFIX)) {
Long auctionId = Long.parseLong(expiredKey.replace(RedisConstants.AUCTION_CLOSE_KEY_PREFIX, ""));
Auction findAuction = auctionRepositoryPort.findAuctionById(auctionId);
close(findAuction);
Long auctionId = parseAuctionId(expiredKey);
Auction auction = auctionRepositoryPort.findAuctionById(auctionId);
closeAuction(auction);
closeAuctionUseCase.delete(auctionId);
}
}

private void close(Auction findAuction) {
if (findAuction.getProgress().equals(AuctionProgress.NOT_STARTED)) {
findAuction.notBid();
private Long parseAuctionId(String expiredKey) {
return Long.parseLong(expiredKey.replace(RedisConstants.AUCTION_CLOSE_KEY_PREFIX, ""));
}

private void closeAuction(Auction auction) {
if (auction.getProgress() == AuctionProgress.NOT_STARTED) {
auction.notBid();
} else {
findAuction.bid();
Bid findBid = bidRepositoryPort.findBidByAuctionId(findAuction.getId());
processWinningBid(findAuction, findBid);
processUserSchedule(findAuction, findBid);
auction.bid();
Bid bid = bidRepositoryPort.findBidByAuctionId(auction.getId());
processWinningBid(auction, bid);
processUserSchedule(auction, bid);
}
auctionRepositoryPort.saveAuction(findAuction);
auctionRepositoryPort.saveAuction(auction);
}

private void processWinningBid(Auction auction, Bid bid) {
bid.win();
bidRepositoryPort.saveBid(bid);
winningBidRepositoryPort.saveWinningBid(WinningBid.of(auction, bid));
profileRepositoryPort.subtractMoney(bid.getBidderId(), bid.getBidPrice());
profileRepositoryPort.addMoney(auction.getAuctioneerId(), bid.getBidPrice());
}

private void processUserSchedule(Auction auction, Bid bid) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package tictoc.auction.exception;

import tictoc.error.ErrorCode;
import tictoc.error.exception.TicTocException;

public class CloseAuctionException extends TicTocException {
public CloseAuctionException(final ErrorCode errorCode) {
super(errorCode);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ private void validateAuctionAlreadyStarted() {
}
}

public void startBid(final Long userId) {
public void validateBeforeBid(final Long userId) {
this.validateAuctionTime();
this.validateBidAccess(userId);
this.validateAuctionProgress();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import tictoc.auction.exception.CloseAuctionException;
import tictoc.profile.model.Profile;
import tictoc.profile.model.ProfileImage;
import tictoc.profile.port.ProfileRepositoryPort;
import tictoc.profile.repository.MoneyHistoryRepository;
import tictoc.profile.repository.ProfileImageRepository;
import tictoc.profile.repository.ProfileRepository;

import static tictoc.error.ErrorCode.*;

@Component
@RequiredArgsConstructor
public class ProfileRepositoryAdapter implements ProfileRepositoryPort {
Expand All @@ -25,4 +29,27 @@ public Profile saveProfile(Profile profile) {
public ProfileImage saveProfileImage(ProfileImage profileImage) {
return profileImageRepository.save(profileImage);
}
}

@Override
public boolean checkMoney(Long userId, Integer price) {
return profileRepository.existsByUserIdAndMoneyGreaterThanEqual(userId, price);
}

@Override
@Transactional
public void subtractMoney(Long userId, Integer price) {
int updatedCount = profileRepository.subtractMoney(userId, price);
if (updatedCount == 0) {
throw new CloseAuctionException(CLOSE_AUCTION_ERROR_FROM_BIDDER);
}
}

@Override
@Transactional
public void addMoney(Long userId, Integer price) {
int updatedCount = profileRepository.addMoney(userId, price);
if (updatedCount == 0) {
throw new CloseAuctionException(CLOSE_AUCTION_ERROR_FROM_AUCTIONEER);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ public class MoneyHistory extends BaseTimeEntity {
private Integer finalMoney;
@Enumerated(EnumType.STRING)
private MoneyHistoryStatus status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@
public interface ProfileRepositoryPort {
Profile saveProfile(Profile profile);
ProfileImage saveProfileImage(ProfileImage profileImage);
boolean checkMoney(Long userId, Integer price);
void subtractMoney(Long userId, Integer price);
void addMoney(Long auctioneerId, Integer bidPrice);
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
package tictoc.profile.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import tictoc.profile.model.Profile;

public interface ProfileRepository extends JpaRepository<Profile, Long> {
boolean existsByUserIdAndMoneyGreaterThanEqual(Long userId, Integer price);

@Modifying(clearAutomatically = true)
@Query("UPDATE Profile p SET p.money = p.money - :price WHERE p.userId = :userId AND p.money >= :price")
int subtractMoney(@Param("userId") Long userId, @Param("price") Integer price);

@Modifying(clearAutomatically = true)
@Query("UPDATE Profile p SET p.money = p.money + :price WHERE p.userId = :userId")
int addMoney(@Param("userId") Long userId, @Param("price") Integer price);
}
Loading