Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

[#70] 배달 매칭 서비스 #69

Open
wants to merge 18 commits into
base: rider_info_service
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 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
2 changes: 2 additions & 0 deletions src/main/java/com/delfood/FoodDeliveryApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;

@SpringBootApplication
Expand All @@ -24,6 +25,7 @@
@EnableAspectJAutoProxy // 최상위 클래스에 적용해야 AOP를 찾을 수 있도록 만들어준다.
@EnableCaching // Spring에서 Caching을 사용하겠다고 선언한다.
@EnableAsync // 메서드를 비동기 방식으로 실행할 수 있도록 설정한다.
@EnableScheduling // 스케줄링을 허용한다.
public class FoodDeliveryApplication {

public static void main(String[] args) {
Expand Down
18 changes: 16 additions & 2 deletions src/main/java/com/delfood/controller/RiderController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.delfood.aop.LoginCheck.UserType;
import com.delfood.aop.RiderLoginCheck;
import com.delfood.dto.rider.RiderDTO;
import com.delfood.service.delivery.DeliveryService;
import com.delfood.service.rider.RiderInfoService;
import com.delfood.utils.SessionUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
Expand Down Expand Up @@ -37,6 +38,9 @@ public class RiderController {
@Autowired
private ObjectMapper objectMapper;

@Autowired
private DeliveryService deliveryService;

/**
* 아이디 중복 체크.
* @author jun
Expand Down Expand Up @@ -129,7 +133,12 @@ public void updateMail(HttpSession session, @RequestBody UpdateMailRequest reque
riderInfoService.changeMail(id, request.getPassword(), request.getUpdateMail());
}


@PostMapping("delivery/accept")
@LoginCheck(type = UserType.RIDER)
Copy link
Member

Choose a reason for hiding this comment

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

모습을 보니 떠오른건데 type보다는 level이 어떨까요?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

level도 생각해보았는데 뭔가 접근의 계층이 있어야 할것같은 느낌이라서요. 제가 만든 로그인은 3가지의 전혀 다른 타입이라서 일단 타입으로 해두었습니다.

public void deliveryAccept(@RequestBody DeliveryAcceptRequest request, HttpSession session) {
String riderId = SessionUtil.getLoginRiderId(session);
deliveryService.acceptDeliveryRequest(riderId, request.getOrderId());
}

// Request
@Getter
Expand All @@ -140,7 +149,12 @@ private static class SignInRequest {
@NonNull
private String password;
}


@Getter
private static class DeliveryAcceptRequest {
private Long orderId;
}

@Getter
private static class UpdatePasswordRequest {
@NonNull
Expand Down
32 changes: 32 additions & 0 deletions src/main/java/com/delfood/dao/FcmDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public class FcmDao {

@Autowired
private ObjectMapper objectMapper;

@Value("#{expire.fcm.rider}")
private static Long riderTokenExpireSecond;

@Value("${expire.fcm.member}")
private static Long memberTokenExpireSecond;
Expand Down Expand Up @@ -85,6 +88,35 @@ public void addOwnerToken(String ownerId, String token) {
}
}

public void addRiderToken(String riderId, String token) {
String key = RedisKeyFactory.generateFcmRiderKey(riderId);
redisTemplate.watch(key);
try {
if (getRiderTokens(riderId).contains(token)) { // 토큰이 이미 있을 경우
return;
}
redisTemplate.multi();

redisTemplate.opsForList().rightPush(key, token);
redisTemplate.expire(key, riderTokenExpireSecond, TimeUnit.SECONDS);

redisTemplate.exec();
} catch (Exception e) {
log.error("Redis Add Rider Token ERROR! key : {}", key);
log.error("ERROR Info : {} ", e.getMessage());
redisTemplate.discard();
throw new RuntimeException(
"Cannot add rider token. key : " + key + ", ERROR Info " + e.getMessage());
}
}

public List<String> getRiderTokens(String riderId) {
return redisTemplate.opsForList().range(RedisKeyFactory.generateFcmRiderKey(riderId), 0, -1)
.stream()
.map(e -> objectMapper.convertValue(e, String.class))
.collect(Collectors.toList());
}

/**
* 해당 고객의 토큰 리스트를 조회한다.
* @author jun
Expand Down
29 changes: 29 additions & 0 deletions src/main/java/com/delfood/dao/deliveery/DeliveryDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.delfood.dao.deliveery;

import com.delfood.dto.OrderDTO.OrderStatus;
import com.delfood.dto.address.Position;
import com.delfood.dto.rider.DeliveryRiderDTO;
import java.util.List;

public interface DeliveryDao {

void updateRiderInfo(DeliveryRiderDTO riderInfo);

long deleteRiderInfo(String riderId);

boolean hasRiderInfo(String riderId);

void deleteNonUpdatedRiders();

DeliveryRiderDTO getRiderInfo(String riderId);

List<DeliveryRiderDTO> toList();

long deleteAll(List<String> idList);

OrderStatus getOrderStatus(Long orderId);

void setOrderStatus(Long orderId, OrderStatus status);

void deleteOrderStatus(Long orderId);
}
166 changes: 166 additions & 0 deletions src/main/java/com/delfood/dao/deliveery/MultiThreadDeliveryDao.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package com.delfood.dao.deliveery;

import com.delfood.dto.OrderDTO.OrderStatus;
import com.delfood.dto.address.Position;
import com.delfood.dto.rider.DeliveryRiderDTO;
import com.delfood.service.OrderService;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import javax.annotation.concurrent.ThreadSafe;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Repository;

@Repository(value = "multiThreadDeliveryDao")
yyy9942 marked this conversation as resolved.
Show resolved Hide resolved
@ThreadSafe
public class MultiThreadDeliveryDao implements DeliveryDao{
Copy link
Member

Choose a reason for hiding this comment

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

저장소가 로컬저장소이므로 MultiThread보단 LocalMemoryRiderMatcher 등 적절한 이름을 지어주는게 좋을 것 같습니다

private Map<String, DeliveryRiderDTO> riders;
private Map<Long, OrderStatus> orders;

@Value("rider.expire")
private static Long expireTime;

@Autowired
private OrderService orderService;

@PostConstruct
public void init() {
this.riders = new ConcurrentHashMap<String, DeliveryRiderDTO>();
this.orders = new ConcurrentHashMap<Long, OrderStatus>();
}

/**
* 내부 Map에 라이더 정보를 갱신한다.
* 만약 Map 내부에 정보가 없다면 새롭게 정보를 추가한다.
* 라이더 정보가 저장되면 라이더는 실시간으로 정보를 업데이트해야한다.
* @param riderInfo 라이더 정보
*/
@Override
public void updateRiderInfo(DeliveryRiderDTO riderInfo) {
if (riders.containsKey(riderInfo.getId())) {
riders.replace(riderInfo.getId(), riderInfo);
yyy9942 marked this conversation as resolved.
Show resolved Hide resolved
} else {
riders.put(riderInfo.getId(), riderInfo);
Copy link
Member

Choose a reason for hiding this comment

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

여기 또한 두 단계가 atomic하지 않습니다

  1. 없으면
  2. 삽입한다

}
}

/**
* 배달 대기를 제거한다.
* @author jun
* @param riderId 제거할 라이더의 아이디
*/
@Override
public long deleteRiderInfo(String riderId) {
Copy link
Member

Choose a reason for hiding this comment

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

삭제성공여부를 리턴하는 것 같은데 boolean으로 리턴하는건 어떨까요?

return riders.remove(riderId) == null ? 0 : 1;
}

/**
* 해당 라이더가 저장소 내에 존재하는지 확인한다.
* @author jun
* @param riderId 라이더 아이디
* @return
*/
@Override
public boolean hasRiderInfo(String riderId) {
return riders.containsKey(riderId);
}

/**
* 리스트 형태로 라이더를 조회한다.
* @author jun
* @return
*/
@Override
public List<DeliveryRiderDTO> toList() {
Copy link
Member

Choose a reason for hiding this comment

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

메소드의 동작을 유추하기에는 getRiderList가 더 나을 것 같습니다

return riders.values().stream().collect(Collectors.toList());
}


/**
* 일정 시간동안 자신의 위치를 업데이트 하지 않는 라이더를 제거한다.
* @author jun
*/
@Override
public void deleteNonUpdatedRiders() {
riders.values().stream()
.filter(
e -> ChronoUnit.SECONDS.between(e.getUpdatedAt(), LocalDateTime.now()) > expireTime)
.forEach(e -> riders.remove(e.getId()));
Copy link
Member

Choose a reason for hiding this comment

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

이곳은 atomic하지 않지만, 특정 시점을 중심으로 데이터를 삭제해주는것이므로 문제가 없습니다.

}

/**
* 라이더의 정보를 조회한다.
* @author jun
*/
@Override
public DeliveryRiderDTO getRiderInfo(String riderId) {
return riders.get(riderId);
}

/**
* 리스트로 받은 아이디를 기반으로 라이더를 배달 매칭에서 제거한다.
* @param idList 라이더의 아이디들
* @return 지워진 라이더 개수
*/
@Override
public long deleteAll(List<String> idList) {
long deleteCount = 0;
for (String id : idList) {
deleteCount += deleteRiderInfo(id);
}
return deleteCount;
Copy link
Member

Choose a reason for hiding this comment

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

여러 스레드가 접근하므로 유효한 삭제가 몇 개인지 세는것은 의미가 없을 것 같습니다. 삭제가 실패했더라도 처리해줄 수 있는게 없기 때문입니다

}

/**
* 주문의 상태를 조회한다.
* 주문 정보가 내부 메모리에 없다면 DB에서 조회한 후 메모리에 저장한다.
* @author jun
* @param orderId 조회할 주문 아이디
*/
@Override
public OrderStatus getOrderStatus(Long orderId) {
OrderStatus status = orders.get(orderId);
Copy link
Member

Choose a reason for hiding this comment

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

이 때에는 order에 없지만

if (Objects.isNull(status)) {
status = orderService.getOrderStatus(orderId);
setOrderStatus(orderId, status);
Copy link
Member

Choose a reason for hiding this comment

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

이 때는 order에 있으면 어떻게 될까요? 중복으로 put을 하게 되지 않을까요?

return status;
}

return status;
}

/**
* 주문 정보를 내부에 저장한다.
* @author jun
* @param orderId 저장할 주문 아이디
* @param status 주문의 상태
*/
@Override
public void setOrderStatus(Long orderId, OrderStatus status) {
if (orders.containsKey(orderId)) {
orders.replace(orderId, status);
} else {
orders.put(orderId, status);
}
}

/**
* 주문 정보를 삭제한다.
* @param orderId 삭제할 주문 정보 아이디
* @author jun
*/
@Override
public void deleteOrderStatus(Long orderId) {
orders.remove(orderId);
}
}
25 changes: 9 additions & 16 deletions src/main/java/com/delfood/dto/push/PushMessage.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.delfood.dto.push;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import org.joda.time.LocalDateTime;
Expand All @@ -12,6 +11,14 @@ public class PushMessage {
@NonNull
private String message;

public static final PushMessage ADD_ORDER_REQUEST = new PushMessage("DelFood 주문", "새로운 주문이 들어왔습니다");
public static final PushMessage ACCEPT_ORDER_REQUEST = new PushMessage("DelFood 접수", "주문이 접수되었습니다");
public static final PushMessage REQUIRED_ORDER_REQUEST = new PushMessage("DelFood 주문취소", "매장에서 주문을 취소하였습니다");
public static final PushMessage DELIVERY_MATCH = new PushMessage("DelFood 배달원 매칭", "배달원이 매칭되었습니다");
public static final PushMessage DELIVERY_START = new PushMessage("DelFood 배달 시작", "음식 배달이 시작되었습니다");
public static final PushMessage DELIVERY_SUCCESS = new PushMessage("DelFood 배달 완료", "배달이 완료되었습니다");
public static final PushMessage DELIVERY_REQUEST = new PushMessage("DelFood 배달 요청", "근처 매장에서 배달을 요청했습니다.");

private LocalDateTime generatedTime;

public PushMessage(String title, String message) {
Expand All @@ -20,21 +27,7 @@ public PushMessage(String title, String message) {
this.generatedTime = LocalDateTime.now();
}



public static PushMessage getMessasge(Type type) {
return type.pushMessage;
}

@AllArgsConstructor
public static enum Type {
addOrderRequest(new PushMessage("DelFood 주문", "새로운 주문이 들어왔습니다")),
acceptOrderRequest(new PushMessage("DelFood 접수", "주문이 접수되었습니다")),
requiredOrderRequest(new PushMessage("DelFood 주문취소", "매장에서 주문을 취소하였습니다")),
deliveryMatch(new PushMessage("DelFood 배달원 매칭", "배달원이 매칭되었습니다")),
deliveryStart(new PushMessage("DelFood 배달 시작", "음식 배달이 시작되었습니다")),
deliverySuccess(new PushMessage("DelFood 배달 완료", "배달이 완료되었습니다"));

private PushMessage pushMessage;
}

}
35 changes: 35 additions & 0 deletions src/main/java/com/delfood/dto/rider/AcceptDeliveryRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.delfood.dto.rider;

import java.time.LocalDateTime;

import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;

@Getter
@Builder
public class AcceptDeliveryRequestDTO {

@NonNull
private Long orderId;

@NonNull
private String riderId;

@NonNull
private RequestResult result;

private LocalDateTime startedAt;


@Builder
public AcceptDeliveryRequestDTO(Long orderId, String riderId) {
this.orderId = orderId;
this.riderId = riderId;
startedAt = LocalDateTime.now();
}

public static enum RequestResult {
SUCCESS, FAIL
}
}
Loading