notification

This commit is contained in:
Mauve
2026-09-15 17:04:55 +08:00
parent cc4739711c
commit e4125149a5
23 changed files with 885 additions and 3 deletions
+10
View File
@@ -120,6 +120,16 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>com.eatthepath</groupId>
<artifactId>pushy</artifactId>
<version>0.15.4</version>
</dependency>
</dependencies>
<build>
<plugins>
@@ -9,7 +9,11 @@ import java.io.InputStreamReader;
import java.util.*;
import com.auth0.jwt.JWT;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.mgm.mcrm.apigateway.common.dtos.LoginUserInfo;
import com.mgm.mcrm.apigateway.common.utils.LogUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.system.ApplicationHome;
import org.springframework.core.io.ClassPathResource;
@@ -99,4 +103,27 @@ public class SysCommon {
return result;
}
public static LoginUserInfo parseUserFromToken(String tokenContent) {
LoginUserInfo loginUser = null;
if(StringUtils.isNotBlank(tokenContent)) {
try {
tokenContent = tokenContent.replace("Bearer ", "");
DecodedJWT decodeData = JWT.decode(tokenContent);
loginUser = new LoginUserInfo();
String userId = decodeData.getSubject();
if(StringUtils.isNotBlank(userId)) {
loginUser.setId(Long.parseLong(userId));
}else {
loginUser.setId(-1L);
}
loginUser.setLoginId(decodeData.getClaim("login_id").asString());
loginUser.setGroupName(decodeData.getClaim("user_group").asString());
}catch(Exception err) {
LogUtil.error(SysCommon.class, "Parse Token failed:"+err.getMessage());
loginUser =null;
}
}
return loginUser;
}
}
@@ -0,0 +1,12 @@
package com.mgm.mcrm.apigateway.common.dtos;
import lombok.Data;
@Data
public class LoginUserInfo {
private Long id;
private String loginId;
private String displayName;
private String groupName;
private String tokenUuid;
}
@@ -0,0 +1,39 @@
package com.mgm.mcrm.apigateway.common.utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@Component
public class NotificationUtil {
public String distinguishLanguage(String inputMsg,Integer languageIndex){
if (StringUtils.isBlank(inputMsg)) {
return inputMsg;
}
String preferredTag = Integer.valueOf(1).equals(languageIndex) ? "CN" : "EN";
String fallbackTag = Integer.valueOf(1).equals(languageIndex) ? "EN" : "CN";
String preferredText = extractNotificationText(inputMsg, preferredTag);
if (StringUtils.isNotBlank(preferredText)) {
return preferredText;
}
String fallbackText = extractNotificationText(inputMsg, fallbackTag);
return StringUtils.isNotBlank(fallbackText) ? fallbackText : inputMsg;
}
private static String extractNotificationText(String text, String tag) {
String marker = "#" + tag + "#";
int start = text.indexOf(marker);
if (start < 0) {
return null;
}
start += marker.length();
int end = text.indexOf(marker, start);
if (end < 0) {
return null;
}
return text.substring(start, end).trim();
}
}
@@ -46,6 +46,12 @@ public class DaylightConfigurationScheduler {
public void fetchDaylightConfigurationOnStartupIfMissing() {
LogUtil.info(DaylightConfigurationScheduler.class, "Checking saved Daylight configurations on startup");
try {
if (cfgDaylightConfigurationDao.existsSavedDaylightConfiguration()) {
LogUtil.info(DaylightConfigurationScheduler.class, "Saved Daylight configurations found, skip startup fetch");
return;
}
LogUtil.warn(DaylightConfigurationScheduler.class, "No saved Daylight configurations found, start startup fetch");
BaseApiResponse configResponse = daylightFetchConfigurationService.fetchDaylightConfiguration();
if (configResponse.isSuccess()) {
LogUtil.info(DaylightConfigurationScheduler.class, "Startup configurations completed: " + configResponse.getMessage());
@@ -67,6 +73,12 @@ public class DaylightConfigurationScheduler {
return;
}
if (cfgDaylightConfigurationDao.existsSavedDaylightBlockCodes()) {
LogUtil.info(DaylightConfigurationScheduler.class, "Saved Daylight block codes found, skip startup fetch");
return;
}
LogUtil.warn(DaylightConfigurationScheduler.class, "No saved Daylight block codes found, start startup fetch");
BaseApiResponse blockResponse = daylightFetchConfigurationService.fetchDaylightBlockCodes();
if (blockResponse.isSuccess()) {
LogUtil.info(DaylightConfigurationScheduler.class, "Startup block codes completed: " + blockResponse.getMessage());
@@ -106,7 +106,7 @@ public class DaylightFetchConfigurationService {
String apiUrl = daylightApiSetting.getServerUrl() + path;
String types = "PurchaseElement,MarketSegment,RatePlan,BookingSource,MembershipLevel," +
"Attribute,RoomType,GuaranteeType,DistributionChannel,AgeBucket,CancellationReason," +
"ComplimentaryAccountingPreset,ComplimentaryType,TransactionCode";
"ComplimentaryAccountingPreset,ComplimentaryType,TransactionCode,Room";
String pagingNext;
int pageNumber = 1;
@@ -29,4 +29,11 @@ public interface CfgDaylightConfigurationDao extends JpaRepository<CfgDaylightCo
+ "and c.configValue is not null "
+ "and c.configValue <> ''")
boolean existsSavedConfigurationByDlPropertyId(String dlPropertyId);
@Query("select case when count(c) > 0 then true else false end "
+ "from CfgDaylightConfiguration c "
+ "where c.configName = 'BlockCode' "
+ "and c.configValue is not null "
+ "and c.configValue <> ''")
boolean existsSavedDaylightBlockCodes();
}
@@ -7,6 +7,7 @@ import com.mgm.mcrm.apigateway.logging.services.WebhookLogService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Slf4j
@Tag(name = "Allotment", description = "APIs for Room Allotment functions")
@RestController
@RequestMapping("/api/daylight/webhook")
@@ -56,6 +58,7 @@ public class DaylightWebhook {
}catch(Exception err){
result.setSuccess(false);
result.setMessage("Handle Message ["+messageId+"] failed with error:"+err.getMessage());
log.error("Handle Message [{}] failed with error:", messageId, err);
}
}
}
@@ -48,6 +48,7 @@ public class ReservationNotificationHandler implements DaylightWebhookHandler {
private Bookings convertEventToBooking(ReservationNotificationEvent eventData) {
log.info("Reservation Notification Event Data: {}", JSON.toJSONString(eventData));
Bookings result = new Bookings();
@@ -0,0 +1,13 @@
package com.mgm.mcrm.apigateway.logging.dao;
import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SysLogDeviceTokenRepository extends JpaRepository<SysLogDeviceToken, Long> {
SysLogDeviceToken findByUserId(String bookerId);
void deleteByUserId(String userId);
}
@@ -0,0 +1,39 @@
package com.mgm.mcrm.apigateway.logging.models;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.util.Date;
@Data
@Entity
@Table(name = "sys_log_device_token",
indexes = {
@Index(name = "idx_device_token", columnList = "deviceToken")
})
public class SysLogDeviceToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Integer id;
@Column(name = "user_id", unique = true, nullable = false, length = 64)
private String userId;
@Column(name = "device_token", length = 255)
private String deviceToken;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private Date createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private Date updatedAt;
@Column(name = "use_language_type")
private Integer useLanguageType;
}
@@ -0,0 +1,66 @@
package com.mgm.mcrm.apigateway.logging.models;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.Nationalized;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "sys_log_notification")
public class SysLogNotification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name = "user_id", length = 64, nullable = false)
private String userId;
@Column(name = "sender_id", length = 64)
private String senderId;
@Column(name = "transaction_id")
private String transactionId;
@Column(name = "device_token")
private String deviceToken;
@Column(name = "status", nullable = false)
private Integer status;
@Nationalized
@Column(name = "title", columnDefinition = "NVARCHAR(MAX)")
private String title;
@Nationalized
@Column(name = "content", columnDefinition = "NVARCHAR(MAX)")
private String content;
@Column(name = "booking_id", length = 100)
private String bookingId;
@Column(name = "confirmation_no", length = 100)
private String confirmationNo;
@Column(name = "booking_type")
private String bookingType;
@Column(name = "result_message", length = 512)
private String resultMessage;
@Column(name = "apn_response_id")
private String apnResponseId;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "created_at")
private LocalDateTime createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}
@@ -0,0 +1,13 @@
package com.mgm.mcrm.apigateway.logging.repositories;
import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SysLogDeviceTokenDao extends JpaRepository<SysLogDeviceToken, Long> {
SysLogDeviceToken findByUserId(String bookerId);
void deleteByUserId(String userId);
}
@@ -0,0 +1,14 @@
package com.mgm.mcrm.apigateway.logging.repositories;
import com.mgm.mcrm.apigateway.logging.models.SysLogNotification;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface SysLogNotificationRepository extends JpaRepository<SysLogNotification, Long> {
List<SysLogNotification> findByUserIdAndStatus(String userId, Integer status);
List<SysLogNotification> findAllByDeviceTokenAndTransactionId(String deviceToken, String transactionId);
long countByUserIdAndStatus(String userId, Integer status);
}
@@ -0,0 +1,13 @@
package com.mgm.mcrm.apigateway.logging.services;
import com.mgm.mcrm.apigateway.common.utils.LogUtil;
import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken;
import com.mgm.mcrm.apigateway.logging.repositories.SysLogDeviceTokenDao;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class SysLogDeviceTokenService {
}
@@ -0,0 +1,142 @@
package com.mgm.mcrm.apigateway.notification.services;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.PushNotificationResponse;
import com.eatthepath.pushy.apns.util.SimpleApnsPayloadBuilder;
import com.eatthepath.pushy.apns.util.SimpleApnsPushNotification;
import com.eatthepath.pushy.apns.util.concurrent.PushNotificationFuture;
import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse;
import com.mgm.mcrm.apigateway.common.utils.LogUtil;
import com.mgm.mcrm.apigateway.logging.models.SysLogNotification;
import com.mgm.mcrm.apigateway.logging.repositories.SysLogNotificationRepository;
import jakarta.annotation.Resource;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class ApnNotificationService {
@Autowired
private ApnsClient apnsClient;
@Value("${apns.bundle-id}")
private String bundleId;
@Resource
private SysLogNotificationRepository notificationLogDao;
private final Logger logger = LoggerFactory.getLogger(ApnNotificationService.class);
public BaseApiResponse pushApnMessage(String transactionId, String deviceToken, String title, String body, boolean isWaitResponse) {
return pushApnMessage(transactionId, deviceToken, title, body, isWaitResponse, null);
}
public BaseApiResponse pushApnMessage(String transactionId, String deviceToken, String title, String body, boolean isWaitResponse, String confirmNo) {
BaseApiResponse result = new BaseApiResponse();
if(StringUtils.isBlank(deviceToken)){
result.setSuccess(false);
result.setMessage("Device Token is empty");
return result;
}else{
LogUtil.info(ApnNotificationService.class, "Pushing Apn Start");
SimpleApnsPayloadBuilder payloadBuilder = new SimpleApnsPayloadBuilder();
payloadBuilder
.setAlertTitle(title)
.setAlertBody(body)
.setSound("default")
.setBadgeNumber(1);
if(StringUtils.isNotBlank(confirmNo)){
payloadBuilder.addCustomProperty("ConfirmNo", confirmNo);
}
String payload = payloadBuilder.build();
SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(
deviceToken,
bundleId,
payload
);
PushNotificationFuture<SimpleApnsPushNotification, PushNotificationResponse<SimpleApnsPushNotification>>
sendFuture = apnsClient.sendNotification(pushNotification);
if(isWaitResponse){
String resultMessage ="";
boolean isSuccess =false;
String apnResponseId=null;
try {
PushNotificationResponse<SimpleApnsPushNotification> response = sendFuture.get();
if(response!=null && response.isAccepted()){
isSuccess=true;
apnResponseId = response.getApnsId()==null?null:response.getApnsId().toString();
resultMessage = "APN message pushed success with response Id:"+response.getApnsId();
}else if(response!=null && !response.isAccepted()){
resultMessage = "APN message was rejected with reason:"+response.getRejectionReason();
}else{
resultMessage = "APN no response";
}
}catch(Exception err){
resultMessage = "Push APN message failed with error:"+err.getMessage();
}
result.setSuccess(isSuccess);
result.setMessage(resultMessage);
LogUtil.info(ApnNotificationService.class, "Pushing Apn result:"+resultMessage);
if(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(deviceToken)){
List<SysLogNotification> notificationLogs = notificationLogDao.findAllByDeviceTokenAndTransactionId(deviceToken, transactionId);
if(!notificationLogs.isEmpty()){
LocalDateTime currentTime = LocalDateTime.now();
for(SysLogNotification logItem:notificationLogs){
logItem.setStatus(isSuccess?0:-1);
logItem.setApnResponseId(apnResponseId);
logItem.setUpdatedAt(currentTime);
logItem.setResultMessage(resultMessage);
}
notificationLogDao.saveAll(notificationLogs);
}
}
}else{
result.setSuccess(true);
result.setMessage("APN message was pushed and process with ASYNC mode, use txnId to trace log");
// process response in async mode
sendFuture.whenComplete((response, throwable) -> {
String resultMessage ="";
boolean isSuccess =false;
String apnResponseId=null;
if (response != null && response.isAccepted()) {
isSuccess =true;
apnResponseId = response.getApnsId()==null?null:response.getApnsId().toString();
resultMessage = "APN message pushed success with response Id:"+response.getApnsId();
} else if (response != null && !response.isAccepted()) {
resultMessage = "APN message was rejected with reason:"+response.getRejectionReason();
} else {
if(throwable!=null){
resultMessage = "Push APN message failed with error:"+throwable.getMessage();
}else{
resultMessage = "APN no response";
}
}
if(!isSuccess)
logger.error(resultMessage);
//-- update notification log
if(StringUtils.isNotBlank(transactionId) && StringUtils.isNotBlank(deviceToken)){
List<SysLogNotification> notificationLogs = notificationLogDao.findAllByDeviceTokenAndTransactionId(deviceToken, transactionId);
if(!notificationLogs.isEmpty()){
LocalDateTime currentTime = LocalDateTime.now();
for(SysLogNotification logItem:notificationLogs){
logItem.setStatus(isSuccess?0:-1);
logItem.setApnResponseId(apnResponseId);
logItem.setUpdatedAt(currentTime);
logItem.setResultMessage(resultMessage);
}
notificationLogDao.saveAll(notificationLogs);
}
}
});
}
}
return result;
}
}
@@ -0,0 +1,109 @@
package com.mgm.mcrm.apigateway.proxy.config;
import com.eatthepath.pushy.apns.ApnsClient;
import com.eatthepath.pushy.apns.ApnsClientBuilder;
import com.eatthepath.pushy.apns.auth.ApnsSigningKey;
import com.eatthepath.pushy.apns.proxy.HttpProxyHandlerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.io.InputStream;
import java.net.InetSocketAddress;
@Configuration
public class ApnClientConfig {
@Value("${apns.team-id}")
private String teamId;
@Value("${apns.key-id}")
private String keyId;
@Value("${apns.key-file}")
private String keyFilePath;
@Value("${apns.host}")
private String apnHost;
@Value("${apns.proxy.enabled:false}")
private boolean proxyEnabled;
@Value("${apns.proxy.host:}")
private String proxyHost;
@Value("${apns.proxy.port:0}")
private int proxyPort;
@Value("${apns.proxy.username:}")
private String proxyUsername;
@Value("${apns.proxy.password:}")
private String proxyPassword;
@Bean
public ApnsClient apnsClient() throws Exception {
String tid = teamId != null ? teamId.trim() : "";
String kid = keyId != null ? keyId.trim() : "";
if (!StringUtils.hasText(tid) || !StringUtils.hasText(kid)) {
throw new IllegalStateException("apns.team-id / apns.key-id must be non-empty after trim");
}
InputStream privateKeyStream = openApnsKeyStream(keyFilePath);
if (privateKeyStream == null) {
throw new IllegalStateException(
"Cannot load APNs .p8 from classpath (apns.key-file=" + keyFilePath + "). "
+ "Put file under src/main/resources and use path like /AuthKey_XXX.p8");
}
ApnsClientBuilder builder = new ApnsClientBuilder()
.setSigningKey(ApnsSigningKey.loadFromInputStream(privateKeyStream, tid, kid))
.setApnsServer(apnHost.trim());
if (proxyEnabled) {
String host = proxyHost != null ? proxyHost.trim() : "";
if (!StringUtils.hasText(host) || proxyPort <= 0) {
throw new IllegalStateException("apns.proxy.host / apns.proxy.port must be configured when apns.proxy.enabled=true");
}
InetSocketAddress proxyAddress = new InetSocketAddress(host, proxyPort);
if (StringUtils.hasText(proxyUsername)) {
builder.setProxyHandlerFactory(new HttpProxyHandlerFactory(proxyAddress, proxyUsername.trim(), proxyPassword != null ? proxyPassword : ""));
} else {
builder.setProxyHandlerFactory(new HttpProxyHandlerFactory(proxyAddress));
}
}
return builder.build();
}
/**
* Spring Boot / fat jar 下用上下文 ClassLoader 读 resources 比 {@code getClass().getResourceAsStream}
* <p>
* 更可靠;兼容 {@code /xxx.p8} 与 {@code xxx.p8} 两种写法。
*/
private static InputStream openApnsKeyStream(String configuredPath) {
if (!StringUtils.hasText(configuredPath)) {
return null;
}
String path = configuredPath.trim();
String withoutLeadingSlash = path.startsWith("/") ? path.substring(1) : path;
ClassLoader cl = Thread.currentThread().getContextClassLoader();
InputStream in = cl != null ? cl.getResourceAsStream(withoutLeadingSlash) : null;
if (in != null) {
return in;
}
return ApnClientConfig.class.getResourceAsStream(path.startsWith("/") ? path : "/" + path);
}
}
@@ -0,0 +1,70 @@
package com.mgm.mcrm.apigateway.proxy.controllers;
import com.mgm.mcrm.apigateway.common.constants.SysCommon;
import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse;
import com.mgm.mcrm.apigateway.common.dtos.LoginUserInfo;
import com.mgm.mcrm.apigateway.common.dtos.config.DaylightApiSetting;
import com.mgm.mcrm.apigateway.common.dtos.config.EsbApiSetting;
import com.mgm.mcrm.apigateway.common.dtos.config.OperaApiSetting;
import com.mgm.mcrm.apigateway.daylight.common.services.DaylightTokenService;
import com.mgm.mcrm.apigateway.esb.common.services.EsbTokenService;
import com.mgm.mcrm.apigateway.proxy.dtos.request.SendNotificationRequest;
import com.mgm.mcrm.apigateway.proxy.exception.HttpProxyErrorHandler;
import com.mgm.mcrm.apigateway.proxy.exception.SoapProxyErrorHandler;
import com.mgm.mcrm.apigateway.proxy.services.NotificationProxyService;
import com.mgm.mcrm.apigateway.proxy.services.WebProxyService;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.time.Duration;
import java.util.Collections;
@Tag(name = "Notification Proxy", description = "Controller for Notification proxy")
@RestController
public class NotificationProxyController {
private final Logger logger = LoggerFactory.getLogger(WebProxyController.class);
@Resource
private NotificationProxyService notificationProxyService;
@RequestMapping("/api/notification/proxy/**")
public ResponseEntity<BaseApiResponse> invokeNotificationApiProxy(HttpServletRequest request, @RequestBody SendNotificationRequest notificationRequest) throws Exception {
LoginUserInfo requestUser = SysCommon.parseUserFromToken(request.getHeader("Authorization"));
return ResponseEntity.status(HttpStatus.OK).body(notificationProxyService.sendNotificationByRequest(notificationRequest,requestUser.getLoginId()));
}
private HttpHeaders extractHeaders(HttpServletRequest request) {
HttpHeaders headers = new HttpHeaders();
Collections.list(request.getHeaderNames()).forEach(name -> {
if (!name.equalsIgnoreCase("host") &&
!name.equalsIgnoreCase("content-length") &&
!name.equalsIgnoreCase("application-id") &&
!name.equalsIgnoreCase("authorization")
) {
headers.add(name, request.getHeader(name));
}
});
return headers;
}
private byte[] getRequestBody(HttpServletRequest request) {
try {
return request.getInputStream().readAllBytes();
} catch (IOException e) {
logger.error("Failed to read request body of request[{}]",request.getRequestURI(), e);
return new byte[0];
}
}
}
@@ -0,0 +1,29 @@
package com.mgm.mcrm.apigateway.proxy.dtos.request;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
@Schema(description = "Request to send notification by employeeId")
public class SendNotificationRequest {
@Schema(description = "Transaction Id for external tracing, if blank, internal uuid will be generated")
private String transactionId;
@Schema(description = "Employee Id to receive notification")
@NotBlank(message = "Employee Id must not be empty")
private String employeeId;
@Schema(description = "Title of message")
@NotBlank(message = "Title of message must not be blank")
private String title;
@Schema(description = "Content of message")
private String content;
@Schema(description = "Booking confirmation No if need")
private String confirmationNumber;
@Schema(description = "Type of booking ")
private String type;
}
@@ -0,0 +1,132 @@
package com.mgm.mcrm.apigateway.proxy.services;
import com.mgm.mcrm.apigateway.common.dtos.BaseApiResponse;
import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken;
import com.mgm.mcrm.apigateway.logging.models.SysLogNotification;
import com.mgm.mcrm.apigateway.logging.repositories.SysLogDeviceTokenDao;
import com.mgm.mcrm.apigateway.logging.repositories.SysLogNotificationRepository;
import com.mgm.mcrm.apigateway.notification.services.ApnNotificationService;
import com.mgm.mcrm.apigateway.proxy.dtos.request.SendNotificationRequest;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
@RequiredArgsConstructor
@Service
public class NotificationProxyService {
@Resource
private SysLogDeviceTokenDao sysLogDeviceTokenDao;
@Resource
private SysLogNotificationRepository sysLogNotificationRepository;
@Resource
private ApnNotificationService apnNotificationService;
public BaseApiResponse sendNotificationByRequest(SendNotificationRequest requestData, String senderId) {
BaseApiResponse result = new BaseApiResponse();
String[] userIds = requestData.getEmployeeId().split("[;,]");
String transactionId = requestData.getTransactionId();
if(StringUtils.isBlank(transactionId)){
transactionId = UUID.randomUUID().toString();
}
// result.setData(transactionId);
if(userIds.length>0){
int successCount =0;
for(String userId:userIds){
if(StringUtils.isNotBlank(userId)){
userId = userId.trim();
SysLogDeviceToken deviceToken = sysLogDeviceTokenDao.findByUserId(userId);
String deviceTokenValue =null;
if(deviceToken!=null){
deviceTokenValue = deviceToken.getDeviceToken();
}
Integer useLanguage = deviceToken == null ? null : deviceToken.getUseLanguageType();
String notificationTitle = resolveNotificationText(requestData.getTitle(), useLanguage);
String notificationContent = resolveNotificationText(requestData.getContent(), useLanguage);
//这里需要加上判断,判断deviceToken中的UseLanguage是什么语言 0:英文 1:中文 根据不同的Language来推送不同语言的通知
SysLogNotification notificationLog = createNotificationLogByRequest(requestData,transactionId,senderId,userId,deviceTokenValue,notificationTitle,notificationContent);
if(StringUtils.isBlank(deviceTokenValue)){
notificationLog.setStatus(-1);
notificationLog.setResultMessage("No APN token is setup for user:"+userId);
notificationLog.setUpdatedAt(LocalDateTime.now());
}
sysLogNotificationRepository.save(notificationLog);
//-- Process APN sending
BaseApiResponse sendResult = apnNotificationService.pushApnMessage(
transactionId,
deviceTokenValue,
notificationTitle,
notificationContent,
true,
requestData.getConfirmationNumber());
if(sendResult.isSuccess()){
successCount++;
}
}
}
if(successCount>0){
result.setSuccess(true);
}else{
result.setSuccess(false);
result.setMessage("No message was success processed");
}
}else{
result.setSuccess(false);
result.setMessage("employeeId list is empty");
}
return result;
}
private static String resolveNotificationText(String text, Integer useLanguage) {
if (StringUtils.isBlank(text)) {
return text;
}
String preferredTag = Integer.valueOf(1).equals(useLanguage) ? "CN" : "EN";
String fallbackTag = Integer.valueOf(1).equals(useLanguage) ? "EN" : "CN";
String preferredText = extractNotificationText(text, preferredTag);
if (StringUtils.isNotBlank(preferredText)) {
return preferredText;
}
String fallbackText = extractNotificationText(text, fallbackTag);
return StringUtils.isNotBlank(fallbackText) ? fallbackText : text;
}
private static String extractNotificationText(String text, String tag) {
String marker = "#" + tag + "#";
int start = text.indexOf(marker);
if (start < 0) {
return null;
}
start += marker.length();
int end = text.indexOf(marker, start);
if (end < 0) {
return null;
}
return text.substring(start, end).trim();
}
private SysLogNotification createNotificationLogByRequest(SendNotificationRequest requestData,String transactionId, String senderId, String userId, String deviceToken, String title, String content){
SysLogNotification notificationLog = new SysLogNotification();
notificationLog.setUserId(userId);
notificationLog.setDeviceToken(deviceToken);
notificationLog.setSenderId(senderId);
notificationLog.setStatus(0);
notificationLog.setConfirmationNo(requestData.getConfirmationNumber());
notificationLog.setBookingType(requestData.getType());
notificationLog.setTitle(title);
notificationLog.setContent(content);
notificationLog.setTransactionId(transactionId);
notificationLog.setCreatedAt(LocalDateTime.now());
return notificationLog;
}
}
@@ -22,19 +22,26 @@ import com.mgm.mcrm.apigateway.basesetting.room.repositories.RoomTypeDao;
import com.mgm.mcrm.apigateway.common.dtos.ServiceResult;
import com.mgm.mcrm.apigateway.common.utils.DateTimeUtil;
import com.mgm.mcrm.apigateway.common.utils.LogUtil;
import com.mgm.mcrm.apigateway.common.utils.NotificationUtil;
import com.mgm.mcrm.apigateway.esb.common.dtos.mgmapi.PlayerProfileApiResult;
import com.mgm.mcrm.apigateway.esb.common.dtos.mgmapi.UserInfoApiRecord;
import com.mgm.mcrm.apigateway.esb.player.services.MgmUserService;
import com.mgm.mcrm.apigateway.logging.dao.SysLogDeviceTokenRepository;
import com.mgm.mcrm.apigateway.logging.models.SysLogDeviceToken;
import com.mgm.mcrm.apigateway.notification.services.ApnNotificationService;
import com.mgm.mcrm.apigateway.player.services.PlayerService;
import com.mgm.mcrm.apigateway.reservation.models.Bookings;
import com.mgm.mcrm.apigateway.reservation.repositories.BookingDao;
import jakarta.annotation.Resource;
import lombok.extern.java.Log;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Strings;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@@ -65,11 +72,20 @@ public class BookingService {
private MgmUserService mgmUserService;
@Resource
private BlockCodeDao blockCodeDao;
@Resource
private SysLogDeviceTokenRepository sysLogDeviceTokenRepository;
@Resource
private NotificationUtil notificationUtil;
@Resource
private ApnNotificationService apnNotificationService;
public void processDaylightUpdateBooking(Bookings daylightBooking) throws Exception {
if(daylightBooking!=null){
String dlReservationId=daylightBooking.getDlReservationId();
String confirmNo = daylightBooking.getConfirmationNo();
String dbBookingStatus = null;
LocalDateTime checkInDate = null;
LocalDateTime checkOutDate = null;
if(StringUtils.isNotBlank(confirmNo)
&& StringUtils.isNotBlank(dlReservationId)
){
@@ -92,6 +108,9 @@ public class BookingService {
for(Bookings booking:dbBookings){
if("N".equalsIgnoreCase(booking.getApproveStatus())) {
dbBooking = booking;
dbBookingStatus = dbBooking.getLastOperaStatus();
checkInDate = dbBooking.getCheckinDate();
checkOutDate = dbBooking.getCheckoutDate();
break;
}
}
@@ -102,6 +121,88 @@ public class BookingService {
dbBooking = this.createBookingFromDaylight(daylightBooking);
}
bookingDao.save(dbBooking);
CfgProperties property = propertyDao.findByDlPropertyId(dbBooking.getDlPropertyId());
//Send Notification to Booker when booking lastOpera status Changed
LogUtil.info(BookingService.class,"Daylight webhook updated, prepare send notification");
LogUtil.info(BookingService.class,"DaylightBooking status=" + daylightBooking.getLastOperaStatus());
LogUtil.info(BookingService.class,"Db booking status=" + dbBookingStatus);
if (Strings.CS.equals(daylightBooking.getLastOperaStatus(),"RESERVED") && !Strings.CS.equals(dbBookingStatus,daylightBooking.getLastOperaStatus())){
LogUtil.info(BookingService.class,"Booking last opera status changed, DB Record status:"+dbBookingStatus+", DlBooking status:"+daylightBooking.getLastOperaStatus());
//设置Notification 内容
String notificationTitle = "#EN# Room status updated#EN#" +
"#CN#房間狀態已更改#CN#";
String notificationContent = "#EN#Player {playerId}, {propertyName}, {roomCode},room status changed to reserved.#EN#" +
"#CN#Player {playerId}, {propertyName}, {roomCode}, 房間狀態已預訂。#CN#";
String[] placeholders = {
"{playerId}", "{propertyName}", "{roomCode}"
};
LogUtil.info(BookingService.class,"Set notification info");
String[] values = {
StringUtils.defaultString(dbBooking.getPlayerId()),
StringUtils.defaultString(property == null ? null : property.getName()),
StringUtils.defaultString(dbBooking.getRoomTypeCode())
};
LogUtil.info(BookingService.class,"Replace each");
notificationContent = StringUtils.replaceEach(notificationContent, placeholders, values);
String transactionId = "DLBKSC"+dbBooking.getId();
LogUtil.info(BookingService.class,"Get device token from DB Start");
SysLogDeviceToken deviceToken = sysLogDeviceTokenRepository.findByUserId(StringUtils.trim(dbBooking.getCreatedBy()));
LogUtil.info(BookingService.class,"Get device token from DB:" + (deviceToken == null ? "null" : deviceToken.getDeviceToken()));
String deviceTokenValue =null;
String finalTitle = null;
String finalContent = null;
if(deviceToken!=null){
deviceTokenValue = deviceToken.getDeviceToken();
finalTitle = notificationUtil.distinguishLanguage(notificationTitle,deviceToken.getUseLanguageType());
notificationContent = notificationUtil.distinguishLanguage(notificationContent,deviceToken.getUseLanguageType());
}
// NOTIFICATION
apnNotificationService.pushApnMessage(transactionId,deviceTokenValue,finalTitle,notificationContent,true);
}
//Send notification to Booker when Booking C/I | C/O date changed
if ((checkInDate != null && !checkInDate.equals(daylightBooking.getCheckinDate()))||
(checkOutDate != null && !checkOutDate.equals(daylightBooking.getCheckoutDate()))) {
LogUtil.info(BookingService.class,"Booking date changed, DB Record C/I:"+checkInDate+", DB Record C/O:"+checkOutDate+
", Dl Record C/I"+daylightBooking.getCheckinDate()+", Dl Record C/O"+daylightBooking.getCheckoutDate());
//设置Notification 内容
String notificationTitle = "#EN# Room status updated#EN#" +
"#CN#房間狀態已更改#CN#";
String notificationContent = "#EN#Player {playerId}, {propertyName}, {roomCode},Stay date changed.#EN#" +
"#CN#Player {playerId}, {propertyName}, {roomCode}, 住宿日期已變更。#CN#";
String[] placeholders = {
"{playerId}", "{propertyName}", "{roomCode}"
};
String[] values = {
StringUtils.defaultString(dbBooking.getPlayerId()),
StringUtils.defaultString(property == null ? null : property.getName()),
StringUtils.defaultString(dbBooking.getRoomTypeCode())
};
notificationContent = StringUtils.replaceEach(notificationContent, placeholders, values);
String transactionId = "DLBKDC" + dbBooking.getId();
SysLogDeviceToken deviceToken = sysLogDeviceTokenRepository.findByUserId(StringUtils.trim(dbBooking.getCreatedBy()));
LogUtil.info(BookingService.class,"Get device token from DB:" + (deviceToken == null ? "null" : deviceToken.getDeviceToken()));
String deviceTokenValue = null;
String finalTitle = null;
String finalContent = null;
if (deviceToken != null) {
LogUtil.info(BookingService.class,"Get device Token success");
deviceTokenValue = deviceToken.getDeviceToken();
finalTitle = notificationUtil.distinguishLanguage(notificationTitle, deviceToken.getUseLanguageType());
notificationContent = notificationUtil.distinguishLanguage(notificationContent, deviceToken.getUseLanguageType());
}
// NOTIFICATION
apnNotificationService.pushApnMessage(transactionId, deviceTokenValue, finalTitle, notificationContent, true);
}
LogUtil.info(BookingService.class,"End send notification function");
}
}
}
@@ -128,6 +229,14 @@ public class BookingService {
updateBooking.setDlAccountId(daylightBooking.getDlAccountId());
updateBooking.setDlBlockId(daylightBooking.getDlBlockId());
updateBooking.setDlExternalNo(daylightBooking.getDlExternalNo());
if (updateBooking.getWaitlistId() != null) {
bookingDao.findById(updateBooking.getWaitlistId()).ifPresent(booking -> {
booking.setConfirmationNo(daylightBooking.getConfirmationNo());
bookingDao.save(booking);
});
}
// updateBooking.setGroupId(daylightBooking.getGroupId());
updateBooking.setDlReservationId(daylightBooking.getDlReservationId());
// 通过 dlRoomTypeId 查找 room type(与 createBookingFromDaylight 流程保持一致)
@@ -190,7 +299,7 @@ public class BookingService {
String booker = StringUtils.trim(updateBooking.getBooker());
if(booker==null||StringUtils.isEmpty(booker)) getUser=true;
if(daylightBooking.getCreatedBy()!=null && !daylightBooking.getCreatedBy().equals(updateBooking.getCreatedBy())) {
updateBooking.setCreatedBy(daylightBooking.getCreatedBy());
// updateBooking.setCreatedBy(daylightBooking.getCreatedBy());
getUser=true;
}
if(getUser) {
@@ -0,0 +1,6 @@
-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgfQ1bQlwCYsPyrRua
2OB2VRxxlA2/3PUFWuS/+/3P1RygCgYIKoZIzj0DAQehRANCAASn2hmu3BJ02OmZ
hNS/y6SwiIJi4tWQB1lxX0EksfsRgU1e3pp9gcMeIvY8gQA44QQJSvV7JWIu2fMj
1b9xpZU/
-----END PRIVATE KEY-----
+17 -1
View File
@@ -137,4 +137,20 @@ springdoc:
api-docs:
path: /v3/api-docs
default-consumes-media-type: application/json
default-produces-media-type: application/json
default-produces-media-type: application/json
apns:
team-id: YXJ2H36G5R
key-id: 79FNU7K4J9
bundle-id: com.mgm.crmhostroombookingdaylightdevelopment
key-file: /AuthKey_79FNU7K4J9_Development.p8
proxy:
enabled: false
host:
port: 0
username:
password:
# Dev host
host: api.sandbox.push.apple.com
# Production Host
# host: api.push.apple.com