# BadGuestNotifyServiceImpl.java

    package cloud.wconnect.frontoffice.service.badguest.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.pojo.dto.ApproveLevelDTO;
    import cloud.wconnect.commonlib.pojo.dto.NotifyDTO;
    import cloud.wconnect.commonlib.pojo.dto.NotifyIdentityDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.pojo.dto.line.MoreApprovalMsgParm;
    import cloud.wconnect.commonlib.pojo.dto.line.PublicFlexMsgParm;
    import cloud.wconnect.commonlib.pojo.dto.line.SingleApprovalMsgParm;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.frontoffice.cons.FunctionPointEnum;
    import cloud.wconnect.frontoffice.cons.I18nConstant;
    import cloud.wconnect.frontoffice.cons.badguest.BlacklistApproveStatusEnum;
    import cloud.wconnect.frontoffice.cons.badguest.BlacklistNotifyConfigTypeEnum;
    import cloud.wconnect.frontoffice.cons.badguest.BlacklistReasonEnum;
    import cloud.wconnect.frontoffice.cons.badguest.BlacklistTypeEnum;
    import cloud.wconnect.frontoffice.entity.badguest.*;
    import cloud.wconnect.frontoffice.mapper.badguest.NotifiedBadGuestMapper;
    import cloud.wconnect.frontoffice.pojo.dto.badguest.GuestInHouseDTO;
    import cloud.wconnect.frontoffice.pojo.vo.badguest.BadGuestCompanionNotifyVO;
    import cloud.wconnect.frontoffice.pojo.vo.badguest.BadGuestNotifyVO;
    import cloud.wconnect.frontoffice.pojo.vo.badguest.BgApproveLevelVO;
    import cloud.wconnect.frontoffice.service.badguest.*;
    import com.alibaba.fastjson.JSONObject;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.lang.Nullable;
    import org.springframework.stereotype.Service;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

    import java.text.MessageFormat;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    import static java.util.stream.Collectors.groupingBy;

    /**
     * 黑名单通知服务类
     *
     * @author lichen
     * @date 2023/7/1
     */
    @Slf4j
    @Service
    @DS("logbook")
    public class BadGuestNotifyServiceImpl implements IBadGuestNotifyService {

        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private IBgEmailConfigService emailConfigService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private IBadGuestApproveLogService approveLogService;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        @Lazy
        private IBadGuestCompanionService badGuestCompanionService;
        @Autowired
        @Lazy
        private IBadGuestService badGuestService;
        @Autowired
        private NotifiedBadGuestMapper notifiedBadGuestMapper;
        @Autowired
        private INotifiedBadGuestService notifiedBadGuestService;
        @Value("${here.link.bad-guest}")
        private String hereLink;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private IBgEmailTargetService emailTargetService;

        /**
         * 通知给下一个审批节点的审批人
         *
         * @param propertyId    资产id
         * @param notifyUserIds 被通知的用户id集合
         * @param badGuests     黑名单集合
         * @param cc            抄送人json数组
         * @return void
         * @author lichen
         * @date 2023/9/25
         */
        @Override
        public void notifyNextApproveUsers(Integer propertyId, List<Integer> notifyUserIds, List<BadGuest> badGuests, @Nullable String cc) {
            if (CollectionUtils.isEmpty(badGuests)) return;
            if (CollectionUtils.isEmpty(notifyUserIds)) return;

            //获取资产信息
            PropertyDTO propertyDTO = propertyInternalService.getOne(propertyId);
            String language = propertyDTO.getLanguage();
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();

            //获取用户自定义的主题前缀和主题后缀
            BgEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(), BlacklistNotifyConfigTypeEnum.WAIT_FOR_APPROVING.getType());
            if (Objects.isNull(emailConfig)) {
                emailConfig = new BgEmailConfig();
                emailConfig.setStatus(true);
            }
            if (!emailConfig.getStatus()) {
                log.warn("DNR notify approver end, email config is close.");
                return;
            }
            //获取默认主题
            String defaultSubject = getDefaultSubject(propertyDTO);
            //获取通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //通知人集合
            List<NotifyIdentityDTO> identityDTOList = Lists.newArrayListWithCapacity(notifyUserIds.size());
            List<NotifyIdentityDTO> toUsers = notifyUserIds.stream().map(userId ->
                    new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER, userId, EmailConfigCons.TARGET_TYPE_TO, "")).toList();
            identityDTOList.addAll(toUsers);
            if (StringUtils.isNotBlank(cc) && !cc.equals("[]")) {
                List<NotifyIdentityDTO> identitysByCC = JsonUtils.convertStringToList(objectMapper, cc, NotifyIdentityDTO.class);
                identitysByCC.forEach(item -> item.setTargetType(EmailConfigCons.TARGET_TYPE_CC));
                identityDTOList.addAll(identitysByCC);
            }

            //获取邮件通知内容
            String emailContent = getEmailContent(BlacklistApproveStatusEnum.APPROVEING.getStatus(), propertyDTO, badGuests, defaultSubject);

            //line消息内容
            JSONObject lineMsg;
            if (badGuests.size() == 1) {
                lineMsg = getSingleApprovalLineMsg(propertyDTO, badGuests.get(0), BlacklistApproveStatusEnum.APPROVEING.getStatus());
            } else {
                String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

                LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
                List<String> guestNames = badGuests.stream()
                        .map(item -> InternationalizationUtils.formatName(item.getFirstName(), item.getLastName(), nameOrder))
                        .toList();
                businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.GUEST_NAME, language),
                        StringUtils.join(guestNames, ";"));

                lineMsg = lineMessageUtil.generateMoreApprovalMsg(MoreApprovalMsgParm.builder()
                        .propertyName(propertyDTO.getName())
                        .propertyId(propertyId)
                        .functionName(i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, language))
                        .approveStatus(BlacklistApproveStatusEnum.APPROVEING.getStatus())
                        .count(badGuests.size())
                        .link(linkUrl)
                        .businessMap(businessMap)
                        .build());
            }

            //发送通知
            doSendNotify(propertyDTO, subject, identityDTOList, FunctionPointEnum.DNR_APPROVE.getFunctionPoint(),
                    emailContent, lineMsg);
        }

        /**
         * 通知给创建人
         *
         * @param badGuest      黑名单数据
         * @param approveStatus 通知类型：3.approved 4.rejected
         * @return void
         * @author lichen
         * @date 2023/9/26
         */
        @Override
        public void notifyCreateBy(BadGuest badGuest, Integer approveStatus) {
            //查询资产信息
            Integer propertyId = badGuest.getPropertyId();
            PropertyDTO propertyDTO = propertyInternalService.getOne(propertyId);

            //通知人
            ArrayList<NotifyIdentityDTO> identitys = Lists.newArrayList(new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER,
                    badGuest.getCreatedBy(), EmailConfigCons.TARGET_TYPE_TO, ""));

            String language = propertyDTO.getLanguage();
            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, propertyDTO.getLanguage());
            //设置默认主题
            String defaultSubject = "";
            if (approveStatus == BlacklistApproveStatusEnum.APPROVED.getStatus()) {
                defaultSubject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_PASSED, language, functionName, propertyDTO.getName());
            } else {
                defaultSubject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_REJECTED, language, functionName, propertyDTO.getName());
            }

            //获取邮件通知内容
            String emailContent = getEmailContent(approveStatus, propertyDTO, Lists.newArrayList(badGuest), defaultSubject);
            //获取line通知内容
            JSONObject lineMsg = getSingleApprovalLineMsg(propertyDTO, badGuest, approveStatus);

            //发送通知
            doSendNotify(propertyDTO, defaultSubject, identitys,
                    FunctionPointEnum.DNR_APPROVED_NOTIFY_CREATE_USER.getFunctionPoint(), emailContent, lineMsg);
        }

        /**
         * 审批通过时，通知给责任人
         * 使用场景：审批层级发生变化导致审批通过时，通知给责任人
         *
         * @param badGuests 黑名单数据集合
         * @return void
         * @author lichen
         * @date 2023/11/25
         */
        @Override
        public void batchNotifyCreateByByApproved(List<BadGuest> badGuests) {
            if (CollectionUtils.isEmpty(badGuests)) {
                return;
            }

            //获取资产信息
            Integer propertyId = badGuests.get(0).getPropertyId();
            PropertyDTO propertyDTO = propertyInternalService.getOne(propertyId);
            String language = propertyDTO.getLanguage();
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();
            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, language);
            //获取默认主题
            String defaultSubject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_PASSED, language, functionName, propertyDTO.getName());
            //跳转链接
            String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

            //按照创建人分组
            Map<Integer, List<BadGuest>> createUser2BadGuests = badGuests.stream().collect(Collectors.groupingBy(BadGuest::getCreatedBy));

            for (Map.Entry<Integer, List<BadGuest>> entry : createUser2BadGuests.entrySet()) {
                Integer createUser = entry.getKey();
                if (createUser == 0) continue;

                List<BadGuest> badGuestsByCreateBy = entry.getValue();

                //获取邮件通知内容
                String emailContent = getEmailContent(BlacklistApproveStatusEnum.APPROVED.getStatus(), propertyDTO, badGuestsByCreateBy, defaultSubject);
                //获取line通知内容
                JSONObject lineMsg;
                if (badGuestsByCreateBy.size() == 1) {
                    lineMsg = getSingleApprovalLineMsg(propertyDTO, badGuestsByCreateBy.get(0), BlacklistApproveStatusEnum.APPROVED.getStatus());
                } else {
                    LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
                    List<String> guestNames = badGuestsByCreateBy.stream()
                            .map(item -> InternationalizationUtils.formatName(item.getFirstName(), item.getLastName(), nameOrder))
                            .toList();
                    businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.GUEST_NAME, language), StringUtils.join(guestNames, ";"));

                    lineMsg = lineMessageUtil.generateMoreApprovalMsg(MoreApprovalMsgParm.builder()
                            .propertyId(propertyId)
                            .propertyName(propertyDTO.getName())
                            .functionName(functionName)
                            .approveStatus(BlacklistApproveStatusEnum.APPROVED.getStatus())
                            .count(badGuestsByCreateBy.size())
                            .link(linkUrl)
                            .businessMap(businessMap)
                            .build());
                }

                //通知人
                ArrayList<NotifyIdentityDTO> identitys = Lists.newArrayList(new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER,
                        createUser, EmailConfigCons.TARGET_TYPE_TO, ""));
                //发送通知
                doSendNotify(propertyDTO, defaultSubject, identitys,
                        FunctionPointEnum.DNR_APPROVED_NOTIFY_CREATE_USER.getFunctionPoint(), emailContent, lineMsg);
            }
        }

        /**
         * 通知 黑名单客人入住
         * 注意：根据入住记录匹配 黑名单数据 并发送通知
         *
         * @param guestInHouseList 入住记录
         * @param propertyDTO      资产
         * @return void
         * @author lichen
         * @date 2023/5/13
         */
        @Override
        public void notifyBadGuestCheckIn(List<GuestInHouseDTO> guestInHouseList, PropertyDTO propertyDTO) {
            //查询邮件配置
            BgEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(), BlacklistNotifyConfigTypeEnum.BLACK_LIST_CHECKED_IN.getType());
            if (Objects.isNull(emailConfig)) return;
            if (!emailConfig.getStatus()) return;
            List<BgEmailTarget> emailTargets = emailTargetService.listByConfigId(Lists.newArrayList(emailConfig.getId()));
            if (CollectionUtils.isEmpty(emailTargets)) return;

            //查询租户下的资产集合
            Map<Integer, String> propertyId2Name = propertyInternalService.getPropertyByTenant(propertyDTO.getTenantSymbol()).
                    stream().collect(Collectors.toMap(PropertyDTO::getId, PropertyDTO::getName));
            List<Integer> propertyIdsByTenant = Lists.newArrayList(propertyId2Name.keySet());

            //根据入住记录匹配租户下的黑名单数据（原因：租户下的黑名单数据是共享的）
            List<BadGuest> matchedBadGuests = badGuestService.listByTenant(propertyDTO, propertyIdsByTenant, guestInHouseList);
            if (CollectionUtils.isEmpty(matchedBadGuests)) {
                log.info("BadGuestNotifyServiceImpl.badGuestCheckInNotify badGuestList is empty");
                return;
            }
            //查询黑名单关联的同伴集合
            List<BadGuestCompanion> badGuestCompanions = badGuestCompanionService.listByBadGuestIds(matchedBadGuests.stream().map(BadGuest::getId).toList());

            //匹配到的黑名单数据根据姓名分组
            Map<String, List<BadGuest>> guestName2MatchedBadGuest = matchedBadGuests.stream().collect(Collectors.groupingBy(item ->
                    BadGuest.generateKeyByFirstNameAndLastName(item.getFirstName(), item.getLastName())));

            //查询已经通知过的入住信息，并以姓名、入住日期分组
            Map<String, NotifiedBadGuest> groupKey2alreadyNotifyBadGuests = notifiedBadGuestMapper.select(guestInHouseList, propertyDTO.getId())
                    .stream()
                    .collect(Collectors.toMap(NotifiedBadGuest::generateGroupKey, Function.identity()));

            //未通知过的黑名单的客人发送通知后，需要新增到通知表
            List<NotifiedBadGuest> batchInsertByNotify = Lists.newArrayListWithExpectedSize(guestInHouseList.size());
            //通知的内容
            List<BadGuestCompanionNotifyVO> badGuestCompanionNotifyVOList = Lists.newArrayListWithExpectedSize(guestInHouseList.size());
            //更新最新入住日期的黑名单信息集合
            List<BadGuest> updLastDateList = Lists.newArrayListWithExpectedSize(guestInHouseList.size());
            long nowTimestamp = DateTimeUtils.nowTimestamp();//当前时间

            //循环处理报告中解析获得的黑名单
            for (GuestInHouseDTO guestInHouse : guestInHouseList) {
                //验证用户是否是黑名单，考虑姓名颠倒的情况
                String firstName = guestInHouse.getFirstName();
                String lastName = guestInHouse.getLastName();
                // 组合 firstName + lastName 从bgsByName 获取黑名单数据集合
                String firstNameAndLastNameStr = BadGuest.generateKeyByFirstNameAndLastName(firstName, lastName);
                List<BadGuest> badGuestsForFirstNameAndLastName = guestName2MatchedBadGuest.getOrDefault(firstNameAndLastNameStr,
                        new ArrayList<>());
                String lastNameAndFirstNameStr = BadGuest.generateKeyByFirstNameAndLastName(lastName, firstName);
                List<BadGuest> badGuestsForLastNameAndFirstName = guestName2MatchedBadGuest.getOrDefault(lastNameAndFirstNameStr,
                        new ArrayList<>());
                if (CollectionUtils.isEmpty(badGuestsForFirstNameAndLastName) && CollectionUtils.isEmpty(badGuestsForLastNameAndFirstName)) {
                    //入住记录未匹配到黑名单信息
                    continue;
                }

                //入住记录匹配到的黑名单数据集合
                ArrayList<BadGuest> allMatchBadGuests = new ArrayList<>();
                allMatchBadGuests.addAll(badGuestsForFirstNameAndLastName);
                allMatchBadGuests.addAll(badGuestsForLastNameAndFirstName);
                //此处根据ID去重，因为当firstName 和 lastName 一样时，会匹配出重复的黑名单数据
                List<BadGuest> matchBadGuests = allMatchBadGuests.stream()
                        .collect(Collectors.toMap(BadGuest::getId, Function.identity(), (oldValue, newValue) -> oldValue))
                        .values().stream().toList();

                //guestInHouse -> NotifiedBadGuest
                NotifiedBadGuest notifiedBadGuest = beanMapper.map(guestInHouse, NotifiedBadGuest.class, (entity -> {
                    //设置发送记录匹配的黑名单用户数据id
                    String matchedIds = matchBadGuests.stream().map(BadGuest::getId).map(String::valueOf).collect(Collectors.joining(","));
                    entity.setMatchedIds(matchedIds);
                    //设置创建人+创建时间
                    entity.setCreatedBy(BaseConstant.ZERO);
                    entity.setCreatedTime(nowTimestamp);
                }));
                String notifiedGroupKey = notifiedBadGuest.generateGroupKey();
                //判断是否已经通知过
                if (Objects.nonNull(groupKey2alreadyNotifyBadGuests.get(notifiedGroupKey))) {
                    continue;
                }
                //对象存入需要持久化入库的addNbgs中
                batchInsertByNotify.add(notifiedBadGuest);

                //获取要更新黑名单表LastCheckInDate LastCheckOutDate的集合
                List<BadGuest> updLastDateBadGuestList = matchBadGuests.stream().filter(matchBadGuest -> {
                            //数据库中记录的checkInDate checkOutDate
                            Long checkInDate = matchBadGuest.getLastCheckInDate();
                            Long checkOutDate = matchBadGuest.getLastCheckOutDate();
                            //报告推送过来的checkInDate checkOutDate
                            long reportCheckInDate = guestInHouse.getCheckInDate();
                            long reportCheckOutDate = guestInHouse.getCheckOutDate();
                            return checkInDate != reportCheckInDate || checkOutDate != reportCheckOutDate;
                        })
                        .filter(item -> item.getType() == BlacklistTypeEnum.GUEST_ROOMS.getType())
                        .peek(item -> {
                            //更新bad_guest中的LastCheckInDate、LastCheckOutDate
                            item.setLastCheckInDate(guestInHouse.getCheckInDate());
                            item.setLastCheckOutDate(guestInHouse.getCheckOutDate());
                        }).toList();
                updLastDateList.addAll(updLastDateBadGuestList);

                //matchBadGuestDTO -> List<BadGuestEmailVO>
                List<Long> matchBadGuestIds = matchBadGuests.stream().map(BadGuest::getId).toList();
                List<BadGuestCompanion> companionsByMatchBadGuestIds = badGuestCompanions.stream().filter(item -> matchBadGuestIds.contains(item.getBadGuestId())).toList();
                badGuestCompanionNotifyVOList.addAll(convertToEmailVO(matchBadGuests, companionsByMatchBadGuestIds, propertyDTO, propertyId2Name));
            }

            if (batchInsertByNotify.isEmpty()) {
                //没有未通知过的黑名单入住时，直接return
                return;
            }
            //有黑名单客人入住时更新客人的最后一次入住日期
            if (CollectionUtils.isNotEmpty(updLastDateList)) badGuestService.saveOrUpdateBatch(updLastDateList);
            //发送通知
            doNotifyBadGuestCheckIn(propertyDTO, badGuestCompanionNotifyVOList, emailConfig,
                    beanMapper.mapList(emailTargets, NotifyIdentityDTO.class));
            //发送通知后，新增通知记录
            notifiedBadGuestService.saveBatch(batchInsertByNotify);
        }

        /**
         * 发送黑名单入住通知
         *
         * @param propertyDTO                   资产DTO
         * @param badGuestCompanionNotifyVOList 黑名单客户信息
         * @param emailConfig                   邮件配置对象
         * @param identityDTOList               通知对象集合
         * @return void
         * @author lichen
         * @date 2024/3/14
         */
        private void doNotifyBadGuestCheckIn(PropertyDTO propertyDTO, List<BadGuestCompanionNotifyVO> badGuestCompanionNotifyVOList, BgEmailConfig emailConfig, List<NotifyIdentityDTO> identityDTOList) {
            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, propertyDTO.getLanguage());
            //默认主题的国际化code
            String defaultSubjectI18nCode = BlacklistNotifyConfigTypeEnum.getDefaultSubjectI18nCode(emailConfig.getType());
            String defaultSubject = i18nMessageUtils.message(defaultSubjectI18nCode, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
            //自定义主体优先，其次默认主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            List<BadGuestCompanionNotifyVO> VOList = Lists.newArrayListWithCapacity(badGuestCompanionNotifyVOList.size());

            //按照黑名单Id主键分组, 再按照是否主要客户排序
            Map<Long, List<BadGuestCompanionNotifyVO>> badGuestId2VOList = badGuestCompanionNotifyVOList.stream().collect(groupingBy(BadGuestCompanionNotifyVO::getBadGuestId));
            for (Map.Entry<Long, List<BadGuestCompanionNotifyVO>> entry : badGuestId2VOList.entrySet()) {
                List<BadGuestCompanionNotifyVO> voListBySort = entry.getValue().stream().sorted(Comparator.comparing(BadGuestCompanionNotifyVO::getIsPrimary).reversed()).toList();
                VOList.addAll(voListBySort);
            }

            Integer propertyId = propertyDTO.getId();
            String language = propertyDTO.getLanguage();

            //跳转链接
            String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

            Context ct = new Context();
            ct.setVariable("propertyName", propertyDTO.getName());
            ct.setVariable("functionName", functionName);
            ct.setVariable("badGuestCompanionNotifyVOS", VOList);
            ct.setVariable("hereLink", linkUrl);
            String mailBody = templateEngine.process("badguest/dnr-checkedIn_" + language, ct);
            String fullEmailContent = EmailUtil.getFullEmailContent(defaultSubject, mailBody, language);

            //生成line通知内容
            Set<String> badGuestNames = VOList.stream()
                    .filter(BadGuestCompanionNotifyVO::getIsPrimary).map(BadGuestCompanionNotifyVO::getGuestName).collect(Collectors.toSet());
            LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
            businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.GUEST_NAME, language),
                    StringUtils.join(badGuestNames, ";"));

            PublicFlexMsgParm publicFlexMsgParm = PublicFlexMsgParm.builder()
                    .propertyName(propertyDTO.getName())
                    .propertyId(propertyId)
                    .functionName(functionName)
                    .title("Guests checked-in")
                    .link(linkUrl)
                    .businessMap(businessMap)
                    .build();
            JSONObject textMsgJsonObject = lineMessageUtil.generatePublicFlexMsg(publicFlexMsgParm);

            //发送通知
            doSendNotify(propertyDTO, subject, identityDTOList,
                    FunctionPointEnum.DNR_CHECK_IN.getFunctionPoint(), fullEmailContent, textMsgJsonObject);
        }

        /**
         * 获取邮件通知内容
         *
         * @param approveStatus  审批状态
         * @param propertyDTO    资产
         * @param badGuests      黑名单集合
         * @param defaultSubject 默认主题
         * @return java.lang.String
         * @author lichen
         * @date 2023/11/25
         */
        private String getEmailContent(Integer approveStatus, PropertyDTO propertyDTO, List<BadGuest> badGuests, String defaultSubject) {
            List<BadGuestCompanionNotifyVO> badGuestCompanionNotifyVOS = convertToEmailVO(badGuests, propertyDTO);

            List<BadGuestCompanionNotifyVO> VOList = Lists.newArrayListWithCapacity(badGuestCompanionNotifyVOS.size());

            //按照黑名单Id主键分组, 再按照是否主要客户排序
            Map<Long, List<BadGuestCompanionNotifyVO>> badGuestId2VOList = badGuestCompanionNotifyVOS.stream().collect(groupingBy(BadGuestCompanionNotifyVO::getBadGuestId));
            for (Map.Entry<Long, List<BadGuestCompanionNotifyVO>> entry : badGuestId2VOList.entrySet()) {
                List<BadGuestCompanionNotifyVO> voListBySort = entry.getValue().stream().sorted(Comparator.comparing(BadGuestCompanionNotifyVO::getIsPrimary).reversed()).toList();
                VOList.addAll(voListBySort);
            }
            //链接
            String linkUrl = "";
            if (badGuests.size() == 1) {
                BadGuest badGuest = badGuests.get(0);
                linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}&firstName={2}&lastName={3}&approveStatusList={4}&type={5}",
                        propertyDTO.getAbbr(), propertyDTO.getTenantSymbol(), badGuest.getFirstName(), badGuest.getLastName(), approveStatus, badGuest.getType());
            } else {
                linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());
            }
            Context context = new Context();
            context.setVariable("approveStatus", approveStatus);
            context.setVariable("badGuestCompanionNotifyVOS", VOList);
            context.setVariable("hereLink", linkUrl);
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("functionName", i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, propertyDTO.getLanguage()));
            context.setVariable("count", badGuests.size());
            String mailBody = templateEngine.process("badguest/dnr-approval_" + propertyDTO.getLanguage(), context);
            return EmailUtil.getFullEmailContent(defaultSubject, mailBody, propertyDTO.getLanguage());
        }

        /**
         * 获取单条数据的line消息内容
         *
         * @param propertyDTO   资产DTO
         * @param badGuest      黑名单数据
         * @param approveStatus 审批状态
         * @return com.alibaba.fastjson.JSONObject
         * @author lichen
         * @date 2023/11/25
         */
        private JSONObject getSingleApprovalLineMsg(PropertyDTO propertyDTO, BadGuest badGuest, Integer approveStatus) {
            String language = propertyDTO.getLanguage();
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();
            String timezone = propertyDTO.getTimezone();

            //审批日志树
            List<BgApproveLevelVO> levelVOList = approveLogService.listApproveRecord(badGuest, propertyDTO);
            List<ApproveLevelDTO> approveLevelDTOS = beanMapper.mapList(levelVOList, ApproveLevelDTO.class);

            LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
            businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.GUEST_NAME, language),
                    InternationalizationUtils.formatName(badGuest.getFirstName(), badGuest.getLastName(), nameOrder));
            //拼装checkInDate ~ checkOutDate
            long checkInDate = badGuest.getCheckInDate();
            if (checkInDate > 0) {
                businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.CHECK_IN_DATE, language),
                        InternationalizationUtils.formatDateTime(checkInDate, longDate, hourClock, timezone, language));
            }
            long checkOutDate = badGuest.getCheckOutDate();
            if (checkOutDate > 0) {
                businessMap.put(i18nMessageUtils.message(CommonFieldI18nConstant.CHECK_OUT_DATE, language),
                        InternationalizationUtils.formatDateTime(checkInDate, longDate, hourClock, timezone, language));
            }

            //链接
            String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

            //获取line通知内容
            return lineMessageUtil.generateSingleApprovalMsg(SingleApprovalMsgParm.builder()
                    .propertyName(propertyDTO.getName())
                    .propertyId(propertyDTO.getId())
                    .approveStatus(approveStatus)
                    .functionName(i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, language))
                    .approveLevels(approveLevelDTOS)
                    .businessMap(businessMap)
                    .link(linkUrl)
                    .build());
        }

        /**
         * 发送通知
         *
         * @param propertyDTO   资产dto
         * @param subject       邮件配置主题
         * @param identitys     通知对象集合
         * @param functionPoint 功能点
         * @param emailContent  邮件内容
         * @param lineMsg       line消息
         * @return void
         * @author lichen
         * @date 2023/11/25
         */
        private void doSendNotify(PropertyDTO propertyDTO, String subject, List<NotifyIdentityDTO> identitys,
                                  int functionPoint, String emailContent, JSONObject lineMsg) {
            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyDTO.getId());
            notifyDTO.setFunctionPoint(functionPoint);
            notifyDTO.setSubject(subject);
            notifyDTO.setIdentityList(identitys);
            notifyDTO.setEmailContent(emailContent);
            notifyDTO.setLineContent(Lists.newArrayList(lineMsg));
            notifyInternalService.sendNotify(notifyDTO);
        }

        /**
         * 把黑名单集合转化为VO
         *
         * @param badGuests   黑名单集合
         * @param propertyDTO 资产
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.vo.badguest.BadGuestEmailVO>
         * @author lichen
         * @date 2023/11/25
         */
        private List<BadGuestCompanionNotifyVO> convertToEmailVO(List<BadGuest> badGuests, PropertyDTO propertyDTO) {
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();
            InternationalizationDateTimeDTO internationalizationDateTimeDTO = propertyDTO.getInternationalization().getDateTime();
            String longDateFormat = internationalizationDateTimeDTO.getLongDate();
            String hourClock = internationalizationDateTimeDTO.getHourClock();
            String timezone = propertyDTO.getTimezone();
            String language = propertyDTO.getLanguage();

            String i18nYes = i18nMessageUtils.message(I18nConstant.YES, propertyDTO.getLanguage());
            String i18nNo = i18nMessageUtils.message(I18nConstant.NO, propertyDTO.getLanguage());

            //查询用户信息
            List<Integer> createByList = badGuests.stream().map(BadGuest::getCreatedBy).toList();
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), createByList);

            ArrayList<BadGuestNotifyVO> badGuestNotifyVOS = Lists.newArrayListWithCapacity(badGuests.size());

            for (BadGuest badGuest : badGuests) {
                //badGuest -> badGuestNotifyVO
                BadGuestNotifyVO badGuestNotifyVO = beanMapper.map(badGuest, BadGuestNotifyVO.class);
                //全名
                badGuestNotifyVO.setGuestName(InternationalizationUtils.formatName(badGuest.getFirstName(), badGuest.getLastName(), nameOrder));
                //拼装checkInDate ~ checkOutDate
                long checkInDate = badGuest.getCheckInDate();
                long checkOutDate = badGuest.getCheckOutDate();
                String checkInDateStr = InternationalizationUtils.formatDateTime(checkInDate, longDateFormat, StringUtils.EMPTY, timezone, language);
                String checkOutDateStr = InternationalizationUtils.formatDateTime(checkOutDate, longDateFormat, StringUtils.EMPTY, timezone, language);
                badGuestNotifyVO.setIncidentDates(checkInDateStr + " ~ " + checkOutDateStr);
                //拼装lastStayDates
                long lastCheckInDate = badGuest.getLastCheckInDate();
                long lastCheckOutDate = badGuest.getLastCheckOutDate();
                boolean sameAsIncidentDates = (lastCheckInDate == checkInDate) && (lastCheckOutDate == checkOutDate);
                if (lastCheckInDate == 0 && lastCheckOutDate == 0) {
                    badGuestNotifyVO.setLastStayDates("--");
                } else if (sameAsIncidentDates) {
                    badGuestNotifyVO.setLastStayDates("--");
                } else {
                    String lastCheckInDateStr = lastCheckInDate > 0
                            ? InternationalizationUtils.formatDateTime(lastCheckInDate, longDateFormat, StringUtils.EMPTY, timezone, language) : "";
                    String lastCheckOutDateStr = lastCheckOutDate > 0
                            ? InternationalizationUtils.formatDateTime(lastCheckOutDate, longDateFormat, StringUtils.EMPTY, timezone, language) : "";
                    badGuestNotifyVO.setLastStayDates(lastCheckInDateStr + " ~ " + lastCheckOutDateStr);
                }
                //拼装createdBy
                Integer createdBy = badGuest.getCreatedBy();
                badGuestNotifyVO.setCreatedByName(userId2Name.getOrDefault(createdBy, ""));
                //拼装createdDateStr
                badGuestNotifyVO.setCreatedDateStr(InternationalizationUtils.formatDateTime(badGuest.getCreatedTime(),
                        longDateFormat, hourClock, timezone, language));
                badGuestNotifyVO.setApproveStatusDesc(i18nMessageUtils.message(BlacklistApproveStatusEnum.getI18nCode(badGuest.getApproveStatus()), language));

                //黑名单类型
                String typeDesc = i18nMessageUtils.message(BlacklistTypeEnum.getI18nCodeByType(badGuest.getType()), propertyDTO.getLanguage());
                badGuestNotifyVO.setTypeDesc(typeDesc);
                badGuestNotifyVOS.add(badGuestNotifyVO);

                String whatHappend = badGuest.getWhatHappened().replace("\n", "<br/>");
                badGuestNotifyVO.setWhatHappened(whatHappend);

                String reportedTo = badGuest.getReportedTo().replace("\n", "<br/>");
                badGuestNotifyVO.setReportedTo(reportedTo);

                String results = badGuest.getResults().replace("\n", "<br/>");
                badGuestNotifyVO.setResults(results);
            }

            Map<Long, BadGuestNotifyVO> id2BadGuest = badGuestNotifyVOS.stream().collect(Collectors.toMap(BadGuestNotifyVO::getId, Function.identity()));

            //查询黑名单同伴
            List<Long> badGuestIds = badGuests.stream().map(BadGuest::getId).toList();
            List<BadGuestCompanion> badGuestCompanionList = badGuestCompanionService.listByBadGuestIds(badGuestIds);

            //黑名单Id对应的同伴数
            Map<Long, Long> badGuestId2CompanionSize = badGuestCompanionList.stream().collect(groupingBy(BadGuestCompanion::getBadGuestId, Collectors.counting()));

            //黑名单同伴信息
            return beanMapper.mapList(badGuestCompanionList, BadGuestCompanionNotifyVO.class, (entity, notifyVO) -> {
                notifyVO.setGuestName(InternationalizationUtils.formatName(entity.getFirstName(), entity.getLastName(), nameOrder));
                notifyVO.setIsPrimaryDesc(entity.getIsPrimary() ? i18nYes : i18nNo);

                //手机号
                notifyVO.setPhoneList(JsonUtils.convertStringToList(objectMapper, entity.getPhone(), String.class));
                //地址
                notifyVO.setAddressList(JsonUtils.convertStringToList(objectMapper, entity.getAddress(), String.class));
                //会员卡号
                notifyVO.setMemberCardNumList(JsonUtils.convertStringToList(objectMapper, entity.getMemberCardNum(), String.class));
                //邮箱
                notifyVO.setEmailList(JsonUtils.convertStringToList(objectMapper, entity.getEmail(), String.class));
                //设置黑名单信息
                if (notifyVO.getIsPrimary()) {
                    notifyVO.setBadGuest(id2BadGuest.get(entity.getBadGuestId()));
                }
                //设置黑名单同伴数
                notifyVO.setSize(badGuestId2CompanionSize.get(entity.getBadGuestId()));
            });
        }

        /**
         * 把黑名单数据转化为VO
         *
         * @param badGuests          黑名单数据
         * @param badGuestCompanions 同伴数据集合
         * @param propertyDTO        资产DTO
         * @param propertyId2Name    资产名称集合
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.vo.badguest.BadGuestCompanionNotifyVO>
         * @author lichen
         * @date 2024/4/22
         */
        private List<BadGuestCompanionNotifyVO> convertToEmailVO(List<BadGuest> badGuests, List<BadGuestCompanion> badGuestCompanions, PropertyDTO propertyDTO,
                                                                 Map<Integer, String> propertyId2Name) {
            String language = propertyDTO.getLanguage();
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();
            String timezone = propertyDTO.getTimezone();//获取资产时区

            String i18nYes = i18nMessageUtils.message(I18nConstant.YES, propertyDTO.getLanguage());
            String i18nNo = i18nMessageUtils.message(I18nConstant.NO, propertyDTO.getLanguage());

            //黑名单数据
            List<BadGuestNotifyVO> badGuestNotifyVOS = beanMapper.mapList(badGuests, BadGuestNotifyVO.class, (badGuest, badGuestVO) -> {
                //badGuest -> BadGuestEmailVO
                badGuestVO.setGuestName(InternationalizationUtils.formatName(badGuest.getFirstName(), badGuest.getLastName(), nameOrder));
                badGuestVO.setPropertyName(propertyId2Name.get(badGuest.getPropertyId()));

                //设置checkInDate checkOutDate
                long checkInDate = badGuest.getCheckInDate();
                long checkOutDate = badGuest.getCheckOutDate();
                if (checkInDate == BaseConstant.ZERO && checkOutDate == BaseConstant.ZERO) {
                    badGuestVO.setIncidentDates("");
                } else {
                    String checkInDateStr = checkInDate > BaseConstant.ZERO
                            ? InternationalizationUtils.formatDateTime(checkInDate, longDate, hourClock, timezone, language)
                            : "";
                    String checkOutDateStr = checkOutDate > BaseConstant.ZERO
                            ? InternationalizationUtils.formatDateTime(checkOutDate, longDate, hourClock, timezone, language)
                            : "";
                    badGuestVO.setIncidentDates(checkInDateStr + " ~ " + checkOutDateStr);
                }

                //设置lastCheckInDate lastCheckOutDate
                long lastCheckInDate = badGuest.getLastCheckInDate();
                long lastCheckOutDate = badGuest.getLastCheckOutDate();
                boolean sameAsIncidentDates = (lastCheckInDate == checkInDate) && (lastCheckOutDate == checkOutDate);
                if (lastCheckInDate == BaseConstant.ZERO && lastCheckOutDate == BaseConstant.ZERO) {
                    badGuestVO.setLastStayDates("");
                } else if (sameAsIncidentDates) {
                    badGuestVO.setLastStayDates("");
                } else {
                    String lastCheckInDateStr = lastCheckInDate > BaseConstant.ZERO
                            ? InternationalizationUtils.formatDateTime(lastCheckInDate, longDate, hourClock, timezone, language)
                            : "";
                    String lastCheckOutDateStr = lastCheckOutDate > BaseConstant.ZERO
                            ? InternationalizationUtils.formatDateTime(lastCheckOutDate, longDate, hourClock, timezone, language)
                            : "";
                    badGuestVO.setLastStayDates(lastCheckInDateStr + " ~ " + lastCheckOutDateStr);
                }
                //黑名单类型
                String typeDesc = i18nMessageUtils.message(BlacklistTypeEnum.getI18nCodeByType(badGuest.getType()), propertyDTO.getLanguage());
                badGuestVO.setTypeDesc(typeDesc);

                String whatHappend = badGuest.getWhatHappened().replace("\n", "<br/>");
                badGuestVO.setWhatHappened(whatHappend);

                String reportedTo = badGuest.getReportedTo().replace("\n", "<br/>");
                badGuestVO.setReportedTo(reportedTo);

                String results = badGuest.getResults().replace("\n", "<br/>");
                badGuestVO.setResults(results);

                //黑名单理由
                if(StringUtils.isNotBlank(badGuest.getCaseReason())){
                    String caseReasonDesc = i18nMessageUtils.message(BlacklistReasonEnum.getI18nCodeByType(badGuest.getCaseReason()), propertyDTO.getLanguage());
                    badGuestVO.setCaseReasonDesc(caseReasonDesc);
                }
            });
            Map<Long, BadGuestNotifyVO> id2BadGuestNotifyVO = badGuestNotifyVOS.stream().collect(Collectors.toMap(BadGuestNotifyVO::getId, Function.identity()));

            Map<Long, Long> badGuestId2Size = badGuestCompanions.stream().collect(groupingBy(BadGuestCompanion::getBadGuestId, Collectors.counting()));

            return beanMapper.mapList(badGuestCompanions, BadGuestCompanionNotifyVO.class, (entity, vo) -> {
                vo.setGuestName(InternationalizationUtils.formatName(entity.getFirstName(), entity.getLastName(), nameOrder));
                vo.setIsPrimaryDesc(entity.getIsPrimary() ? i18nYes : i18nNo);

                //手机号
                vo.setPhoneList(JsonUtils.convertStringToList(objectMapper, entity.getPhone(), String.class));
                //地址
                vo.setAddressList(JsonUtils.convertStringToList(objectMapper, entity.getAddress(), String.class));
                //会员卡号
                vo.setMemberCardNumList(JsonUtils.convertStringToList(objectMapper, entity.getMemberCardNum(), String.class));
                //邮箱
                vo.setEmailList(JsonUtils.convertStringToList(objectMapper, entity.getEmail(), String.class));
                if (vo.getIsPrimary()) {
                    //设置黑名单数据
                    vo.setBadGuest(id2BadGuestNotifyVO.get(entity.getBadGuestId()));
                }
                //设置同伴的总数
                vo.setSize(badGuestId2Size.get(entity.getBadGuestId()));
            });
        }

        /**
         * 获取默认的主题
         *
         * @param propertyDTO 资产DTO
         * @return java.lang.String
         * @author lichen
         * @date 2025/1/16
         */
        private String getDefaultSubject(PropertyDTO propertyDTO) {
            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.GUEST_BLACK_LIST, propertyDTO.getLanguage());
            //默认主题的国际化code
            String defaultSubjectI18nCode = BlacklistNotifyConfigTypeEnum.getDefaultSubjectI18nCode(BlacklistNotifyConfigTypeEnum.WAIT_FOR_APPROVING.getType());
            //默认的主题
            return i18nMessageUtils.message(defaultSubjectI18nCode, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
        }
    }