Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 쿠폰 발급 요청 및 대기열 사용자 쿠폰 발급 처리 구현 #146

Merged
merged 14 commits into from
Nov 26, 2023
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 @@ -55,7 +55,7 @@ public PurchaseProductResponse purchaseBugProduct(Long memberId, Long productId,
Payment payment = PaymentMapper.toPayment(memberId, product);

if (!isNull(request.couponWalletId())) {
Coupon coupon = couponService.getByWallet(request.couponWalletId(), memberId);
Coupon coupon = couponService.getByWalletIdAndMemberId(request.couponWalletId(), memberId);
payment.applyCoupon(coupon, request.couponWalletId());
}
paymentRepository.save(payment);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.moabam.api.application.coupon;

import java.time.LocalDate;
import java.util.Optional;
import java.util.Set;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.moabam.api.domain.coupon.Coupon;
import com.moabam.api.domain.coupon.CouponWallet;
import com.moabam.api.domain.coupon.repository.CouponManageRepository;
import com.moabam.api.domain.coupon.repository.CouponRepository;
import com.moabam.api.domain.coupon.repository.CouponWalletRepository;
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.BadRequestException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@RequiredArgsConstructor
public class CouponManageService {

private static final long ISSUE_SIZE = 10;

private final ClockHolder clockHolder;

private final CouponRepository couponRepository;
private final CouponManageRepository couponManageRepository;
private final CouponWalletRepository couponWalletRepository;

@Scheduled(fixedDelay = 1000)
public void issue() {
LocalDate now = LocalDate.from(clockHolder.times());
Optional<Coupon> isCoupon = couponRepository.findByStartAt(now);

if (!canIssue(isCoupon)) {
return;
}

Coupon coupon = isCoupon.get();
Set<Long> membersId = couponManageRepository.popMinQueue(coupon.getName(), ISSUE_SIZE);

membersId.forEach(memberId -> {
int nextStock = couponManageRepository.increaseIssuedStock(coupon.getName());

if (coupon.getStock() < nextStock) {
return;
}

CouponWallet couponWallet = CouponWallet.create(memberId, coupon);
couponWalletRepository.save(couponWallet);
});
}

public void register(AuthMember authMember, String couponName) {
double registerTime = System.currentTimeMillis();
validateRegister(couponName);
couponManageRepository.addIfAbsentQueue(couponName, authMember.id(), registerTime);
}

public void deleteCouponManage(String couponName) {
couponManageRepository.deleteQueue(couponName);
couponManageRepository.deleteIssuedStock(couponName);
}

private void validateRegister(String couponName) {
LocalDate now = LocalDate.from(clockHolder.times());
Optional<Coupon> coupon = couponRepository.findByStartAt(now);

if (coupon.isEmpty() || !coupon.get().getName().equals(couponName)) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}
}

private boolean canIssue(Optional<Coupon> coupon) {
if (coupon.isEmpty()) {
return false;
}

Coupon currentCoupon = coupon.get();
int currentStock = couponManageRepository.getIssuedStock(currentCoupon.getName());
int maxStock = currentCoupon.getStock();

return currentStock < maxStock;
}
}

This file was deleted.

26 changes: 12 additions & 14 deletions src/main/java/com/moabam/api/application/coupon/CouponService.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,18 @@
@Transactional(readOnly = true)
public class CouponService {

private final ClockHolder clockHolder;
private final CouponManageService couponManageService;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QueueService지우셨군요! 좋습니다


private final CouponRepository couponRepository;
private final CouponSearchRepository couponSearchRepository;
private final CouponWalletSearchRepository couponWalletSearchRepository;
private final ClockHolder clockHolder;

@Transactional
public void create(AuthMember admin, CreateCouponRequest request) {
validateAdminRole(admin);
validateConflictName(request.name());
validateConflictStartAt(request.startAt());
validatePeriod(request.startAt(), request.openAt());

Coupon coupon = CouponMapper.toEntity(admin.id(), request);
Expand All @@ -49,6 +52,7 @@ public void delete(AuthMember admin, Long couponId) {
Coupon coupon = couponRepository.findById(couponId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));
couponRepository.delete(coupon);
couponManageService.deleteCouponManage(coupon.getName());
}

public CouponResponse getById(Long couponId) {
Expand All @@ -58,7 +62,7 @@ public CouponResponse getById(Long couponId) {
return CouponMapper.toDto(coupon);
}

public Coupon getByWallet(Long couponWalletId, Long memberId) {
public Coupon getByWalletIdAndMemberId(Long couponWalletId, Long memberId) {
return couponWalletSearchRepository.findByIdAndMemberId(couponWalletId, memberId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON_WALLET))
.getCoupon();
Expand All @@ -73,18 +77,6 @@ public List<CouponResponse> getAllByStatus(CouponStatusRequest request) {
.toList();
}

public Coupon validatePeriod(String couponName) {
LocalDate now = LocalDate.from(clockHolder.times());
Coupon coupon = couponRepository.findByName(couponName)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_COUPON));

if (!now.equals(coupon.getStartAt())) {
throw new BadRequestException(ErrorMessage.INVALID_COUPON_PERIOD);
}

return coupon;
}

private void validatePeriod(LocalDate startAt, LocalDate openAt) {
LocalDate now = LocalDate.from(clockHolder.times());

Expand All @@ -108,4 +100,10 @@ private void validateConflictName(String couponName) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_NAME);
}
}

private void validateConflictStartAt(LocalDate startAt) {
if (couponRepository.existsByStartAt(startAt)) {
throw new ConflictException(ErrorMessage.CONFLICT_COUPON_START_AT);
}
}
}
2 changes: 1 addition & 1 deletion src/main/java/com/moabam/api/domain/coupon/Coupon.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public class Coupon extends BaseTimeEntity {
@Column(name = "stock", nullable = false)
private int stock;

@Column(name = "start_at", nullable = false)
@Column(name = "start_at", unique = true, nullable = false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Q: 지금 생각해보는 건데 만약 시작 시간이 다른 쿠폰과의 시간과 별 차이가 없으면 어떻게 되나요?

private LocalDate startAt;

@Column(name = "open_at", nullable = false)
Expand Down
6 changes: 4 additions & 2 deletions src/main/java/com/moabam/api/domain/coupon/CouponWallet.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

Expand All @@ -34,9 +33,12 @@ public class CouponWallet extends BaseTimeEntity {
@ManyToOne(fetch = FetchType.LAZY)
private Coupon coupon;

@Builder
private CouponWallet(Long memberId, Coupon coupon) {
this.memberId = memberId;
this.coupon = coupon;
}

public static CouponWallet create(Long memberId, Coupon coupon) {
return new CouponWallet(memberId, coupon);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.moabam.api.domain.coupon.repository;

import static java.util.Objects.*;

import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.stereotype.Repository;

import com.moabam.api.infrastructure.redis.StringRedisRepository;
import com.moabam.api.infrastructure.redis.ZSetRedisRepository;

import lombok.RequiredArgsConstructor;

@Repository
@RequiredArgsConstructor
public class CouponManageRepository {

private static final String STOCK_KEY = "%s_INCR";

private final ZSetRedisRepository zSetRedisRepository;
private final StringRedisRepository stringRedisRepository;

public void addIfAbsentQueue(String couponName, Long memberId, double registerTime) {
zSetRedisRepository.addIfAbsent(requireNonNull(couponName), requireNonNull(memberId), registerTime);
}

public Set<Long> popMinQueue(String couponName, long count) {
return zSetRedisRepository
.popMin(requireNonNull(couponName), count)
.stream()
.map(tuple -> (Long)tuple.getValue())
.collect(Collectors.toSet());
}

public void deleteQueue(String couponName) {
stringRedisRepository.delete(requireNonNull(couponName));
}

public int increaseIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));

return stringRedisRepository
.increment(requireNonNull(stockKey))
.intValue();
}

public int getIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));
String stockValue = stringRedisRepository.get(requireNonNull(stockKey));

if (stockValue == null) {
return 0;
}

return Integer.parseInt(stockValue);
}

public void deleteIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));
stringRedisRepository.delete(requireNonNull(stockKey));
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.moabam.api.domain.coupon.repository;

import java.time.LocalDate;
import java.util.Optional;

import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -8,7 +9,9 @@

public interface CouponRepository extends JpaRepository<Coupon, Long> {

Optional<Coupon> findByName(String couponName);

boolean existsByName(String name);

boolean existsByStartAt(LocalDate startAt);

Optional<Coupon> findByStartAt(LocalDate startAt);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ public void save(String key, String value, Duration timeout) {
.set(key, value, timeout);
}

public void delete(String key) {
redisTemplate.delete(key);
public Long increment(String key) {
return redisTemplate
.opsForValue()
.increment(key);
}

public String get(String key) {
Expand All @@ -32,4 +34,8 @@ public String get(String key) {
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}

public void delete(String key) {
redisTemplate.delete(key);
}
}
Loading