Skip to content

Commit

Permalink
Merge pull request #195 from SKY-HORSE-MAN-POWER/develop
Browse files Browse the repository at this point in the history
[DEPLOYMENT] 경매 낙찰 시, `auction-close` 토픽으로 수정
  • Loading branch information
chanchanwoong authored Jun 22, 2024
2 parents 8e19bfd + b90273e commit 639c9ce
Show file tree
Hide file tree
Showing 21 changed files with 671 additions and 67 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
public interface AuctionService {
void offerBiddingPrice(OfferBiddingPriceDto offerBiddingPriceDto);

void auctionClose(String auctionUuid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

import com.skyhorsemanpower.auction.application.AuctionService;
import com.skyhorsemanpower.auction.common.exception.CustomException;
import com.skyhorsemanpower.auction.domain.AuctionCloseState;
import com.skyhorsemanpower.auction.domain.RoundInfo;
import com.skyhorsemanpower.auction.repository.AuctionHistoryRepository;
import com.skyhorsemanpower.auction.kafka.KafkaProducerCluster;
import com.skyhorsemanpower.auction.kafka.Topics;
import com.skyhorsemanpower.auction.kafka.dto.AuctionCloseDto;
import com.skyhorsemanpower.auction.repository.*;
import com.skyhorsemanpower.auction.common.exception.ResponseStatus;
import com.skyhorsemanpower.auction.data.dto.*;
import com.skyhorsemanpower.auction.domain.AuctionHistory;
import com.skyhorsemanpower.auction.repository.AuctionHistoryReactiveRepository;
import com.skyhorsemanpower.auction.repository.RoundInfoReactiveRepository;
import com.skyhorsemanpower.auction.repository.RoundInfoRepository;
import com.skyhorsemanpower.auction.status.AuctionStateEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.mongodb.core.MongoTemplate;
Expand All @@ -18,6 +20,10 @@

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Service
@RequiredArgsConstructor
Expand All @@ -29,6 +35,8 @@ public class AuctionServiceImpl implements AuctionService {
private final MongoTemplate mongoTemplate;
private final RoundInfoReactiveRepository roundInfoReactiveRepository;
private final RoundInfoRepository roundInfoRepository;
private final AuctionCloseStateRepository auctionCloseStateRepository;
private final KafkaProducerCluster producer;

@Override
@Transactional
Expand Down Expand Up @@ -58,11 +66,92 @@ public void offerBiddingPrice(OfferBiddingPriceDto offerBiddingPriceDto) {
updateRoundInfo(roundInfo);
}

@Override
public void auctionClose(String auctionUuid) {
// auction_close_state 도큐먼트에 acutionUuid 데이터가 있으면(마감됐으면) 바로 return
if (auctionCloseStateRepository.findByAuctionUuid(auctionUuid).isPresent()) {
log.info("Auction already close");
return;
}

log.info("Auction Close Start");

// auction_history 도큐먼트를 조회하여 경매 상태를 변경
if (auctionHistoryRepository.findFirstByAuctionUuidOrderByBiddingTimeDesc(auctionUuid).isEmpty()) {
log.info("auction_history is not exist! No one bid at auction!");
producer.sendMessage(Topics.AUCTION_CLOSE.getTopic(), AuctionStateEnum.AUCTION_NO_PARTICIPANTS);
return;
}
;

log.info("auction_history is exist!");

// 경매 마감 로직
// 마지막 라운드 수, 낙찰 가능 인원 수 조회
RoundInfo lastRoundInfo = roundInfoRepository.findFirstByAuctionUuidOrderByCreatedAtDesc(auctionUuid)
.orElseThrow(() -> new CustomException(ResponseStatus.NO_DATA)
);
log.info("Last Round Info >>> {}", lastRoundInfo.toString());

int round = lastRoundInfo.getRound();
long numberOfParticipants = lastRoundInfo.getNumberOfParticipants();

// 마감 로직
// 마지막 라운드 입찰 이력
List<AuctionHistory> lastRoundAuctionHistory = auctionHistoryRepository.
findByAuctionUuidAndRoundOrderByBiddingTime(auctionUuid, round);
log.info("Last Round Auction History >>> {}", lastRoundAuctionHistory.toString());

// 마지막 - 1 라운드 입찰 이력
List<AuctionHistory> lastMinusOneRoundAuctionHistory = auctionHistoryRepository.
findByAuctionUuidAndRoundOrderByBiddingTime(auctionUuid, round - 1);
log.info("Before Last Round Auction History >>> {}", lastMinusOneRoundAuctionHistory.toString());

// 마지막 라운드 입찰자를 낙찰자로 고정
Set<String> memberUuids = new HashSet<>();
for (AuctionHistory auctionHistory : lastRoundAuctionHistory) {
memberUuids.add(auctionHistory.getBiddingUuid());
}

// 마지막 직전 라운드 입찰자 중 낙찰자 추가
for (AuctionHistory auctionHistory : lastMinusOneRoundAuctionHistory) {
// 동일 입찰자 제외하고 추가
memberUuids.add(auctionHistory.getBiddingUuid());

// 낙찰 가능 인원 수 만큼 리스트 추가
if (memberUuids.size() == numberOfParticipants) break;
}

log.info("memberUuids >>> {}", memberUuids.toString());

// 낙찰가는 마지막 이전 라운드에서 biddingPrice로 결정
BigDecimal price = lastMinusOneRoundAuctionHistory.get(0).getBiddingPrice();
log.info("price >>> {}", price);

// 카프카로 경매 서비스 메시지 전달
AuctionCloseDto auctionCloseDto = AuctionCloseDto.builder()
.auctionUuid(auctionUuid)
.memberUuids(memberUuids.stream().toList())
.price(price)
.auctionState(AuctionStateEnum.AUCTION_NORMAL_CLOSING)
.build();
log.info("Kafka Message To Payment Service >>> {}", auctionCloseDto.toString());

// 경매글 마감 처리 메시지와 결제 서비스 메시지 동일 토픽으로 진행
producer.sendMessage(Topics.Constant.AUCTION_CLOSE, auctionCloseDto);

// 경매 마감 여부 저장
auctionCloseStateRepository.save(AuctionCloseState.builder()
.auctionUuid(auctionUuid)
.auctionCloseState(true)
.build());
}

private void updateRoundInfo(RoundInfo roundInfo) {
RoundInfo updatedRoundInfo;

// 다음 라운드로 round_info 도큐먼트 갱신
if(roundInfo.getLeftNumberOfParticipants().equals(1L)) {
if (roundInfo.getLeftNumberOfParticipants().equals(1L)) {
updatedRoundInfo = RoundInfo.nextRoundUpdate(roundInfo);
}

Expand All @@ -85,15 +174,26 @@ private void isBiddingPossible(OfferBiddingPriceDto offerBiddingPriceDto, RoundI
checkBiddingTime(roundInfo.getRoundStartTime(), roundInfo.getRoundEndTime());
log.info("입찰 시간 통과");

// 조건2. 남은 인원이 1 이상
// 조건2. 해당 라운드에 참여 여부
checkBiddingRound(offerBiddingPriceDto.getBiddingUuid(), offerBiddingPriceDto.getRound());
log.info("현재 라운드에 참여한 적 없음");

// 조건3. 남은 인원이 1 이상
checkLeftNumberOfParticipant(roundInfo.getLeftNumberOfParticipants());
log.info("남은 인원 통과");

// 조건3. round 입찰가와 입력한 입찰가 확인
// 조건4. round 입찰가와 입력한 입찰가 확인
checkRoundAndBiddingPrice(offerBiddingPriceDto, roundInfo);
log.info("라운드 및 입찰가 통과");
}

private void checkBiddingRound(String biddingUuid, int round) {
if (auctionHistoryRepository.findByBiddingUuidAndRound(biddingUuid, round).isPresent()) {
throw new CustomException(ResponseStatus.ALREADY_BID_IN_ROUND);
}
;
}

private void checkLeftNumberOfParticipant(Long leftNumberOfParticipants) {
log.info("leftNumberOfParticipants >>> {}", leftNumberOfParticipants);
if (leftNumberOfParticipants < 1L) throw new CustomException(ResponseStatus.FULL_PARTICIPANTS);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.skyhorsemanpower.auction.common;

import com.skyhorsemanpower.auction.status.TimeZoneChangeEnum;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;

public class DateTimeConverter {

public static LocalDateTime instantToLocalDateTime(long longOfInstant) {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(longOfInstant), ZoneId.systemDefault());
}

public static long localDateTimeToInstant(LocalDateTime localDateTime) {
return localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
}

public static long kstLocalDateTimeToInstant(LocalDateTime kstLocalDateTime) {
return kstLocalDateTime.minusHours(TimeZoneChangeEnum.KOREA.getTimeDiff())
.toInstant(ZoneOffset.UTC)
.toEpochMilli();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ public enum ResponseStatus {
// 입찰자를 다 모집한 경우
FULL_PARTICIPANTS(404, "현 라운드는 입찰자가 다 찼습니다."),

// 이번 라운드에 입찰하고 또 한 경우
ALREADY_BID_IN_ROUND(404, "이미 이번 라운드에 입찰하셨습니다."),

// 예외 테스트 용
EXCEPTION_TEST(500, "예외 테스트") ;

Expand Down
51 changes: 19 additions & 32 deletions src/main/java/com/skyhorsemanpower/auction/config/QuartzConfig.java
Original file line number Diff line number Diff line change
@@ -1,64 +1,51 @@
package com.skyhorsemanpower.auction.config;

import com.skyhorsemanpower.auction.quartz.EndAuction;
import com.skyhorsemanpower.auction.kafka.dto.InitialAuctionDto;
import com.skyhorsemanpower.auction.quartz.AuctionClose;
import lombok.RequiredArgsConstructor;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Calendar;
import java.time.Instant;
import java.util.Date;

@Configuration
@RequiredArgsConstructor
public class QuartzConfig {
private final Scheduler scheduler;

@Bean
public Scheduler scheduler() throws SchedulerException {
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
return scheduler;
}

// SimpleScheduler 메서드
public void schedulerEndAuctionJob(String auctionUuid) throws SchedulerException {
// 경매를 만드는 시간에서 하루를 더한 endedAt
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 1);
Date endedAt = calendar.getTime();

// 경매 시작과 경매 마감의 상태 변경 스케줄링
public void schedulerUpdateAuctionStateJob(InitialAuctionDto initialAuctionDto) throws SchedulerException {
// JobDataMap 생성 및 auctionUuid 설정
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("auctionUuid", auctionUuid);

// 오류로 실행되지 않았을 때를 위한 retryCount key 값
jobDataMap.put("retryCount", 0);
jobDataMap.put("auctionUuid", initialAuctionDto.getAuctionUuid());

// Job 생성
JobDetail job = JobBuilder
.newJob(EndAuction.class)
.withIdentity("EndAuctionJob_" + auctionUuid, "EndAuctionGroup")
JobDetail auctionCloseJob = JobBuilder
.newJob(AuctionClose.class)
.withIdentity("AuctionCloseJob_" + initialAuctionDto.getAuctionUuid(),
"AuctionCloseGroup")
.usingJobData(jobDataMap)
.withDescription("경매 마감 Job")
.build();

Date auctionEndDate = Date.from(Instant.ofEpochMilli(initialAuctionDto.getAuctionStartTime()));

// Trigger 생성
Trigger trigger = TriggerBuilder
Trigger auctionCloseTrigger = TriggerBuilder
.newTrigger()
.withIdentity("EndAuctionTrigger_" + auctionUuid, "EndAuctionGroup")
.withIdentity("AuctionCloseTrigger_" + initialAuctionDto.getAuctionUuid(),
"AuctionCloseGroup")
.withDescription("경매 마감 Trigger")

// test용 30초 후 시작하는 스케줄러
// test용 60초 후 시작하는 스케줄러
.startAt(DateBuilder.futureDate(60, DateBuilder.IntervalUnit.SECOND))

//Todo 실제 배포에서는 endedAt을 사용해야 한다.
// .startAt(endedAt)
//Todo 실제 배포에서는 auctionEndDate을 사용해야 한다.
// .startAt(auctionEndDate)
.build();

// 스케줄러 생성 및 Job, Trigger 등록
scheduler.scheduleJob(job, trigger);

scheduler.scheduleJob(auctionCloseJob, auctionCloseTrigger);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.skyhorsemanpower.auction.domain;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;

@Getter
@NoArgsConstructor
@ToString
@Document(collection = "auction_close_state")
public class AuctionCloseState {
@Id
private String auctionCloseStateId;

private String auctionUuid;
private boolean auctionCloseState;

@Builder
public AuctionCloseState(String auctionUuid, boolean auctionCloseState) {
this.auctionUuid = auctionUuid;
this.auctionCloseState = auctionCloseState;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.skyhorsemanpower.auction.kafka;

import com.skyhorsemanpower.auction.common.DateTimeConverter;
import com.skyhorsemanpower.auction.config.QuartzConfig;
import com.skyhorsemanpower.auction.domain.RoundInfo;
import com.skyhorsemanpower.auction.kafka.dto.InitialAuctionDto;
import com.skyhorsemanpower.auction.repository.RoundInfoRepository;
import com.skyhorsemanpower.auction.status.RoundTimeEnum;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.handler.annotation.Headers;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;

@Slf4j
@RequiredArgsConstructor
@Component
public class KafkaConsumerCluster {
private final RoundInfoRepository roundInfoRepository;
private final QuartzConfig quartzConfig;

@KafkaListener(topics = Topics.Constant.INITIAL_AUCTION, groupId = "${spring.kafka.consumer.group-id}")
public void initialAuction(@Payload LinkedHashMap<String, Object> message,
@Headers MessageHeaders messageHeaders) {
log.info("consumer: success >>> message: {}, headers: {}", message.toString(),
messageHeaders);

// round_info 초기 데이터 저장
InitialAuctionDto initialAuctionDto = InitialAuctionDto.builder()
.auctionUuid(message.get("auctionUuid").toString())
.startPrice(new BigDecimal(message.get("startPrice").toString()))
.numberOfEventParticipants((Integer) message.get("numberOfEventParticipants"))
.auctionStartTime(((Long) message.get("auctionStartTime")))
.auctionEndTime(((Long) message.get("auctionEndTime")))
.incrementUnit(new BigDecimal(message.get("incrementUnit").toString()))
.build();
log.info("InitialAuctionDto >>> {}", initialAuctionDto.toString());

initialRoundInfo(initialAuctionDto);

// 경매 마감 스케줄러 등록
try {
quartzConfig.schedulerUpdateAuctionStateJob(initialAuctionDto);
} catch (Exception e1) {
log.warn(e1.getMessage());
}
}

private void initialRoundInfo(InitialAuctionDto initialAuctionDto) {
// Instant 타입을 LocalDateTime 변환
LocalDateTime roundStartTime = DateTimeConverter.
instantToLocalDateTime(initialAuctionDto.getAuctionStartTime());

RoundInfo roundinfo = RoundInfo.builder()
.auctionUuid(initialAuctionDto.getAuctionUuid())
.round(1)
.roundStartTime(roundStartTime)
.roundEndTime(roundStartTime.plusSeconds(RoundTimeEnum.SECONDS_60.getSecond()))
.incrementUnit(initialAuctionDto.getIncrementUnit())
.price(initialAuctionDto.getStartPrice())
.isActive(true)
.numberOfParticipants((long) initialAuctionDto.getNumberOfEventParticipants())
.leftNumberOfParticipants((long) initialAuctionDto.getNumberOfEventParticipants())
.createdAt(LocalDateTime.now())
.build();

log.info("Initial round_info >>> {}", roundinfo);
roundInfoRepository.save(roundinfo);
}
}
Loading

0 comments on commit 639c9ce

Please sign in to comment.