Skip to content

Commit

Permalink
Merge pull request #82 from f-lab-edu/feature/78-alarm-message
Browse files Browse the repository at this point in the history
[#78] alarm message
  • Loading branch information
JinDDung2 authored Apr 24, 2024
2 parents bd80602 + 0cad55e commit fbce7f8
Show file tree
Hide file tree
Showing 12 changed files with 301 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.jinddung2.givemeticon.common.config;

import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
import org.springframework.kafka.core.ConsumerFactory;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class NotificationConsumerConfig {

@Value("${bootstrap.server}")
private String bootstrapServer;

@Bean
public ConcurrentKafkaListenerContainerFactory<String, CreateNotificationRequestDto> notificationListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, CreateNotificationRequestDto> factory =
new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(notificationConsumerFactory());

return factory;
}

@Bean
public ConsumerFactory<String, CreateNotificationRequestDto> notificationConsumerFactory() {
return new DefaultKafkaConsumerFactory<>(
notificationConsumerConfigs(),
new StringDeserializer(),
new JsonDeserializer<>(CreateNotificationRequestDto.class));
}

// FIXME: bootstrap-server 변경 예정
private Map<String, Object> notificationConsumerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "alarm");
return props;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.jinddung2.givemeticon.common.config;

import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class NotificationProducerConfig {

@Value("${bootstrap.server}")
private String bootstrapServer;

@Bean
public KafkaTemplate<String, CreateNotificationRequestDto> notificationKafkaTemplate() {
return new KafkaTemplate<>(notificationProducerFactory());
}

@Bean
public ProducerFactory<String, CreateNotificationRequestDto> notificationProducerFactory() {
return new DefaultKafkaProducerFactory<>(notificationProducerConfigs());
}

// FIXME: bootstrap-server 변경 예정
@Bean
public Map<String, Object> notificationProducerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServer);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
return props;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.jinddung2.givemeticon.domain.notification.consumer;

import com.jinddung2.givemeticon.domain.notification.domain.Notification;
import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import com.jinddung2.givemeticon.domain.notification.mapper.NotificationMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.KafkaException;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
@Slf4j
public class NotificationConsumer {
private final NotificationMapper notificationMapper;

@KafkaListener(topics = "alarm", groupId = "alarm", containerFactory = "notificationListenerContainerFactory")
public void consume(ConsumerRecord<String, CreateNotificationRequestDto> record) {
try {
notificationMapper.save(new Notification(
record.value().saleId(),
record.value().sellerId(),
record.value().message(),
false
));
log.info("consumed message={}", record.value());
} catch (KafkaException e) {
log.error("failed to create alarm:: msg={}", e.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.jinddung2.givemeticon.domain.notification.domain;

import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Getter
@NoArgsConstructor
public class Notification {

private int id;
private int saleId;
private int sellerId;
private String message;
private boolean isRead;
private LocalDateTime createdDate;

public Notification(int saleId, int sellerId, String message, boolean isRead) {
this.saleId = saleId;
this.sellerId = sellerId;
this.message = message;
this.isRead = isRead;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.jinddung2.givemeticon.domain.notification.domain.dto;

public record CreateNotificationRequestDto (
int saleId,
int sellerId,
String message
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.jinddung2.givemeticon.domain.notification.mapper;

import com.jinddung2.givemeticon.domain.notification.domain.Notification;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface NotificationMapper {
int save(Notification notification);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.jinddung2.givemeticon.domain.notification.producer;

import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class NotificationProducer {

private final String TOPIC_NAME = "alarm";
private final KafkaTemplate<String, CreateNotificationRequestDto> kafkaTemplate;

public NotificationProducer(KafkaTemplate<String, CreateNotificationRequestDto> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}

public void create(CreateNotificationRequestDto request) {
kafkaTemplate.send(TOPIC_NAME, request);
log.info("kafka producer message request={}", request);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.jinddung2.givemeticon.domain.item.domain.Item;
import com.jinddung2.givemeticon.domain.item.service.ItemService;
import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import com.jinddung2.givemeticon.domain.notification.producer.NotificationProducer;
import com.jinddung2.givemeticon.domain.sale.domain.Sale;
import com.jinddung2.givemeticon.domain.sale.service.SaleService;
import com.jinddung2.givemeticon.domain.trade.controller.dto.TradeDto;
Expand All @@ -27,6 +29,7 @@ public class TradeSaleItemUserFacade {
private final SaleService saleService;
private final ItemService itemService;
private final TradeService tradeService;
private final NotificationProducer producer;

@Transactional
public int transact(int saleId, int buyerId) {
Expand All @@ -48,6 +51,8 @@ public int transact(int saleId, int buyerId) {

sale.updateBoughtState();
saleService.update(sale);
producer.create(new CreateNotificationRequestDto(saleId, sale.getSellerId(),
String.format("%s이(가) 판매되었습니다.", item.getName())));
return tradeService.save(trade, restDay);
}

Expand Down Expand Up @@ -96,6 +101,11 @@ public List<TradeDto> getUnusedTradeHistory(int buyerId, boolean orderByBoughtDa
public void buyConfirmation(int tradeId, int buyerId) {
checkUserExists(buyerId);
tradeService.buyConfirmation(tradeId, buyerId);
Trade trade = tradeService.getTrade(tradeId);
Sale sale = saleService.getSale(trade.getSaleId());
Item item = itemService.getItem(sale.getItemId());
producer.create(new CreateNotificationRequestDto(sale.getId(), sale.getSellerId(),
String.format("%s이(가) 구매 확정되었습니다.", item.getName())));
}

private long getRestDay(LocalDate expiredDate) {
Expand Down
20 changes: 20 additions & 0 deletions src/main/resources/mapper/NotificationMapper.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.jinddung2.givemeticon.domain.notification.mapper.NotificationMapper">
<insert id="save" parameterType="Notification" useGeneratedKeys="true" keyProperty="id">
INSERT INTO notification (id,
sale_id,
seller_id,
message,
is_read,
created_date)
VALUES (#{id},
#{saleId},
#{sellerId},
#{message},
#{isRead},
NOW())
</insert>
</mapper>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.jinddung2.givemeticon.domain.notification.consumer;

import com.jinddung2.givemeticon.domain.notification.domain.Notification;
import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import com.jinddung2.givemeticon.domain.notification.mapper.NotificationMapper;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class NotificationConsumerTest {

@InjectMocks
NotificationConsumer notificationConsumer;

@Mock
NotificationMapper notificationMapper;

int saleId = 1;
int sellerId = 2;
int notificationId= 3;
@Test
@DisplayName("알람 요청을 consume 해서 DB에 저장한다.")
void consume() {
CreateNotificationRequestDto requestFakeDto = new CreateNotificationRequestDto(saleId, sellerId, "fakeMessage");
ConsumerRecord<String, CreateNotificationRequestDto> record =
new ConsumerRecord<>("fakeTopic", 0, 0, "fakeKey", requestFakeDto);

Mockito.when(notificationMapper.save(Mockito.any(Notification.class))).thenReturn(notificationId);

notificationConsumer.consume(record);

Mockito.verify(notificationMapper).save(Mockito.any(Notification.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.jinddung2.givemeticon.domain.notification.producer;

import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.kafka.core.KafkaTemplate;

@ExtendWith(MockitoExtension.class)
class NotificationProducerTest {

@InjectMocks
private NotificationProducer notificationProducer;

@Mock
private KafkaTemplate<String, CreateNotificationRequestDto> kafkaTemplate;

int saleId = 1;
int sellerId = 2;

@Test
@DisplayName("알람 요청을 만들어 \"alarm\" 이라는 토픽을 카프카 큐에 넣는다.")
void create() {
CreateNotificationRequestDto fakeDto =
new CreateNotificationRequestDto(saleId, sellerId, "fakeMessage");

notificationProducer.create(fakeDto);

Mockito.verify(kafkaTemplate).send(Mockito.eq("alarm"), Mockito.eq(fakeDto));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.jinddung2.givemeticon.domain.item.domain.Item;
import com.jinddung2.givemeticon.domain.item.service.ItemService;
import com.jinddung2.givemeticon.domain.notification.domain.dto.CreateNotificationRequestDto;
import com.jinddung2.givemeticon.domain.notification.producer.NotificationProducer;
import com.jinddung2.givemeticon.domain.sale.domain.Sale;
import com.jinddung2.givemeticon.domain.sale.service.SaleService;
import com.jinddung2.givemeticon.domain.trade.controller.dto.TradeDto;
Expand Down Expand Up @@ -41,6 +43,9 @@ class TradeSaleItemUserFacadeTest {
@Mock
TradeService tradeService;

@Mock
NotificationProducer producer;

int buyerId;
int saleId;
int itemId;
Expand Down Expand Up @@ -85,6 +90,7 @@ void transact() {

Mockito.verify(saleService).update(sale);
Mockito.verify(tradeService).save(Mockito.any(Trade.class), Mockito.any(Long.class));
Mockito.verify(producer).create(Mockito.any(CreateNotificationRequestDto.class));
}

@Test
Expand Down Expand Up @@ -158,9 +164,13 @@ void get_My_Unused_Trade_History() {
@DisplayName("구매 확정에 성공한다.")
void buy_Confirmation() {
Mockito.when(userService.isExists(buyerId)).thenReturn(true);
Mockito.when(tradeService.getTrade(tradeId)).thenReturn(trade);
Mockito.when(saleService.getSale(saleId)).thenReturn(sale);
Mockito.when(itemService.getItem(itemId)).thenReturn(item);

tradeSaleItemUserFacade.buyConfirmation(tradeId, buyerId);

Mockito.verify(tradeService).buyConfirmation(tradeId, buyerId);
Mockito.verify(producer).create(Mockito.any(CreateNotificationRequestDto.class));
}
}

0 comments on commit fbce7f8

Please sign in to comment.