Skip to content

Commit

Permalink
Merge branch 'develop' of https://github.com/team-moabam/moabam-BE in…
Browse files Browse the repository at this point in the history
…to fix/#172-token-decoding

# Conflicts:
#	src/main/java/com/moabam/api/application/coupon/CouponManageService.java
#	src/main/resources/config
#	src/main/resources/static/docs/coupon.html
  • Loading branch information
parksey committed Nov 29, 2023
2 parents 8639f57 + 25932fb commit 109a842
Show file tree
Hide file tree
Showing 17 changed files with 2,520 additions and 899 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import com.moabam.api.application.notification.NotificationService;
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;
Expand All @@ -25,67 +25,69 @@
@RequiredArgsConstructor
public class CouponManageService {

private static final String SUCCESS_ISSUE_BODY = "%s 쿠폰 발행을 성공했습니다. 축하드립니다!";
private static final String FAIL_ISSUE_BODY = "%s 쿠폰 발행을 실패했습니다. 다음 기회에!";
private static final long ISSUE_SIZE = 10;
private static final long ISSUE_FIRST = 0;

private final ClockHolder clockHolder;
private final NotificationService notificationService;

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

private long current = ISSUE_FIRST;

@Scheduled(cron = "0 0 0 * * *")
public void init() {
current = ISSUE_FIRST;
}

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

if (!canIssue(isCoupon)) {
if (optionalCoupon.isEmpty()) {
return;
}

Coupon coupon = isCoupon.get();
Set<Long> membersId = couponManageRepository.popMinQueue(coupon.getName(), ISSUE_SIZE);
Coupon coupon = optionalCoupon.get();
String couponName = coupon.getName();
int max = coupon.getStock();

membersId.forEach(memberId -> {
int nextStock = couponManageRepository.increaseIssuedStock(coupon.getName());
Set<Long> membersId = couponManageRepository.rangeQueue(couponName, current, current + ISSUE_SIZE);

if (coupon.getStock() < nextStock) {
return;
for (Long memberId : membersId) {
int rank = couponManageRepository.rankQueue(couponName, memberId);

if (max < rank) {
notificationService.sendCouponIssueResult(memberId, couponName, FAIL_ISSUE_BODY);
continue;
}

CouponWallet couponWallet = CouponWallet.create(memberId, coupon);
couponWalletRepository.save(couponWallet);
});
couponWalletRepository.save(CouponWallet.create(memberId, coupon));
notificationService.sendCouponIssueResult(memberId, couponName, SUCCESS_ISSUE_BODY);
current++;
}
}

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

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

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

if (coupon.isEmpty() || !coupon.get().getName().equals(couponName)) {
if (!couponRepository.existsByNameAndStartAt(couponName, now)) {
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,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());
couponManageService.deleteQueue(coupon.getName());
}

public CouponResponse getById(Long couponId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.common.util.ClockHolder;
import com.moabam.global.error.exception.ConflictException;
import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;
Expand All @@ -42,12 +43,18 @@ public class NotificationService {
public void sendKnock(AuthMember member, Long targetId, Long roomId) {
roomService.validateRoomById(roomId);
validateConflictKnock(member.id(), targetId, roomId);
String fcmToken = fcmService.findTokenByMemberId(targetId)
.orElseThrow(() -> new NotFoundException(ErrorMessage.NOT_FOUND_FCM_TOKEN));

String fcmToken = fcmService.findTokenByMemberId(targetId);
fcmService.sendAsync(fcmToken, String.format(KNOCK_BODY, member.nickname()));
notificationRepository.saveKnock(member.id(), targetId, roomId);
}

public void sendCouponIssueResult(Long memberId, String couponName, String body) {
String fcmToken = fcmService.findTokenByMemberId(memberId).orElse(null);
fcmService.sendAsync(fcmToken, String.format(body, couponName));
}

@Scheduled(cron = "0 50 * * * *")
public void sendCertificationTime() {
int certificationTime = (clockHolder.times().getHour() + ONE_HOUR) % HOURS_IN_A_DAY;
Expand All @@ -56,7 +63,7 @@ public void sendCertificationTime() {
participants.parallelStream().forEach(participant -> {
String roomTitle = participant.getRoom().getTitle();
String notificationBody = String.format(CERTIFY_TIME_BODY, roomTitle);
String fcmToken = fcmService.findTokenByMemberId(participant.getMemberId());
String fcmToken = fcmService.findTokenByMemberId(participant.getMemberId()).orElse(null);
fcmService.sendAsync(fcmToken, notificationBody);
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,48 +16,35 @@
@RequiredArgsConstructor
public class CouponManageRepository {

private static final String STOCK_KEY = "%s_INCR";
private static final int EXPIRE_DAYS = 2;

private final ZSetRedisRepository zSetRedisRepository;
private final ValueRedisRepository valueRedisRepository;

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

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

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

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

return valueRedisRepository
.increment(requireNonNull(stockKey))
public int rankQueue(String couponName, Long memberId) {
return zSetRedisRepository
.rank(requireNonNull(couponName), requireNonNull(memberId))
.intValue();
}

public int getIssuedStock(String couponName) {
String stockKey = String.format(STOCK_KEY, requireNonNull(couponName));
String stockValue = valueRedisRepository.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));
valueRedisRepository.delete(requireNonNull(stockKey));
public void deleteQueue(String couponName) {
valueRedisRepository.delete(requireNonNull(couponName));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@

public interface CouponRepository extends JpaRepository<Coupon, Long> {

boolean existsByName(String name);
boolean existsByName(String couponName);

boolean existsByStartAt(LocalDate startAt);

Optional<Coupon> findByStartAt(LocalDate startAt);

boolean existsByNameAndStartAt(String couponName, LocalDate startAt);
}
16 changes: 4 additions & 12 deletions src/main/java/com/moabam/api/infrastructure/fcm/FcmService.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.moabam.api.infrastructure.fcm;

import java.util.Optional;

import org.springframework.stereotype.Service;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.Notification;
import com.moabam.global.auth.model.AuthMember;
import com.moabam.global.error.exception.NotFoundException;
import com.moabam.global.error.model.ErrorMessage;

import lombok.RequiredArgsConstructor;

Expand All @@ -32,10 +32,8 @@ public void deleteTokenByMemberId(Long memberId) {
fcmRepository.deleteTokenByMemberId(memberId);
}

public String findTokenByMemberId(Long targetId) {
validateToken(targetId);

return fcmRepository.findTokenByMemberId(targetId);
public Optional<String> findTokenByMemberId(Long targetId) {
return Optional.ofNullable(fcmRepository.findTokenByMemberId(targetId));
}

public void sendAsync(String fcmToken, String notificationBody) {
Expand All @@ -46,10 +44,4 @@ public void sendAsync(String fcmToken, String notificationBody) {
firebaseMessaging.sendAsync(message);
}
}

private void validateToken(Long memberId) {
if (!fcmRepository.existsTokenByMemberId(memberId)) {
throw new NotFoundException(ErrorMessage.NOT_FOUND_FCM_TOKEN);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import static java.util.Objects.*;

import java.time.Duration;
import java.util.Set;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations.TypedTuple;
import org.springframework.stereotype.Repository;

import lombok.RequiredArgsConstructor;
Expand All @@ -16,17 +16,23 @@ public class ZSetRedisRepository {

private final RedisTemplate<String, Object> redisTemplate;

public void addIfAbsent(String key, Object value, double score) {
if (redisTemplate.opsForZSet().score(key, value) == null) {
redisTemplate
.opsForZSet()
.add(requireNonNull(key), requireNonNull(value), score);
}
public void addIfAbsent(String key, Object value, double score, int expire) {
redisTemplate
.opsForZSet()
.addIfAbsent(requireNonNull(key), requireNonNull(value), score);
redisTemplate
.expire(key, Duration.ofDays(expire));
}

public Set<Object> range(String key, long start, long end) {
return redisTemplate
.opsForZSet()
.range(key, start, end);
}

public Set<TypedTuple<Object>> popMin(String key, long count) {
public Long rank(String key, Object value) {
return redisTemplate
.opsForZSet()
.popMin(key, count);
.rank(key, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public List<CouponResponse> getAllByStatus(@Valid @RequestBody CouponStatusReque
@PostMapping("/coupons")
@ResponseStatus(HttpStatus.OK)
public void registerQueue(@Auth AuthMember authMember, @RequestParam("couponName") String couponName) {
couponManageService.register(authMember, couponName);
couponManageService.registerQueue(authMember.id(), couponName);
}

@GetMapping({"/my-coupons", "/my-coupons/{couponId}"})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,10 @@ public class EmbeddedRedisConfig {
private int availablePort;
private RedisServer redisServer;

public EmbeddedRedisConfig(@Value("${spring.data.redis.port}") int redisPort,
@Value("${spring.data.redis.host}") String redisHost) {
public EmbeddedRedisConfig(
@Value("${spring.data.redis.port}") int redisPort,
@Value("${spring.data.redis.host}") String redisHost
) {
this.redisPort = redisPort;
this.redisHost = redisHost;

Expand Down
Loading

0 comments on commit 109a842

Please sign in to comment.