# AdjSendNotifyServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.EmailConfigCons;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    import cloud.wconnect.commonlib.cons.NotifySubjectI18nCons;
    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.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.pojo.dto.line.PublicFlexMsgParm;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.NotifyInternalService;
    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.adjustment.*;
    import cloud.wconnect.frontoffice.entity.adjustment.*;
    import cloud.wconnect.frontoffice.pojo.dto.adjustment.AdjEmailDTO;
    import cloud.wconnect.frontoffice.pojo.vo.adjustment.ApprovalLevelVO;
    import cloud.wconnect.frontoffice.service.adjustment.*;
    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 com.google.common.collect.Maps;
    import com.google.common.collect.Sets;
    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.stereotype.Service;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

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

    /**
     * @author lichen
     * @description Adjustment Requests 发送通知实现类
     * @date 2023/3/30 11:56
     */
    @Service
    @Slf4j
    @DS("logbook")
    public class AdjSendNotifyServiceImpl implements IAdjSendNotifyService {

        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        private IAdjApproveConfigService approveConfigService;
        @Autowired
        private IAdjApproveLevelService approveLevelService;
        @Autowired
        private IAdjApproveLogService approveLogService;
        @Value("${here.link.adjustment-requests}")
        private String hereLink;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private IAdjEmailConfigService emailConfigService;
        @Autowired
        private IAdjEmailTargetService emailTargetService;
        @Autowired
        private IAdjOutOfOfficeConfigService adjOutOfOfficeConfigService;
        @Autowired
        private IAdjCheckUserEffectiveService adjCheckUserEffectiveService;
        @Autowired
        private IAdjEmailTargetService adjEmailTargetService;
        @Autowired
        private IAdjApproveLevelService adjApproveLevelService;

        /**
         * 通知审批人
         * 使用场景：一条数据走审批流程过程中，通知审批人进行审批
         *
         * @param adRequest adjustment数据
         * @author lichen
         * @date 2023/11/24
         */
        @Override
        public void notifyApprover(AdRequest adRequest) {
            //查询资产信息
            Integer propertyId = adRequest.getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            //获取邮件配置
            AdjEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(),
                    AdjNotifyConfigTypeEnum.getTypeByRequestType(adRequest.getRequestType()));
            if (Objects.isNull(emailConfig)) {
                emailConfig = new AdjEmailConfig();
                emailConfig.setStatus(true);
            }
            if (!emailConfig.getStatus()) {
                log.warn("Adjustment notify approver end, email config is close.");
                return;
            }

            //默认主题
            String defaultSubject = getDefaultSubject(propertyDTO, adRequest.getRequestType());

            //获取通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //获取调整数据的通知对象
            List<NotifyIdentityDTO> identityList = getNotifyIdentity(adRequest);
            if (CollectionUtils.isEmpty(identityList)) {
                return;
            }
            //获取邮件通知内容
            String emailContent = getEmailContent(AdjApprovalStatusEnum.APPROVING.getStatus(), propertyDTO, Lists.newArrayList(adRequest), defaultSubject);
            //获取line通知内容
            JSONObject lineMsg = generateLineMsg(propertyDTO, adRequest, AdjApprovalStatusEnum.APPROVING.getStatus());
            //发送通知
            doSendNotify(propertyDTO, subject, identityList, FunctionPointEnum.ADJ_APPROVE.getFunctionPoint(),
                    emailContent, lineMsg);
        }

        /**
         * 通知下一个审批节点的审批人
         * 使用场景：审批层级发生变化时，推送至下一个审批节点的审批人
         *
         * @param adjList          adjustment数据集合
         * @param nextApproveLevel 下一个审批层级对象
         * @author lichen
         * @date 2023/11/24
         */
        @Override
        public void notifyNextApprovalNodeApprover(List<AdRequest> adjList, AdjApproveLevel nextApproveLevel) {
            if (CollectionUtils.isEmpty(adjList)) {
                return;
            }
            //获取资产信息
            Integer propertyId = adjList.get(0).getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            //获取邮件配置
            AdjEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(),
                    AdjNotifyConfigTypeEnum.getTypeByRequestType(nextApproveLevel.getType()));
            if (Objects.isNull(emailConfig)) {
                emailConfig = new AdjEmailConfig();
                emailConfig.setStatus(true);
            }
            if (!emailConfig.getStatus()) {
                log.warn("Adjustment notify approver end, email config is close.");
                return;
            }

            //默认主题
            String defaultSubject = getDefaultSubject(propertyDTO, nextApproveLevel.getType());

            //获取通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //下一个审批节点的审批人集合
            List<Integer> nextApproveConfigs = approveConfigService.listApproverByLevelId(nextApproveLevel.getPropertyId(), nextApproveLevel.getId());

            // 汇总通知用户
            // 1.排除掉无效审批人员（禁用或无审批权限用户），2.若审批人设置了补位审批人，则将补位审批人替换成审批人
            List<Integer> effectiveApprovers = adjCheckUserEffectiveService.checkUserEffective(propertyId, nextApproveConfigs);
            if(CollectionUtils.isEmpty(effectiveApprovers)) return;
            // 补位人员
            Map<Integer, Integer> substitutePersonnelIdByApprover = adjOutOfOfficeConfigService.listEffectiveConfigForApprover(propertyId, nextApproveLevel.getType(), effectiveApprovers)
                    .stream().collect(Collectors.toMap(AdjOutOfOfficeConfig::getUpdatedBy, AdjOutOfOfficeConfig::getSubstitutePersonnelId));
            // 定义通知人
            List<Integer> notifyApprover = Lists.newArrayListWithExpectedSize(effectiveApprovers.size());
            // 补位人员替换
            effectiveApprovers.forEach(approver -> notifyApprover.add(substitutePersonnelIdByApprover.getOrDefault(approver, approver)));

            //审批人集合
            List<NotifyIdentityDTO> identitys = Lists.newArrayListWithCapacity(notifyApprover.size());
            identitys.addAll(notifyApprover.stream().map(userId -> new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER, userId,
                    EmailConfigCons.TARGET_TYPE_TO, "")).toList());
            //抄送对象集合
            if (StringUtils.isNotBlank(nextApproveLevel.getCc()) && !nextApproveLevel.getCc().equals("[]")) {
                List<NotifyIdentityDTO> identitysByCC = JsonUtils.convertStringToList(objectMapper, nextApproveLevel.getCc(), NotifyIdentityDTO.class);
                identitysByCC.forEach(cc -> cc.setTargetType(EmailConfigCons.TARGET_TYPE_CC));
                identitys.addAll(identitysByCC);
            }

            //获取邮件通知内容
            String emailContent = getEmailContent(AdjApprovalStatusEnum.APPROVING.getStatus(), propertyDTO, adjList, defaultSubject);

            //line消息内容
            JSONObject lineMsg;
            if (adjList.size() == 1) {
                lineMsg = generateLineMsg(propertyDTO, adjList.get(0), AdjApprovalStatusEnum.APPROVING.getStatus());
            } else {
                lineMsg = generateMultipleDataLineMsg(propertyDTO, adjList, AdjApprovalStatusEnum.APPROVING.getStatus());
            }

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

        /**
         * 审批通过时，通知给责任人
         * 使用场景：审批层级发生变化导致审批通过时，通知给责任人
         *
         * @param adjList adjustment数据集合
         * @author lichen
         * @date 2023/11/24
         */
        @Override
        public void batchNotifyDutyUserByApproved(List<AdRequest> adjList) {
            if (CollectionUtils.isEmpty(adjList)) {
                return;
            }
            //获取资产信息
            Integer propertyId = adjList.get(0).getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            String language = propertyDTO.getLanguage();

            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, language);

            //审批通过的默认主题
            String subject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_PASSED, propertyDTO.getLanguage(), functionName, propertyDTO.getName());

            //按照创建人分组
            Map<Integer, List<AdRequest>> dutyUser2AdjList = adjList.stream().collect(Collectors.groupingBy(AdRequest::getDutyUser));

            for (Map.Entry<Integer, List<AdRequest>> entry : dutyUser2AdjList.entrySet()) {
                Integer dutyUserId = entry.getKey();
                List<AdRequest> adjListByDutyUser = entry.getValue();

                //获取邮件通知内容
                String emailContent = getEmailContent(AdjApprovalStatusEnum.APPROVED.getStatus(), propertyDTO, adjListByDutyUser, subject);
                //获取line通知内容
                JSONObject lineMsg;
                if (adjListByDutyUser.size() == 1) {
                    lineMsg = generateLineMsg(propertyDTO, adjListByDutyUser.get(0), AdjApprovalStatusEnum.APPROVED.getStatus());
                } else {
                    lineMsg = generateMultipleDataLineMsg(propertyDTO, adjListByDutyUser, AdjApprovalStatusEnum.APPROVED.getStatus());
                }
                //通知人
                ArrayList<NotifyIdentityDTO> identitys = Lists.newArrayList(new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER,
                        dutyUserId, EmailConfigCons.TARGET_TYPE_TO, ""));
                //发送通知
                doSendNotify(propertyDTO, subject, identitys,
                        FunctionPointEnum.ADJ_APPROVED_NOTIFY_DUTY_USER.getFunctionPoint(), emailContent, lineMsg);
            }

            //审批通过时，通知配置人
            notifyConfigUserByApproved(propertyDTO, adjList);
        }

        /**
         * 审批通过/审批拒绝/退回 时，通知给责任人
         * 使用场景：一条数据走审批完结时，通知给责任人
         *
         * @param adRequest     adjustment数据
         * @param approveStatus 审批状态
         * @author lichen
         * @date 2023/11/24
         */
        @Override
        public void notifyDutyUser(AdRequest adRequest, Integer approveStatus) {
            //查询资产信息
            Integer propertyId = adRequest.getPropertyId();
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, propertyDTO.getLanguage());

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

            //设置主题
            String subject = "";
            if (approveStatus == AdjApprovalStatusEnum.APPROVED.getStatus()) {
                subject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_PASSED, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
            } else if (approveStatus == AdjApprovalStatusEnum.REJECTED.getStatus()) {
                subject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_REJECTED, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
            } else {
                subject = i18nMessageUtils.message(NotifySubjectI18nCons.APPROVAL_RETURNED, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
            }

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

            //给责任人发送通知
            doSendNotify(propertyDTO, subject, identitys,
                    FunctionPointEnum.ADJ_APPROVED_NOTIFY_DUTY_USER.getFunctionPoint(), emailContent, lineMsg);

            if (approveStatus == AdjApprovalStatusEnum.APPROVED.getStatus()) {
                notifyConfigUserByApproved(propertyDTO, Collections.singletonList(adRequest));
            }
        }

        /**
         * 生成单条数据的lineMsg
         *
         * @param propertyDTO   资产DTO
         * @param adRequest     adjustment数据
         * @param approveStatus 审批状态
         * @return com.alibaba.fastjson.JSONObject
         * @author lichen
         * @date 2023/11/24
         */
        private JSONObject generateLineMsg(PropertyDTO propertyDTO, AdRequest adRequest, Integer approveStatus) {
            String timezone = propertyDTO.getTimezone();
            //配置的日期格式
            String longDateFormat = propertyDTO.getInternationalization().getDateTime().getLongDate();
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            //语言
            String language = propertyDTO.getLanguage();

            LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
            businessMap.put(i18nMessageUtils.message(AdjColumnEnum.ADJ_NUMBER.getI18nCode(), language), adRequest.getAdjNumber());
            businessMap.put(i18nMessageUtils.message(AdjColumnEnum.GUEST_NAME.getI18nCode(), language), adRequest.getGuestName());
            businessMap.put(i18nMessageUtils.message(AdjColumnEnum.ADJUSTED_DOLLAR.getI18nCode(), language),
                    InternationalizationUtils.formatAmountViewValue(adRequest.getAdjustedDollar(), number.getCalculationRule(), number.getCurrency()));

            //拼装checkInDate ~ checkOutDate
            long checkInDate = adRequest.getCheckInDate();
            if (checkInDate > 0) {
                String checkInDateStr = InternationalizationUtils.formatDateTime(checkInDate, longDateFormat, StringUtils.EMPTY, timezone, language);
                businessMap.put(i18nMessageUtils.message(AdjColumnEnum.CHECK_IN_DATE.getI18nCode(), language), checkInDateStr);
            }
            long checkOutDate = adRequest.getCheckOutDate();
            if (checkOutDate > 0) {
                String checkOutDateStr = InternationalizationUtils.formatDateTime(checkOutDate, longDateFormat, StringUtils.EMPTY, timezone, language);
                businessMap.put(i18nMessageUtils.message(AdjColumnEnum.CHECK_OUT_DATE.getI18nCode(), language), checkOutDateStr);
            }

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

            //获取line通知内容
            return lineMessageUtil.generatePublicFlexMsg(PublicFlexMsgParm.builder()
                    .propertyId(propertyDTO.getId())
                    .propertyName(propertyDTO.getName())
                    .functionName(i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, language))
                    .title(getLineTitle(approveStatus, language))
                    .businessMap(businessMap)
                    .link(linkUrl)
                    .build());
        }

        /**
         * 获取line通知的标题
         *
         * @param approveStatus 审批状态
         * @param language      语言标识
         * @return java.lang.String
         * @author lichen
         * @date 2024/11/13
         */
        private String getLineTitle(Integer approveStatus, String language) {
            if (approveStatus == AdjApprovalStatusEnum.APPROVING.getStatus()) {
                return "Waiting for approval";
            } else if (approveStatus == AdjApprovalStatusEnum.APPROVED.getStatus()) {
                return "Approval Passed";
            } else if (approveStatus == AdjApprovalStatusEnum.REJECTED.getStatus()) {
                return "Approval Rejected";
            } else if (approveStatus == AdjApprovalStatusEnum.RETURN.getStatus()) {
                return "Approval Returned";
            } else {
                return "";
            }
        }

        /**
         * 生成多条数据的lineMsg
         *
         * @param propertyDTO   资产DTO
         * @param adjList       adj集合
         * @param approveStatus 审批状态
         * @return com.alibaba.fastjson.JSONObject
         * @author lichen
         * @date 2024/1/30
         */
        private JSONObject generateMultipleDataLineMsg(PropertyDTO propertyDTO, List<AdRequest> adjList, Integer approveStatus) {
            String language = propertyDTO.getLanguage();
            String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

            LinkedHashMap<String, String> businessMap = new LinkedHashMap<>();
            List<String> adjNumbers = adjList.stream().map(AdRequest::getAdjNumber).collect(Collectors.toList());
            businessMap.put(i18nMessageUtils.message(AdjColumnEnum.ADJ_NUMBER.getI18nCode(), propertyDTO.getLanguage()), StringUtils.join(adjNumbers, ","));

            return lineMessageUtil.generatePublicFlexMsg(PublicFlexMsgParm.builder()
                    .propertyId(propertyDTO.getId())
                    .propertyName(propertyDTO.getName())
                    .functionName(i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, language))
                    .title(getLineTitle(approveStatus, language))
                    .businessMap(businessMap)
                    .link(linkUrl)
                    .build());
        }

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

        /**
         * 获取邮件内容
         *
         * @param propertyDTO 资产DTO
         * @param adjs        adj数据集合
         * @return java.lang.String
         * @author lichen
         * @date 2023/11/24
         */
        public String getEmailContent(Integer approveStatus, PropertyDTO propertyDTO, List<AdRequest> adjs, String defaultSubject) {
            //用户信息
            Set<Integer> dutyUserIds = adjs.stream().map(AdRequest::getDutyUser).collect(Collectors.toSet());
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), dutyUserIds);
            //转化数据
            List<AdjEmailDTO> adjEmailDTOS = convert(adjs, propertyDTO, userId2Name);

            //跳转链接
            String linkUrl = "";
            if (adjs.size() == 1) {
                linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}&adjNumber={2}", propertyDTO.getAbbr(),
                        propertyDTO.getTenantSymbol(), adjs.get(0).getAdjNumber());
            } 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("adjs", adjEmailDTOS);
            context.setVariable("count", adjEmailDTOS.size());
            context.setVariable("hereLink", linkUrl);
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("functionName", i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, propertyDTO.getLanguage()));

            String mailBody = templateEngine.process("adjustment/adjustment-approval_" + propertyDTO.getLanguage(), context);
            return EmailUtil.getFullEmailContent(defaultSubject, mailBody, propertyDTO.getLanguage());
        }

        /**
         * 获取调整数据的通知对象
         *
         * @param adRequest 调整数据
         * @return java.util.List<cloud.wconnect.commonlib.pojo.dto.NotifyIdentityDTO>
         * @author lichen
         * @date 2024/3/11
         */
        private List<NotifyIdentityDTO> getNotifyIdentity(AdRequest adRequest) {
            //查询adjustment数据的审批层级
            AdjApproveLevel approveLevel = approveLevelService.getById(adRequest.getApproveLevelId());
            //查询level下配置的审批人
            Integer propertyId = adRequest.getPropertyId();
            Set<Integer> approveUserIds = approveConfigService.getApproveUsers(propertyId, approveLevel.getId());

            //初始化未审批的人等于当前配置的审批人
            Set<Integer> unApprover = Sets.newHashSet(approveUserIds);

            //查询已经审批的日志
            Set<Integer> alreadyApprover = approveLogService.listCurrentNodeApprovalLog(adRequest)
                    .stream().map(AdjApproveLog::getApproveBy).collect(Collectors.toSet());

            //已审批人不为空时
            if (CollectionUtils.isNotEmpty(alreadyApprover)) {
                //未审批人等于 应该审批的人 减去 已审批人
                unApprover.removeAll(alreadyApprover);
            }

            // 过滤掉无效用户（1.禁用用户，2.无审批权限用户），不给这些人发通知，若都是无效人员，则不发通知，等管理员调整审批人。然后剩下的人，看看是否有补位人员，若有，将此人换成给补位人员发通知
            List<Integer> effectiveUnApprover = adjCheckUserEffectiveService.checkUserEffective(propertyId, new ArrayList<>(unApprover));
            if (CollectionUtils.isEmpty(effectiveUnApprover)) return Collections.emptyList();

            Map<Integer, Integer> substitutePersonnelIdByApprover =
                    adjOutOfOfficeConfigService.listEffectiveConfigForApprover(propertyId, adRequest.getRequestType(), new ArrayList<>(effectiveUnApprover))
                            .stream().collect(Collectors.toMap(AdjOutOfOfficeConfig::getUpdatedBy, AdjOutOfOfficeConfig::getSubstitutePersonnelId));
            // 定义需要发送审批通知的用户
            List<Integer> notifyUserIds = Lists.newArrayListWithExpectedSize(effectiveUnApprover.size());
            for (Integer userId : effectiveUnApprover) {
                // 若有补位人员且补位人员没有审批过，则添加补位人员，否则添加审批人
                Integer substitutePersonnelId = substitutePersonnelIdByApprover.get(userId);
                if (Objects.nonNull(substitutePersonnelId)) {
                    if (!alreadyApprover.contains(substitutePersonnelId)) {
                        notifyUserIds.add(substitutePersonnelId);
                    }
                } else {
                    notifyUserIds.add(userId);
                }
            }

            //审批人集合
            List<NotifyIdentityDTO> identitys = Lists.newArrayListWithCapacity(unApprover.size());
            identitys.addAll(notifyUserIds.stream().map(userId -> new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER, userId,
                    EmailConfigCons.TARGET_TYPE_TO, "")).toList());
            //抄送对象集合
            if (StringUtils.isNotBlank(approveLevel.getCc()) && !approveLevel.getCc().equals("[]")) {
                List<NotifyIdentityDTO> identitysByCC = JsonUtils.convertStringToList(objectMapper, approveLevel.getCc(), NotifyIdentityDTO.class);
                identitysByCC.forEach(cc -> cc.setTargetType(EmailConfigCons.TARGET_TYPE_CC));
                identitys.addAll(identitysByCC);
            }
            return identitys;
        }

        /**
         * AdRequest -> AdjEmailDTO
         *
         * @param adRequests  adjustment 集合
         * @param propertyDTO 资产信息
         * @return cloud.wconnect.frontoffice.pojo.dto.adjustment.AdjEmailDTO
         * @author lichen
         * @date 2023/5/23
         */
        private List<AdjEmailDTO> convert(List<AdRequest> adRequests, PropertyDTO propertyDTO, Map<Integer, String> userId2Name) {
            String timezone = propertyDTO.getTimezone();
            //配置的日期格式
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDateFormat = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            //获取货币符号
            String currency = number.getCurrency();
            //语言
            String language = propertyDTO.getLanguage();

            String i18nYesMsg = i18nMessageUtils.message(I18nConstant.YES, language);
            String i18nNoMsg = i18nMessageUtils.message(I18nConstant.NO, language);
            String i18nArchivedMsg = i18nMessageUtils.message(I18nConstant.ARCHIVED, language);
            String i18nApproved = i18nMessageUtils.message(AdjApprovalStatusEnum.APPROVED.getI18nCode(), language);
            String i18nRejected = i18nMessageUtils.message(AdjApprovalStatusEnum.REJECTED.getI18nCode(), language);
            String matched = i18nMessageUtils.message(AdjFieldI18nConstant.MATCHED, language);
            String unMatched = i18nMessageUtils.message(AdjFieldI18nConstant.UN_MATCHED, language);

            return beanMapper.mapList(adRequests, AdjEmailDTO.class, (adRequest, adjEmailDTO) -> {
                if(StringUtils.isBlank(adRequest.getRoomNumber())){
                    adjEmailDTO.setRoomNumber("--");
                }
                if(StringUtils.isBlank(adRequest.getGuestName())){
                    adjEmailDTO.setGuestName("--");
                }
                //拼装checkInDate ~ checkOutDate
                String checkInDateStr = adRequest.getCheckInDate() > 0
                        ? InternationalizationUtils.formatDateTime(adRequest.getCheckInDate(), longDateFormat, StringUtils.EMPTY, timezone, language)
                        : "--";
                adjEmailDTO.setCheckInDateStr(checkInDateStr);

                String checkOutDateStr = adRequest.getCheckOutDate() > 0
                        ? InternationalizationUtils.formatDateTime(adRequest.getCheckOutDate(), longDateFormat, StringUtils.EMPTY, timezone, language)
                        : "--";
                adjEmailDTO.setCheckOutDateStr(checkOutDateStr);

                //格式化金额
                String totalAmountStr = InternationalizationUtils.formatAmountViewValue(adRequest.getAdjustedDollar(), number.getCalculationRule(), currency);
                adjEmailDTO.setFormatAdjustedDollar(totalAmountStr);

                //requestTypeDesc赋值
                adjEmailDTO.setRequestTypeDesc(i18nMessageUtils.message(AdjTypeEnum.getI18nCode(adRequest.getRequestType()), language));
                //revenueLostDesc赋值
                adjEmailDTO.setRevenueLostDesc(Objects.isNull(adRequest.getRevenueLost()) ? "--" : adRequest.getRevenueLost() ? i18nYesMsg : i18nNoMsg);
                //拼装dutyUser
                Integer dutyUser = adRequest.getDutyUser();
                adjEmailDTO.setDutyByName(userId2Name.get(dutyUser));
                //拼装businessDateStr
                if (adRequest.getBusinessDate() > 0) {
                    adjEmailDTO.setBusinessDateStr(InternationalizationUtils.formatDateTime(
                            DateTimeUtils.dateStrToEpochSecond(adRequest.getBusinessDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD, timezone),
                            longDateFormat, "", timezone, language));
                } else {
                    adjEmailDTO.setBusinessDateStr("--");
                }
                //设置显示状态
                String viewStatus = "";
                if (adRequest.getFlowStatus() == FlowStatusEnum.DRAFT.getFlowStatus()) {
                    viewStatus = i18nMessageUtils.message(FlowStatusEnum.DRAFT.getI18nCode(), language);
                } else if (adRequest.getFlowStatus() == FlowStatusEnum.APPROVAING.getFlowStatus()) {
                    viewStatus = i18nMessageUtils.message(FlowStatusEnum.APPROVAING.getI18nCode(), language);
                } else {
                    if (adRequest.getStatus() == AdjustmentStatusEnum.VALID.getStatus()) {
                        if (adRequest.getIsArchive()) {
                            viewStatus = i18nArchivedMsg + "\r\n" + "(" + i18nApproved + ")";
                        } else {
                            viewStatus = i18nApproved;
                        }
                    } else if (adRequest.getStatus() == AdjustmentStatusEnum.INVALID.getStatus()) {
                        if (adRequest.getIsArchive()) {
                            viewStatus = i18nArchivedMsg + "\r\n" + "(" + i18nRejected + ")";
                        } else {
                            viewStatus = i18nRejected;
                        }
                    }
                }
                adjEmailDTO.setViewStatusDesc(viewStatus);
                //设置创建日期
                adjEmailDTO.setCreateTimeStr(InternationalizationUtils.formatDateTime(adRequest.getCreateTime(), longDateFormat, hourClock, timezone, language));
                //设置匹配状态
                adjEmailDTO.setMatchStatusStr(adRequest.getBusinessDate() > 0 ? matched : unMatched);
            });
        }

        /**
         * 审批通过时，通知配置人
         *
         * @param propertyDTO 资产DTO
         * @param adjList     调账数据集合
         * @return void
         * @author lichen
         * @date 2024/11/13
         */
        private void notifyConfigUserByApproved(PropertyDTO propertyDTO, List<AdRequest> adjList) {
            String language = propertyDTO.getLanguage();

            //查询邮件配置
            AdjEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(), AdjNotifyConfigTypeEnum.APPROVED.getType());
            if (Objects.isNull(emailConfig)) return;
            if (!emailConfig.getStatus()) {
                log.warn("Adjustment notifyConfigUserByApproved end, email config is close.");
                return;
            }

            //查询配置的通知人
            List<AdjEmailTarget> emailTargets = emailTargetService.listByConfigId(Lists.newArrayList(emailConfig.getId()));
            if (CollectionUtils.isEmpty(emailTargets)) return;
            Map<Integer, List<AdjEmailTarget>> requestType2EmailTargets = emailTargets.stream().collect(Collectors.groupingBy(AdjEmailTarget::getRequestType));
            HashMap<Integer, List<NotifyIdentityDTO>> requestType2NotifyIdentityDTOS = Maps.newHashMapWithExpectedSize(requestType2EmailTargets.size());
            for (Map.Entry<Integer, List<AdjEmailTarget>> entry : requestType2EmailTargets.entrySet()) {
                beanMapper.mapList(emailTargets, NotifyIdentityDTO.class);
                requestType2NotifyIdentityDTOS.put(entry.getKey(), beanMapper.mapList(entry.getValue(), NotifyIdentityDTO.class));
            }

            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, language);

            //默认主题的国际化code
            String defaultSubjectI18nCode = AdjNotifyConfigTypeEnum.getDefaultSubjectI18nCode(AdjNotifyConfigTypeEnum.APPROVED.getType());
            //默认的主题
            String defaultSubject = i18nMessageUtils.message(defaultSubjectI18nCode, language, functionName, propertyDTO.getName());
            //获取通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //按照类型分组
            Map<Integer, List<AdRequest>> requestType2AdjList = adjList.stream().collect(Collectors.groupingBy(AdRequest::getRequestType));
            for (Map.Entry<Integer, List<AdRequest>> entry : requestType2AdjList.entrySet()) {
                Integer requestType = entry.getKey();
                List<AdRequest> adjListByRequestType = entry.getValue();
                //指定类型下的审批通过通知人集合
                List<NotifyIdentityDTO> identityDTOS = requestType2NotifyIdentityDTOS.get(requestType);
                if (CollectionUtils.isNotEmpty(identityDTOS)) {
                    //获取邮件通知内容
                    String emailContent = getEmailContent(AdjApprovalStatusEnum.APPROVED.getStatus(), propertyDTO, adjListByRequestType, defaultSubject);
                    //获取line通知内容
                    JSONObject lineMsg;
                    if (adjListByRequestType.size() == 1) {
                        lineMsg = generateLineMsg(propertyDTO, adjListByRequestType.get(0), AdjApprovalStatusEnum.APPROVED.getStatus());
                    } else {
                        lineMsg = generateMultipleDataLineMsg(propertyDTO, adjListByRequestType, AdjApprovalStatusEnum.APPROVED.getStatus());
                    }
                    //发送通知
                    doSendNotify(propertyDTO, subject, identityDTOS,
                            FunctionPointEnum.ADJ_APPROVED_NOTIFY.getFunctionPoint(), emailContent, lineMsg);
                }
            }
        }

        /**
         * 获取通知的默认主题
         *
         * @param propertyDTO 资产DTO
         * @param requestType 调账类型
         * @return java.lang.String
         * @author lichen
         * @date 2025/1/15
         */
        private String getDefaultSubject(PropertyDTO propertyDTO, int requestType) {
            //功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, propertyDTO.getLanguage());
            //获取通知类型
            int notifyType = AdjNotifyConfigTypeEnum.getTypeByRequestType(requestType);
            //默认主题的国际化code
            String defaultSubjectI18nCode = AdjNotifyConfigTypeEnum.getDefaultSubjectI18nCode(notifyType);
            //默认的主题
            return i18nMessageUtils.message(defaultSubjectI18nCode, propertyDTO.getLanguage(), functionName, propertyDTO.getName());
        }

        /**
         * 通知指定用户
         *
         * @param propertyDTO  资产
         * @param type         类型
         * @param adRequests   adjustment数据集合
         * @param targetUserId 通知人 id
         * @author yyt
         * @date 2026/3/19
         */
        @Override
        public void notifyTargetUser(PropertyDTO propertyDTO, Integer type, List<AdRequest> adRequests, Integer targetUserId) {
            if (CollectionUtils.isEmpty(adRequests)) {
                return;
            }
            //获取邮件配置
            AdjEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyDTO.getId(), AdjNotifyConfigTypeEnum.getTypeByRequestType(type));
            if (Objects.isNull(emailConfig)) {
                emailConfig = new AdjEmailConfig();
                emailConfig.setStatus(true);
            }
            if (!emailConfig.getStatus()) {
                log.warn("Adjustment notify approver end, email config is close.");
                return;
            }

            //默认主题
            String defaultSubject = getDefaultSubject(propertyDTO, type);

            //获取通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            // 审批人
            List<NotifyIdentityDTO> identitys = Lists.newArrayList(new NotifyIdentityDTO(EmailConfigCons.SOURCE_USER, targetUserId,
                    EmailConfigCons.TARGET_TYPE_TO, StringUtils.EMPTY));

            //获取邮件通知内容
            String emailContent = getEmailContent(AdjApprovalStatusEnum.APPROVING.getStatus(), propertyDTO, adRequests, defaultSubject);

            //line消息内容
            JSONObject lineMsg;
            if (adRequests.size() == 1) {
                lineMsg = generateLineMsg(propertyDTO, adRequests.get(0), AdjApprovalStatusEnum.APPROVING.getStatus());
            } else {
                lineMsg = generateMultipleDataLineMsg(propertyDTO, adRequests, AdjApprovalStatusEnum.APPROVING.getStatus());
            }

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

        /**
         * 发送审批人员禁用通知
         *
         * @param propertyDTO       资产
         * @param userId            禁用人员 id
         * @param adjApproveConfigs 禁用人员所属审批层级
         * @author yyt
         * @date 2026/3/20
         */
        @Override
        public void notifyApprovalPersonnelDisabled(PropertyDTO propertyDTO, Integer userId, List<AdjApproveConfig> adjApproveConfigs) {
            // 邮件配置
            Integer propertyId = propertyDTO.getId();
            AdjEmailConfig emailConfig = emailConfigService.getByPropertyIdAndType(propertyId, AdjNotifyConfigTypeEnum.APPROVAL_PERSON_DISABLED.getType());
            if (Objects.isNull(emailConfig) || !emailConfig.getStatus()) {
                return;
            }
            List<AdjEmailTarget> adjEmailTargets = adjEmailTargetService.listByConfigId(emailConfig.getId());
            if (CollectionUtils.isEmpty(adjEmailTargets)) return;

            String language = propertyDTO.getLanguage();
            String propertyName = propertyDTO.getName();

            // 功能名称
            String functionName = i18nMessageUtils.message(FunctionCons.ADJUSTMENT_REQUESTS, language);
            // 默认主题
            String defaultSubject = i18nMessageUtils.message(AdjNotifyConfigTypeEnum.APPROVAL_PERSON_DISABLED.getDefaultSubjectI18nCode(), language, functionName, propertyName);
            // 通知主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;
            // 页面跳转链接
            String linkUrl = MessageFormat.format(hereLink + "?fromPage=email&property={0}&tenant={1}", propertyDTO.getAbbr(), propertyDTO.getTenantSymbol());

            // 禁用人员姓名
            String userName = userInternalService.getOne(propertyDTO.getTenantId(), userId).getName();

            // 汇总审批层级名称
            Map<Integer, Map<Integer, String>> approveLevelStr = getApproveLevelName(propertyDTO);

            // 邮件内容
            Context context = new Context();
            context.setVariable("hereLink", linkUrl);
            context.setVariable("propertyName", propertyName);
            context.setVariable("functionName", functionName);
            context.setVariable("userName", userName);
            context.setVariable("approvalLevelVOs", adjApproveConfigs.stream()
                    .sorted(Comparator.comparing(AdjApproveConfig::getType))
                    .map(item ->
                            new ApprovalLevelVO(i18nMessageUtils.message(AdjTypeEnum.getI18nCode(item.getType()), language),
                                    approveLevelStr.getOrDefault(item.getType(), Collections.emptyMap()).get(item.getApproveIndex()))
                    ).toList());
            String emailContent = templateEngine.process("adjustment/approval-personnel-disabled_" + language, context);
            emailContent = EmailUtil.getFullEmailContent(defaultSubject, emailContent, language);

            // line通知内容
            String lineContentDetail = "The approver " + userName + " has been disabled and will be skipped in the subsequent approval process. Please adjust the approval configuration in a timely manner.";
            JSONObject lineMsg = lineMessageUtil.generateTextMsgByContent(lineContentDetail);

            // 发送通知
            doSendNotify(propertyDTO, subject, beanMapper.mapList(adjEmailTargets, NotifyIdentityDTO.class),
                    FunctionPointEnum.ADJ_APPROVE_DISABLED_NOTIFY.getFunctionPoint(), emailContent, lineMsg);
        }

        /**
         * 汇总审批层级名称
         *
         * @param propertyDTO 资产
         * @return 审批层级名称
         * @author yyt
         * @date 2026/3/26
         **/
        private Map<Integer, Map<Integer, String>> getApproveLevelName(PropertyDTO propertyDTO) {
            List<AdjApproveLevel> adjApproveLevels = adjApproveLevelService.listByPropertyId(propertyDTO.getId());
            Map<Integer, Map<Integer, String>> approveLevelStrByType = Maps.newLinkedHashMapWithExpectedSize(adjApproveLevels.size());

            String language = propertyDTO.getLanguage();
            adjApproveLevels.stream().collect(Collectors.groupingBy(AdjApproveLevel::getType))
                    .forEach((type, currentTypeApproveLevels) -> {
                        currentTypeApproveLevels.sort(Comparator.comparing(AdjApproveLevel::getApproveIndex));

                        Map<Integer, String> approveLevelStr = Maps.newLinkedHashMapWithExpectedSize(currentTypeApproveLevels.size());
                        for (int i = 1; i <= currentTypeApproveLevels.size(); i++) {
                            // 第几级审批
                            approveLevelStr.put(currentTypeApproveLevels.get(i - 1).getApproveIndex(), i18nMessageUtils.message(AdjFieldI18nConstant.APPROVAL_NO, language, i));
                        }
                        approveLevelStrByType.put(type, approveLevelStr);
                    });

            return approveLevelStrByType;
        }

    }