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

[OING-177] feat: 매일 사용자 휴대폰 FCM 노티 발송 기능 추가 #145

Merged
merged 4 commits into from
Feb 3, 2024
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
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ subprojects {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'com.github.f4b6a3:ulid-creator:5.2.2'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'com.google.firebase:firebase-admin:9.2.0'
implementation 'com.google.api-client:google-api-client:1.32.1'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.3'
Expand Down
15 changes: 15 additions & 0 deletions common/src/main/java/com/oing/service/FCMNotificationService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.oing.service;

import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.MulticastMessage;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 4:09 AM
*/
public interface FCMNotificationService {
void sendMessage(Message message);
void sendMulticastMessage(MulticastMessage message);
}
41 changes: 41 additions & 0 deletions gateway/src/main/java/com/oing/component/ApplicationListener.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.oing.component;

import com.oing.domain.BulkNotificationCompletedEvent;
import com.oing.domain.ErrorReportDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
Expand Down Expand Up @@ -27,6 +28,46 @@ private boolean isProductionInstance() {
return activeProfiles != null && (Arrays.asList(activeProfiles).contains("prod") || Arrays.asList(activeProfiles).contains("dev"));
}

@Async
@EventListener
public void onNotificationSent(BulkNotificationCompletedEvent event) {
if (!isProductionInstance()) return;
SlackGateway.SlackBotDto dto = SlackGateway.SlackBotDto.builder()
.attachments(List.of(
SlackGateway.SlackBotAttachmentDto.builder()
.authorName("no5ing-server")
.title("알림 발송 리포트")
.color("#abcdef")
.text("백엔드 서버에서 FCM 벌크 알림이 발송되었습니다")
.fields(List.of(
SlackGateway.SlackBotFieldDto.builder()
.title("알림 종류")
.value(event.reason())
.shortField(false)
.build(),
SlackGateway.SlackBotFieldDto.builder()
.title("알람 발송 대상 기기 수")
.value(String.valueOf(event.totalTargets()))
.shortField(true)
.build(),
SlackGateway.SlackBotFieldDto.builder()
.title("알람 발송 대상 사용자 수")
.value(String.valueOf(event.totalMembers()))
.shortField(true)
.build(),
SlackGateway.SlackBotFieldDto.builder()
.title("소요시간 (밀리초)")
.value(String.valueOf(event.elapsedMillis()))
.shortField(true)
.build()
))
.build()
))
.build();

slackGateway.sendSlackBotMessage(dto);
}

@Async
@EventListener
public void onErrorReport(ErrorReportDTO errorReportDTO) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.oing.component;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.MulticastMessage;
import com.oing.service.FCMNotificationService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.stereotype.Component;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 4:10 AM
*/

@RequiredArgsConstructor
@Component
@ConditionalOnBean(FirebaseMessaging.class)
public class FCMNotificationServiceImpl implements FCMNotificationService {
private final FirebaseMessaging firebaseMessaging;

@Override
public void sendMessage(Message message) {
firebaseMessaging.sendAsync(message);
}

@Override
public void sendMulticastMessage(MulticastMessage message) {
firebaseMessaging.sendEachForMulticastAsync(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.oing.component;

import com.google.firebase.messaging.FirebaseMessaging;
import com.google.firebase.messaging.Message;
import com.google.firebase.messaging.MulticastMessage;
import com.oing.service.FCMNotificationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Component;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 4:10 AM
*/

@Slf4j
@RequiredArgsConstructor
@Component
@ConditionalOnMissingBean(FirebaseMessaging.class)
public class MockFCMNotificationServiceImpl implements FCMNotificationService {

@Override
public void sendMessage(Message message) {
log.info("MockFCMNotificationServiceImpl.sendMessage: {}", message);
}

@Override
public void sendMulticastMessage(MulticastMessage message) {
log.info("MockFCMNotificationServiceImpl.sendMulticastMessage: {}", message);
}
}
2 changes: 1 addition & 1 deletion gateway/src/main/java/com/oing/component/SlackGateway.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static class SlackBotAttachmentDto {
@JsonProperty("mrkdwn_in")
final String mrkdwn_in = "[\"text\"]";
@JsonProperty("color")
final String color = "#ff2400";
String color = "#ff2400";
@JsonProperty("author_name")
final String authorName;
@JsonProperty("title")
Expand Down
36 changes: 36 additions & 0 deletions gateway/src/main/java/com/oing/config/FirebaseConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.oing.config;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.firebase.FirebaseApp;
import com.google.firebase.FirebaseOptions;
import com.google.firebase.messaging.FirebaseMessaging;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Base64;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 3:59 AM
*/
@Profile({"prod", "dev"})
@Configuration
public class FirebaseConfig {
@Value("${cloud.firebase}")
private String firebaseSecret;

@Bean
public FirebaseMessaging firebaseMessaging() throws IOException {
ByteArrayInputStream credentials = new ByteArrayInputStream(Base64.getDecoder().decode(firebaseSecret));
FirebaseOptions options = FirebaseOptions.builder()
.setCredentials(GoogleCredentials.fromStream(credentials)).build();
FirebaseApp firebaseApp = FirebaseApp.initializeApp(options);
return FirebaseMessaging.getInstance(firebaseApp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.oing.domain;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 4:49 AM
*/
public record BulkNotificationCompletedEvent(
String reason,
int totalTargets,
int totalMembers,
long elapsedMillis
) {
}
122 changes: 122 additions & 0 deletions gateway/src/main/java/com/oing/job/DailyNotificationJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package com.oing.job;

import com.google.common.collect.Lists;
import com.google.firebase.messaging.ApnsConfig;
import com.google.firebase.messaging.Aps;
import com.google.firebase.messaging.MulticastMessage;
import com.google.firebase.messaging.Notification;
import com.oing.domain.BulkNotificationCompletedEvent;
import com.oing.domain.Member;
import com.oing.service.FCMNotificationService;
import com.oing.service.MemberDeviceService;
import com.oing.service.MemberPostService;
import com.oing.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.util.HashSet;
import java.util.List;

/**
* no5ing-server
* User: CChuYong
* Date: 2/2/24
* Time: 4:16 AM
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class DailyNotificationJob {
private final ApplicationEventPublisher eventPublisher;
private final FCMNotificationService fcmNotificationService;

private final MemberService memberService;
private final MemberDeviceService memberDeviceService;
private final MemberPostService memberPostService;

@Scheduled(cron = "0 0 12 * * *", zone = "Asia/Seoul") // 12:00 PM
public void sendDailyUploadNotification() {
long start = System.currentTimeMillis();
log.info("[DailyNotificationJob] 오늘 업로드 알림 전송 시작");
HashSet<String> targetFcmTokens = new HashSet<>();
List<Member> members = memberService.findAllMember();
for (Member member : members) {
targetFcmTokens.addAll(memberDeviceService.getFcmTokensByMemberId(member.getId()));
}

Lists.partition(targetFcmTokens.stream().toList(), 500).forEach(partitionedList -> {
MulticastMessage multicastMessage = MulticastMessage.builder()
.setNotification(
buildNotification("삐삐", "지금 바로 가족에게 일상 공유를 해볼까요?")
)
.addAllTokens(partitionedList)
.setApnsConfig(buildApnsConfig())
.build();
fcmNotificationService.sendMulticastMessage(multicastMessage);
});
log.info("[DailyNotificationJob] 오늘 업로드 알림 전송 완료. (총 {}명, {}토큰) 소요시간 : {}ms",
members.size(),
targetFcmTokens.size(),
System.currentTimeMillis() - start);

eventPublisher.publishEvent(
new BulkNotificationCompletedEvent(
"오늘 업로드 알림 전송 완료", targetFcmTokens.size(), members.size(),
System.currentTimeMillis() - start
)
);
}

@Scheduled(cron = "0 30 23 * * *", zone = "Asia/Seoul") // 11:30 PM
public void sendDailyRemainingNotification() {
long start = System.currentTimeMillis();
log.info("[DailyNotificationJob] 오늘 미 업로드 사용자 대상 알림 전송 시작");
LocalDate today = LocalDate.now();
List<Member> allMembers = memberService.findAllMember();
HashSet<String> targetFcmTokens = new HashSet<>();
HashSet<String> postedMemberIds = new HashSet<>(memberPostService.getMemberIdsPostedToday(today));
allMembers.stream()
.filter(member -> !postedMemberIds.contains(member.getId())) //오늘 업로드한 사람이 아닌 사람들은
.forEach(member -> targetFcmTokens.addAll(memberDeviceService.getFcmTokensByMemberId(member.getId())));

Lists.partition(targetFcmTokens.stream().toList(), 500).forEach(partitionedList -> {
MulticastMessage multicastMessage = MulticastMessage.builder()
.setNotification(
buildNotification("삐삐", "사진을 공유할 수 있는 시간이 얼마 남지 않았어요.")
)
.addAllTokens(partitionedList)
.setApnsConfig(buildApnsConfig())
.build();
fcmNotificationService.sendMulticastMessage(multicastMessage);
});
log.info("[DailyNotificationJob] 오늘 미 업로드 사용자 대상 알림 전송 완료. (총 {}명, {}토큰) 소요시간 : {}ms",
allMembers.size() - postedMemberIds.size(),
targetFcmTokens.size(),
System.currentTimeMillis() - start);

eventPublisher.publishEvent(
new BulkNotificationCompletedEvent(
"오늘 미업로드자 업로드 알림 전송 완료", targetFcmTokens.size(),
allMembers.size() - postedMemberIds.size(),
System.currentTimeMillis() - start
)
);
}

private Notification buildNotification(String title, String body){
return Notification.builder()
.setTitle(title)
.setBody(body)
.build();
}

private ApnsConfig buildApnsConfig(){
return ApnsConfig.builder()
.setAps(Aps.builder().setSound("default").build())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ public class MemberPostRepositoryCustomImpl implements MemberPostRepositoryCusto

private final JPAQueryFactory queryFactory;

@Override
public List<String> getMemberIdsPostedToday(LocalDate date) {
return queryFactory
.select(memberPost.memberId)
.from(memberPost)
.where(memberPost.createdAt.between(date.atStartOfDay(), date.atTime(23, 59, 59)))
.fetch();
}

@Override
public List<MemberPost> findLatestPostOfEveryday(List<String> memberIds, LocalDateTime startDate, LocalDateTime endDate) {
return queryFactory
Expand Down
2 changes: 1 addition & 1 deletion gateway/src/main/resources/application-prod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,4 @@ management:
endpoints:
web:
exposure:
include: health,info,prometheus
include: health,info,prometheus
3 changes: 2 additions & 1 deletion gateway/src/main/resources/application.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ logging:
com.oing: DEBUG

cloud:
firebase: ${FIREBASE_SECRET}
ncp:
region: ${OBJECT_STORAGE_REGION}
end-point: ${OBJECT_STORAGE_END_POINT}
Expand All @@ -111,4 +112,4 @@ management:
endpoints:
web:
exposure:
include: health,info,prometheus
include: health,info,prometheus
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@
import com.oing.domain.key.MemberDeviceKey;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemberDeviceRepository extends JpaRepository<MemberDevice, MemberDeviceKey> {
List<MemberDevice> findAllByMemberId(String memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ public interface MemberRepository extends JpaRepository<Member, String> {
Page<Member> findAllByFamilyIdAndDeletedAtIsNull(String familyId, PageRequest pageRequest);

long countByFamilyIdAndFamilyJoinAtBefore(String familyId, LocalDateTime dateTime);

List<Member> findAllByDeletedAtIsNull();
}
Loading
Loading