# CbCaseNotifyServiceImpl.java

    package cloud.wconnect.finance.service.chargeback.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.ApproveStatusEnum;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.EmailConfigCons;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    import cloud.wconnect.commonlib.pojo.dto.*;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationDateTimeDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.pojo.dto.line.SingleApprovalMsgParm;
    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.finance.cons.FunctionPointEnum;
    import cloud.wconnect.finance.cons.chargeback.*;
    import cloud.wconnect.finance.entity.chargeback.*;
    import cloud.wconnect.finance.pojo.dto.chargeback.CaseStageApproveLevelDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.PublishApproveLogDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.PublishApproveLogsDTO;
    import cloud.wconnect.finance.pojo.dto.chargeback.PublishLogDTO;
    import cloud.wconnect.finance.pojo.vo.chargeback.CaseNotifyVO;
    import cloud.wconnect.finance.service.chargeback.*;
    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 jakarta.annotation.Nullable;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.compress.utils.Sets;
    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.util.*;
    import java.util.stream.Collectors;

    /**
     * @author yyt
     * 2023/8/25
     */
    @Service
    @DS("finance")
    public class CbCaseNotifyServiceImpl implements ICbCaseNotifyService {

        @Autowired
        private ICbCaseBankService cbCaseBankService;
        @Autowired
        private ICbCaseApproveLevelService cbCaseApproveLevelService;
        @Autowired
        private ICbCaseApproveLevelUserService cbCaseApproveLevelUserService;
        @Autowired
        private ICbCaseActionService cbCaseActionService;
        @Autowired
        private TemplateEngine templateEngine;
        @Autowired
        private ICbCasePublishLogService cbCasePublishLogService;
        @Autowired
        private ICbCaseEmailConfigService cbCaseEmailConfigService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private LineMessageUtil lineMessageUtil;
        @Autowired
        private ICbCaseService cbCaseService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private ICbCaseApproveLogService cbCaseApproveLogService;
        @Autowired
        private ObjectMapper objectMapper;

        @Value("${here.link.chargeback.case-list}")
        private String caseLink;

        public static final String APPROVE_SUBJECT = " Waiting For Approval";
        public static final String PROCESSING_SUBJECT = " Waiting For Processing";

        public static final String SINGLE_CASE_NOTIFY_HTML = "chargeback/case_notify_";

        /**
         * case 通知用户
         *
         * @param cbCases             case
         * @param excludeTargetUserId 排除通知指定用户
         * @param singleCasePush      单个 case 推送
         * @param lastStage           上个阶段（为空时，表示阶段未变更）
         * @author yyt
         * @date 2023/8/25
         */
        @Override
        public void notifyUser(List<CbCase> cbCases, @Nullable Integer excludeTargetUserId, boolean singleCasePush,  @Nullable Integer lastStage) {
            CbCase cbCase = cbCases.get(0);
            Integer propertyId = cbCase.getPropertyId();
            if (singleCasePush) {
                notifyUserSingleCase(cbCase, excludeTargetUserId, lastStage);
            } else {
                // 若当前阶段 通知关闭时，不发通知
                if (!cbCaseEmailConfigService.checkNotify(propertyId, cbCaseEmailConfigService.getTypeForStage(cbCase.getStage()))) {
                    return;
                }

                // 批量推送
                notifyUserBatchCase(cbCases);
            }
        }

        /**
         * 单个 case 通知用户
         *
         * @param cs                  case
         * @param excludeTargetUserId 排除通知指定用户
         * @param lastStage           上个阶段
         * @author yyt
         * @date 2023/9/11
         */
        @Override
        public void notifyUserSingleCase(CbCase cs, @Nullable Integer excludeTargetUserId, @Nullable Integer lastStage) {
            Integer propertyId = cs.getPropertyId();
            Integer stage = cs.getStage();
            if (Objects.nonNull(lastStage)) {
                // 处理阶段变更时，若变更前的前阶段 通知关闭时，不发通知
                if (!cbCaseEmailConfigService.checkNotify(propertyId, cbCaseEmailConfigService.getTypeForStage(lastStage))) {
                    return;
                }
            }

            String subject = getSubject(cs, lastStage, false);

            // 若当前阶段 通知关闭时，不发通知
            if (!cbCaseEmailConfigService.checkNotify(propertyId, cbCaseEmailConfigService.getTypeForStage(stage))) {
                return;
            }

            // 下一个节点审批（1：下个阶段，2：当前阶段的下级审批）
            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(cs.getLevelId());
            List<CbCaseApproveLevelUser> approveLevelUsers = cbCaseApproveLevelUserService.listApproveUser(propertyId, stage, approveLevel.getId());
            // 审批人
            List<Integer> userIds = approveLevelUsers.stream().map(CbCaseApproveLevelUser::getUserId).filter(userId -> {
                        if (Objects.isNull(excludeTargetUserId)) {
                            return true;
                        }
                        return !userId.equals(excludeTargetUserId);
                    })
                    .collect(Collectors.toList());

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            List<JSONObject> lineMsg;
            if (approveLevel.getApproveIndex().equals(BaseConstant.ZERO)) {
                lineMsg = Lists.newArrayList(generateNotifyLineMsg(propertyDTO, cs));
            } else {
                JSONObject msg = generateApproveLineMsg(propertyDTO, cs, ApproveStatusEnum.APPROVEING.getStatus());
                lineMsg = Lists.newArrayList(msg);
            }
            Context context = packageEmailContent(cs, propertyDTO, lastStage, false);
            context.setVariable("lastStage", lastStage);
            context.setVariable("reject", false);
            String content = renderingEmails(context, SINGLE_CASE_NOTIFY_HTML, propertyDTO.getLanguage(), subject);

            handleNotify(propertyId, FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(),
                    subject, userIds, lineMsg, content, approveLevel.getCc());
        }

        /**
         * 获取邮件标题
         *
         * @param cs        case
         * @param lastStage 上个阶段
         * @param reject    审批拒绝
         * @return 邮件标题
         * @author yyt
         * @date 2024/2/19
         **/
        private String getSubject(CbCase cs, Integer lastStage, boolean reject) {
            Integer currentStage = cs.getStage();
            String subject = StringUtils.EMPTY;
            PropertyDTO propertyDTO = propertyI18nRedisService.get(cs.getPropertyId());
            String caseNumber = cs.getCaseNumber();
            Integer result = cs.getResult();
            String abbr = StringUtils.upperCase(propertyDTO.getAbbr());
            String language = propertyDTO.getLanguage();
            Map<Integer, Integer> actionByStage = cbCaseActionService.list(Lists.newArrayList(cs.getId()),
                            Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE))
                    .stream().collect(Collectors.toMap(CbCaseAction::getStage, CbCaseAction::getAction));
            Integer overdueAction = actionByStage.get(StageCons.OVERDUE);
            if (Objects.isNull(lastStage)) {
                // 阶段未变更：1.推送时，等待审批，2.审批拒绝
                switch (currentStage) {
                    case StageCons.INITIAL_CASE, StageCons.OVERDUE ->
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.REJECT : CaseNotifyCons.WAIT_APPROVE, language, abbr, caseNumber);
                    case StageCons.HOTEL_OPERATION -> {
                        if (Objects.isNull(overdueAction)) {
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.REJECT : CaseNotifyCons.WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            // 有 overdue ，选择 accept
                            // 有 overdue，选择 dispute
                            if (reject) {
                                CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getCaseRejectLevel(cs);
                                subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_REJECT, language, abbr, caseNumber, approveLevel.getName());
                            } else {
                                subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_WAIT_APPROVE, language, abbr, caseNumber);
                            }
                        }
                    }
                    case StageCons.PORTAL -> {
                        if (result == CaseResultEnum.WIN.getCode()) {
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.WIN_REJECT : CaseNotifyCons.WIN_WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.LOSS_REJECT : CaseNotifyCons.LOSS_WAIT_APPROVE, language, abbr, caseNumber);
                        }
                    }
                    case StageCons.PATCH -> {
                        if (Objects.isNull(overdueAction)) {
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.ADJ_REJECT : CaseNotifyCons.ADJ_WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(reject ? CaseNotifyCons.OVERDUE_ADJ_REJECT : CaseNotifyCons.OVERDUE_ADJ_WAIT_APPROVE, language, abbr, caseNumber);
                        }
                    }
                    case StageCons.FINAL -> {
                        subject = i18nMessageUtils.message(reject ? CaseNotifyCons.CLOSE_REJECT : CaseNotifyCons.CLOSE_WAIT_APPROVE, language, abbr, caseNumber);
                    }
                }
            } else {
                // 阶段变更：1.推送时，无审批，2.审批同意，推送至下个阶段
                // 初始阶段 -> 酒店表态阶段
                switch (lastStage) {
                    case StageCons.INITIAL_CASE ->
                            subject = i18nMessageUtils.message(CaseNotifyCons.NO_APPROVE, language, abbr, caseNumber);
                    case StageCons.HOTEL_OPERATION -> {
                        // 酒店表态阶段 推送
                        // 无 overdue
                        if (Objects.isNull(overdueAction)) {
                            subject = i18nMessageUtils.message(cs.getAction() == CaseActionEnum.ACCEPT.getCode() ?
                                    CaseNotifyCons.ACCEPT : CaseNotifyCons.DISPUTE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_HOTEL_OPERATIONS_APPROVED,
                                    language, abbr, caseNumber);
                        }
                    }
                    // overdue 阶段推送
                    case StageCons.OVERDUE -> {
                        subject = i18nMessageUtils.message(overdueAction == CaseActionEnum.ACCEPT.getCode() ?
                                CaseNotifyCons.OVERDUE_ACCEPT : CaseNotifyCons.OVERDUE_DISPUTE, language, abbr, caseNumber);

                        // dispute, 向酒店表态 处理人发送邮件
                        if (overdueAction == CaseActionEnum.DISPUTE.getCode()) {
                            handleNotifyHotelOperationHandler(propertyDTO, cs);
                        }
                    }
                    // portal 阶段推送
                    case StageCons.PORTAL -> {
                        if (Objects.isNull(overdueAction)) {
                            // win -  CPE  Chargeback TEST003 Case Win
                            // loss - CPE  Chargeback TEST001 Please Fill out Adjustment Form
                            subject = i18nMessageUtils.message(result == CaseResultEnum.WIN.getCode() ?
                                    CaseNotifyCons.WIN : CaseNotifyCons.LOSS, language, abbr, caseNumber);
                        } else {
                            // overdue - win - CPE  Chargeback TEST007 Overdue-Win，同步向 酒店表态阶段的 处理人 + 审批人发送邮件
                            // overdue - loss - CPE  Chargeback TEST006 Overdue-Please Fill Out Adjustment Form
                            subject = i18nMessageUtils.message(result == CaseResultEnum.WIN.getCode() ?
                                    CaseNotifyCons.OVERDUE_WIN : CaseNotifyCons.OVERDUE_LOSS, language, abbr, caseNumber);

                            if (result == CaseResultEnum.WIN.getCode()) {
                                handleNotifyHotelOperationHandlerAndApprover(propertyDTO, cs, lastStage);
                            }
                        }
                    }
                    // patch 阶段推送
                    case StageCons.PATCH -> {
                        // 未 overdue - CPE  Chargeback TEST001 Please Process Write-Off
                        // 有 overdue - CPE  Chargeback TEST006 Overdue- Please Process Write-Off
                        subject = i18nMessageUtils.message(Objects.isNull(overdueAction) ? CaseNotifyCons.ACCEPT :
                                CaseNotifyCons.OVERDUE_PATCH_ACCEPT, language, abbr, caseNumber);
                    }
                }
            }

            return subject;
        }

        /**
         * 处理向酒店表态阶段的处理人发送邮件
         *
         * @param propertyDTO 资产
         * @param cs          case
         * @author yyt
         * @date 2024/2/22
         **/
        private void handleNotifyHotelOperationHandler(PropertyDTO propertyDTO, CbCase cs) {
            if (!cbCaseEmailConfigService.checkNotify(cs.getPropertyId(), cbCaseEmailConfigService.getTypeForStage(StageCons.HOTEL_OPERATION))) {
                return;
            }

            String language = propertyDTO.getLanguage();

            String subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_DISPUTE, language, StringUtils.upperCase(propertyDTO.getAbbr()), cs.getCaseNumber());

            Optional<CbCaseApproveLevel> approveLevelOptional = cbCaseApproveLevelService.listZeroLevel(propertyDTO.getId()).stream()
                    .filter(item -> item.getStage() == StageCons.HOTEL_OPERATION && item.getApproveIndex() == BaseConstant.ZERO).findFirst();
            if (approveLevelOptional.isEmpty()) {
                // 未配置 处理人层级
                return;
            }

            CbCaseApproveLevel approveLevel = approveLevelOptional.get();
            List<CbCaseApproveLevelUser> approveLevelUsers = cbCaseApproveLevelUserService.listApproveUser(propertyDTO.getId(),
                    StageCons.HOTEL_OPERATION, approveLevel.getId());
            if (CollectionUtils.isEmpty(approveLevelUsers)) {
                // 未配置 处理人层级下的处理人
                return;
            }

            List<JSONObject> lineMsg = Lists.newArrayList(generateNotifyLineMsg(propertyDTO, cs));

            Context context = packageEmailContent(cs, propertyDTO, null, false);
            context.setVariable("overdueSendHotelOperationHandle", true);
            String content = renderingEmails(context, SINGLE_CASE_NOTIFY_HTML, propertyDTO.getLanguage(), subject);

            handleNotify(propertyDTO.getId(), FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(),
                    subject, approveLevelUsers.stream().map(CbCaseApproveLevelUser::getUserId).toList(), lineMsg, content, null);
        }

        /**
         * 处理向酒店表态阶段的处理人+审批人发送邮件
         *
         * @param propertyDTO 资产
         * @param cs          case
         * @param lastStage   上个阶段
         * @author yyt
         * @date 2024/2/22
         **/
        private void handleNotifyHotelOperationHandlerAndApprover(PropertyDTO propertyDTO, CbCase cs, Integer lastStage) {
            if (!cbCaseEmailConfigService.checkNotify(cs.getPropertyId(), cbCaseEmailConfigService.getTypeForStage(StageCons.HOTEL_OPERATION))) {
                return;
            }

            String language = propertyDTO.getLanguage();
            String subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_WIN, language, StringUtils.upperCase(propertyDTO.getAbbr()), cs.getCaseNumber());

            List<CbCaseApproveLevel> approveLevels = cbCaseApproveLevelService.listApproveLevel(propertyDTO.getId()).stream()
                    .filter(item -> item.getStage() == StageCons.HOTEL_OPERATION).toList();
            if (CollectionUtils.isEmpty(approveLevels)) {
                // 未配置 处理人层级 + 审批人层级
                return;
            }

            List<CbCaseApproveLevelUser> approveLevelUsers = cbCaseApproveLevelUserService.listApproveUser(propertyDTO.getId(),
                    approveLevels.stream().map(CbCaseApproveLevel::getId).toList());
            if (CollectionUtils.isEmpty(approveLevelUsers)) {
                // 未配置 处理人层级 + 审批层级 下的处理人
                return;
            }

            List<JSONObject> lineMsg = Lists.newArrayList(generateNotifyLineMsg(propertyDTO, cs));

            Context context = packageEmailContent(cs, propertyDTO, lastStage, false);
            context.setVariable("lastStage", lastStage);
            String content = renderingEmails(context, SINGLE_CASE_NOTIFY_HTML, propertyDTO.getLanguage(), subject);

            handleNotify(propertyDTO.getId(), FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(),
                    subject, approveLevelUsers.stream().map(CbCaseApproveLevelUser::getUserId).toList(), lineMsg, content, null);
        }

        /**
         * 生成通知 line msg
         *
         * @param propertyDTO 资产
         * @param cs         case
         * @return line msg
         * @author yyt
         * @date 2023/11/28
         */
        private JSONObject generateNotifyLineMsg(PropertyDTO propertyDTO, CbCase cs) {
            // CPE 20231010001 waiting For Processing
            JSONObject msg = new JSONObject();
            msg.put("type", "text");
            msg.put("text", propertyDTO.getName() + " " + cs.getCaseNumber() + StringUtils.lowerCase(PROCESSING_SUBJECT));
            return msg;
        }

        /**
         * 创建审批 line msg
         *
         * @param propertyDTO    资产
         * @param cs            case
         * @param approveStatus 审批状态
         * @return line msg
         * @author yyt
         * @date 2023/11/27
         */
        private JSONObject generateApproveLineMsg(PropertyDTO propertyDTO, CbCase cs, int approveStatus) {
            List<CaseStageApproveLevelDTO> caseStageApproveLevelDTOs = cbCaseService.listApproveRecord(cs, propertyDTO);
            List<ApproveLevelDTO> approveLevelDTOS = beanMapper.mapList(caseStageApproveLevelDTOs, ApproveLevelDTO.class);
            LinkedHashMap<String, String> businessMap = Maps.newLinkedHashMapWithExpectedSize(3);
            businessMap.put("Case Number", cs.getCaseNumber());

            Map<Integer, String> vendorById = cbCaseBankService.list(Sets.newHashSet(cs.getPropertyId()));
            businessMap.put("Vendor", vendorById.get(cs.getVendor()));

            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            businessMap.put("Dispute Amount", InternationalizationUtils.formatAmountViewValue(cs.getDisputeAmount(),
                    number.getCalculationRule(), number.getCurrency()));

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

        /**
         * 设置邮件内容
         *
         * @param cs         case
         * @param propertyDTO 资产
         * @param lastStage  上个阶段（为空时，表示非切换阶段）
         * @param reminder   提醒处理
         * @return 邮件内容
         * @author yyt
         * @date 2023/9/11
         */
        private Context packageEmailContent(CbCase cs, PropertyDTO propertyDTO, @Nullable Integer lastStage, boolean reminder) {
            CaseNotifyVO caseNotifyVO = new CaseNotifyVO();
            caseNotifyVO.setCaseNumber(cs.getCaseNumber());

            Map<Integer, String> vendorById = cbCaseBankService.list(Sets.newHashSet(cs.getPropertyId()));
            caseNotifyVO.setVendor(vendorById.get(cs.getVendor()));

            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            caseNotifyVO.setDisputeAmount(InternationalizationUtils.formatAmountViewValue(cs.getDisputeAmount(),
                    number.getCalculationRule(), number.getCurrency()));

            Map<Integer, Integer> actionByStage = cbCaseActionService.list(Lists.newArrayList(cs.getId()),
                            Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE))
                    .stream().collect(Collectors.toMap(CbCaseAction::getStage, CbCaseAction::getAction));

            String language = propertyDTO.getLanguage();
            caseNotifyVO.setHotelAction(actionByStage.get(StageCons.HOTEL_OPERATION));
            Integer hotelAction = actionByStage.get(StageCons.HOTEL_OPERATION);
            caseNotifyVO.setHotelActionDesc(Objects.isNull(hotelAction) ? "--" : i18nMessageUtils.message(CaseActionEnum.getActionDescForEmail(hotelAction), language));
            caseNotifyVO.setOverdueAction(actionByStage.get(StageCons.OVERDUE));
            Integer overdueAction = actionByStage.get(StageCons.OVERDUE);
            caseNotifyVO.setOverdueActionDesc(Objects.isNull(overdueAction) ? "--" : i18nMessageUtils.message(CaseActionEnum.getActionDescForEmail(overdueAction), language));
            caseNotifyVO.setResult(cs.getResult());
            caseNotifyVO.setResultDesc(cs.getResult() == BaseConstant.ZERO ? "--" : i18nMessageUtils.message(CaseResultEnum.getResultDescForEmail(cs.getResult()), language));
            caseNotifyVO.setLastStage(lastStage);
            caseNotifyVO.setStage(cs.getStage());
            caseNotifyVO.setStageDesc(i18nMessageUtils.message(StageEnum.getStageDescForEmail(cs.getStage()), language));

            List<Integer> userIds = Lists.newArrayListWithExpectedSize(2);
            Integer hotelOperationBy = cs.getHotelOperationBy();
            if (hotelOperationBy > BaseConstant.ZERO) {
                userIds.add(hotelOperationBy);
            }
            Integer patchBy = cs.getPatchBy();
            if (patchBy > BaseConstant.ZERO) {
                userIds.add(patchBy);
            }

            // 汇总各个阶段推送日志 + 审批日志
            Long caseId = cs.getId();
            // 提醒操作时，若当前 case 所处阶段未 publish，则不查询当前阶段的推送日志
            Integer excludeStage = null;
            Long levelId = cs.getLevelId();
            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(levelId);
            if (reminder && approveLevel.getApproveIndex() == BaseConstant.ZERO) {
                excludeStage = cs.getStage();
            }
            List<CbCasePublishLog> publishLogs = cbCasePublishLogService.listLatestLog(caseId, excludeStage);
            if (CollectionUtils.isNotEmpty(publishLogs)) {
                userIds.addAll(publishLogs.stream().map(CbCasePublishLog::getNotifiedBy).toList());
            }
            List<CbCaseApproveLog> approveLogs = cbCaseApproveLogService.listByStageAndBatchId(publishLogs);
            if (CollectionUtils.isNotEmpty(approveLogs)) {
                userIds.addAll(approveLogs.stream().map(CbCaseApproveLog::getApproveBy).toList());
            }

            Map<Integer, String> userNameById = Collections.emptyMap();
            if (!userIds.isEmpty()) {
                userNameById = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);
            }

            caseNotifyVO.setHotelOperationBy(userNameById.getOrDefault(hotelOperationBy, "--"));

            String timeZone = propertyDTO.getTimezone();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();

            Long hotelOperationTime = cs.getHotelOperationTime();
            caseNotifyVO.setHotelOperationTime(hotelOperationTime > BaseConstant.ZERO ?
                    InternationalizationUtils.formatDateTime(hotelOperationTime, longDate, hourClock, timeZone, language) : "--");

            Long replyBy = cs.getReplyBy();
            caseNotifyVO.setReplyBy(replyBy > BaseConstant.ZERO ?
                    InternationalizationUtils.formatDateTime(replyBy, longDate, StringUtils.EMPTY, timeZone, language) : "--");

            caseNotifyVO.setPatchBy(userNameById.getOrDefault(patchBy, "--"));

            Long patchTime = cs.getPatchTime();
            caseNotifyVO.setPatchTime(patchTime > BaseConstant.ZERO ?
                    InternationalizationUtils.formatDateTime(patchTime, longDate, hourClock, timeZone, language) : "--");

            Long adjReplyBy = cs.getAdjReplyBy();
            caseNotifyVO.setAdjReplyBy(adjReplyBy > BaseConstant.ZERO ?
                    InternationalizationUtils.formatDateTime(adjReplyBy, longDate, StringUtils.EMPTY, timeZone, language) : "--");

            // 邮件追加内容
            CbCasePublishLog publishLog = null;
            if (approveLevel.getApproveIndex() == BaseConstant.ZERO) {
                // 邮件追加内容用于 阶段处理人 查看
                publishLog = cbCasePublishLogService.getLog(cs.getId(), cs.getBatchId());
            }

            // 渲染各个阶段的 处理人层级名称、推送人、审批层级名称、审批人
            Map<Integer, String> zeroLevelNameByStage = cbCaseApproveLevelService.listZeroLevel(cs.getPropertyId()).stream()
                    .collect(Collectors.toMap(CbCaseApproveLevel::getStage, CbCaseApproveLevel::getName));
            Map<Integer, CbCasePublishLog> publishLogByStage = publishLogs.stream().collect(Collectors.toMap(CbCasePublishLog::getStage, v -> v));
            Map<Integer, Map<Long, List<CbCaseApproveLog>>> approveLogByStage = approveLogs.stream()
                    .collect(Collectors.groupingBy(CbCaseApproveLog::getStage, Collectors.groupingBy(CbCaseApproveLog::getLevelId)));

            Context context = new Context();
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("caseVO", caseNotifyVO);
            context.setVariable("draftStageLogs", packagePublishLogs(propertyDTO, StageCons.INITIAL_CASE, zeroLevelNameByStage, publishLogByStage, approveLogByStage, userNameById));
            context.setVariable("hotelOperationsStageLogs", packagePublishLogs(propertyDTO, StageCons.HOTEL_OPERATION, zeroLevelNameByStage, publishLogByStage,  approveLogByStage, userNameById));
            context.setVariable("overdueStageLogs", packagePublishLogs(propertyDTO, StageCons.OVERDUE, zeroLevelNameByStage, publishLogByStage, approveLogByStage, userNameById));
            context.setVariable("portalStageLogs", packagePublishLogs(propertyDTO, StageCons.PORTAL, zeroLevelNameByStage, publishLogByStage, approveLogByStage, userNameById));
            context.setVariable("patchStageLogs", packagePublishLogs(propertyDTO, StageCons.PATCH, zeroLevelNameByStage, publishLogByStage, approveLogByStage, userNameById));
            context.setVariable("finalStageLogs", packagePublishLogs(propertyDTO, StageCons.FINAL, zeroLevelNameByStage, publishLogByStage, approveLogByStage, userNameById));
            context.setVariable("moreContent", Objects.isNull(publishLog) || StringUtils.isEmpty(publishLog.getMoreContent()) ?
                    StringUtils.EMPTY : publishLog.getMoreContent());
            context.setVariable("hereLink", caseLink + "?property=" + propertyDTO.getAbbr() +
                    "&tenant=" + propertyDTO.getTenantSymbol() + "&toDetail=1&caseId=" + cs.getId());
            return context;
        }

        /**
         * 渲染邮件
         *
         * @param context        Context
         * @param htmlTemplate   html 模版
         * @param language       多语言
         * @param defaultSubject 默认主题
         * @return 邮件内容
         * @author yyt
         * @date 2024/2/22
         **/
        private String renderingEmails(Context context, String htmlTemplate, String language, String defaultSubject) {
            return EmailUtil.getFullEmailContent(defaultSubject, templateEngine.process(htmlTemplate + language, context), language);
        }

        /**
         * 封装推送日志信息
         *
         * @param propertyDTO          资产
         * @param stage                阶段
         * @param zeroLevelNameByStage 0 及层级名称
         * @param publishLogByStage    推送日志
         * @param approveLogByStage    审批日志
         * @param userNameById         操作人
         * @return 推送日志信息
         * @author yyt
         * @date 2024/2/21
         **/
        private PublishLogDTO packagePublishLogs(PropertyDTO propertyDTO, int stage, Map<Integer, String> zeroLevelNameByStage,
                                          Map<Integer, CbCasePublishLog> publishLogByStage, Map<Integer, Map<Long, List<CbCaseApproveLog>>> approveLogByStage,
                                          Map<Integer, String> userNameById) {
            String language = propertyDTO.getLanguage();
            String timezone = propertyDTO.getTimezone();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            // 推送人
            CbCasePublishLog publishLog = publishLogByStage.get(stage);
            if(Objects.isNull(publishLog)){
                return new PublishLogDTO();
            }
            PublishLogDTO publishLogDTO = new PublishLogDTO();
            publishLogDTO.setLevelName(zeroLevelNameByStage.getOrDefault(stage, i18nMessageUtils.message(CaseNotifyCons.HANDLER, language)));
            publishLogDTO.setPublishUserName(userNameById.get(publishLog.getNotifiedBy()));
            publishLogDTO.setPublishTime(InternationalizationUtils.formatDateTime(publishLog.getNotifiedTime(), dateTime.getLongDate(),
                    dateTime.getHourClock(), timezone, language));
            // 多层审批
            List<PublishApproveLogsDTO> publishApproveLogs = Lists.newArrayListWithExpectedSize(approveLogByStage.size());
            Map<Long, List<CbCaseApproveLog>> approveLogsByLevelId = approveLogByStage.get(stage);
            if (Objects.nonNull(approveLogsByLevelId)) {
                Set<Long> levelIds = approveLogsByLevelId.keySet();
                levelIds.forEach(levelId -> {
                    List<CbCaseApproveLog> approveLogs = approveLogsByLevelId.get(levelId);
                    PublishApproveLogsDTO publishApproveLogsDTO = new PublishApproveLogsDTO();
                    publishApproveLogsDTO.setLevelName(approveLogs.stream().filter(item-> Objects.equals(item.getLevelId(), levelId))
                            .findFirst().get().getName());
                    // 审批人
                    publishApproveLogsDTO.setApproveLogs(approveLogs.stream().map(item -> {
                        PublishApproveLogDTO publishApproveLogDTO = new PublishApproveLogDTO();
                        publishApproveLogDTO.setUserName(userNameById.get(item.getApproveBy()));
                        publishApproveLogDTO.setOperationTime(InternationalizationUtils.formatDateTime(item.getApproveTime(),
                                dateTime.getLongDate(), dateTime.getHourClock(), timezone, language));
                        publishApproveLogDTO.setApproved(item.getIsApproved());
                        return publishApproveLogDTO;
                    }).collect(Collectors.toList()));
                    publishApproveLogs.add(publishApproveLogsDTO);
                });
            }
            if (CollectionUtils.isNotEmpty(publishApproveLogs)) {
                publishLogDTO.setApproveLogs(publishApproveLogs);
            }

            return publishLogDTO;
        }

        /**
         * 批量 case 通知用户
         *
         * @param cbCases    case
         * @author yyt
         * @date 2023/9/11
         */
        @Override
        public void notifyUserBatchCase(List<CbCase> cbCases) {
            // 获取审批层级中配置的 审批人，这一批 case 都属于同一个审批层级
            CbCase firstCase = cbCases.get(0);
            Integer propertyId = firstCase.getPropertyId();
            Long levelId = firstCase.getLevelId();
            List<CbCaseApproveLevelUser> approveLevelUsers = cbCaseApproveLevelUserService.listApproveUser(propertyId, Lists.newArrayList(levelId));

            List<Long> caseIds = cbCases.stream().map(CbCase::getId).collect(Collectors.toList());
            Map<Long, Map<Integer, Integer>> actionsByCaseId = cbCaseActionService.list(caseIds, Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE))
                    .stream().collect(Collectors.groupingBy(CbCaseAction::getCaseId,
                            Collectors.toMap(CbCaseAction::getStage, CbCaseAction::getAction)));
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(levelId);

            notify(propertyId, cbCases, approveLevelUsers, actionsByCaseId, propertyDTO, approveLevel.getCc());
        }

        /**
         * 通知
         *
         * @param propertyId        资产 id
         * @param cases             case
         * @param approveLevelUsers 审批人
         * @param actionsByCaseId   case action
         * @param propertyDTO       资产
         * @param cc                抄送人
         * @author yyt
         * @date 2023/9/11
         */
        private void notify(Integer propertyId, List<CbCase> cases, List<CbCaseApproveLevelUser> approveLevelUsers,
                            Map<Long, Map<Integer, Integer>> actionsByCaseId, PropertyDTO propertyDTO,
                            String cc) {
            if (CollectionUtils.isEmpty(approveLevelUsers)) {
                return;
            }

            Map<Integer, String> vendorById = cbCaseBankService.list(Sets.newHashSet(propertyId));
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            String language = propertyDTO.getLanguage();
            String hereLink = caseLink + "?property=" + propertyDTO.getAbbr() + "&tenant=" + propertyDTO.getTenantSymbol();

            List<CaseNotifyVO> caseVOList = cases.stream().map(item -> {
                CaseNotifyVO caseNotifyVO = new CaseNotifyVO();
                caseNotifyVO.setCaseNumber(item.getCaseNumber());
                caseNotifyVO.setVendor(vendorById.get(item.getVendor()));
                caseNotifyVO.setDisputeAmount(InternationalizationUtils.formatAmountViewValue(item.getDisputeAmount(),
                        number.getCalculationRule(), number.getCurrency()));

                Map<Integer, Integer> actionByStage = actionsByCaseId.getOrDefault(item.getId(),
                        Collections.emptyMap());
                Integer hotelAction = actionByStage.get(StageCons.HOTEL_OPERATION);
                caseNotifyVO.setHotelActionDesc(Objects.isNull(hotelAction) ? "--" : i18nMessageUtils.message(CaseActionEnum.getActionDescForEmail(hotelAction), language));
                Integer overdueAction = actionByStage.get(StageCons.OVERDUE);
                caseNotifyVO.setOverdueActionDesc(Objects.isNull(overdueAction) ? "--" : i18nMessageUtils.message(CaseActionEnum.getActionDescForEmail(overdueAction), language));
                caseNotifyVO.setResultDesc(item.getResult() == BaseConstant.ZERO ? "--" : i18nMessageUtils.message(CaseResultEnum.getResultDescForEmail(item.getResult()), language));
                // 详情链接
                caseNotifyVO.setHereLink(hereLink + "&toDetail=1&caseId=" + item.getId());
                return caseNotifyVO;
            }).toList();

            List<Integer> userIds = approveLevelUsers.stream().map(CbCaseApproveLevelUser::getUserId).collect(Collectors.toList());

            List<JSONObject> lineMsg;
            if (approveLevelUsers.get(0).getApproveIndex().equals(BaseConstant.ZERO)) {
                lineMsg = Lists.newArrayList(generateBatchNotifyLineMsg(propertyDTO, cases));
            } else {
                JSONObject msg = generatePatchApproveLineMsg(propertyDTO, cases);
                lineMsg = Lists.newArrayList(msg);
            }

            String propertyName = propertyDTO.getName();
            String functionName = i18nMessageUtils.message(FunctionCons.CHARGE_BACK, language);
            String subject = i18nMessageUtils.message(CaseNotifyCons.WAITING_FOR_PROCESSING_SUBJECT, language, functionName, propertyName);

            Context context = new Context();
            context.setVariable("propertyName", propertyName);
            context.setVariable("functionName", functionName);
            context.setVariable("caseVOList", caseVOList.stream().limit(EmailUtil.LIMIT_COUNT).toList());
            context.setVariable("count", caseVOList.size());
            context.setVariable("exceedLimit", caseVOList.size() > EmailUtil.LIMIT_COUNT);
            context.setVariable("status", i18nMessageUtils.message(CaseNotifyCons.WAITING_FOR_PROCESSING, language));
            context.setVariable("hereLink", hereLink);
            String mailContent = EmailUtil.getFullEmailContent(subject, templateEngine.process("chargeback/case-notify-batch_" + language, context), language);

            handleNotify(propertyId, FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(), subject, userIds, lineMsg, mailContent, cc);
        }

        /**
         * 创建批量 case 通知消息
         *
         * @param propertyDTO 资产
         * @param cases      case
         * @return line msg
         * @author yyt
         * @date 2023/11/28
         */
        private JSONObject generateBatchNotifyLineMsg(PropertyDTO propertyDTO, List<CbCase> cases) {
            // CPE have 10 waiting for processing cases
            JSONObject msg = new JSONObject();
            msg.put("type", "text");
            msg.put("text", propertyDTO.getName() + " have " + cases.size() + StringUtils.lowerCase(PROCESSING_SUBJECT));
            return msg;
        }

        /**
         * 创建批量 case 审批消息
         *
         * @param propertyDTO 资产
         * @param cases      case
         * @return line msg
         * @author yyt
         * @date 2023/11/28
         */
        private JSONObject generatePatchApproveLineMsg(PropertyDTO propertyDTO, List<CbCase> cases) {
            // CPE have 10 waiting for approval cases
            JSONObject msg = new JSONObject();
            msg.put("type", "text");
            msg.put("text", propertyDTO.getName() + " have " + cases.size() + StringUtils.lowerCase(APPROVE_SUBJECT));
            return msg;
        }

        /**
         * case 拒绝 通知用户
         *
         * @param cs case
         * @author yyt
         * @date 2023/9/11
         */
        @Override
        public void rejectNotifyUser(CbCase cs) {
            Integer propertyId = cs.getPropertyId();
            if (!cbCaseEmailConfigService.checkNotify(propertyId, cbCaseEmailConfigService.getTypeForStage(cs.getStage()))) {
                return;
            }

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            Context context = packageEmailContent(cs, propertyDTO, null, false);
            context.setVariable("reject", true);

            String subject = getSubject(cs, null, true);
            String content = renderingEmails(context, SINGLE_CASE_NOTIFY_HTML, propertyDTO.getLanguage(), subject);

            // 获取送审人
            CbCasePublishLog lastLog = cbCasePublishLogService.getLog(cs.getId(), cs.getBatchId());
            UserDTO user = userInternalService.getOne(propertyDTO.getTenantId(), lastLog.getNotifiedBy());
            String cc = cbCaseApproveLevelService.getById(cs.getLevelId()).getCc();

            handleNotify(cs.getPropertyId(), FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(),
                    subject,
                    Lists.newArrayList(user.getId()), Lists.newArrayList(generateApproveLineMsg(propertyDTO, cs, ApproveStatusEnum.REJECTED.getStatus())),
                    content, cc);
        }

        /**
         * 提醒处理
         *
         * @param cs      case
         * @param userIds 通知人
         * @author yyt
         * @date 2023/9/1
         */
        @Override
        public void notifyHandle(CbCase cs, List<Integer> userIds) {
            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(cs.getLevelId());
            List<JSONObject> lineMsg;
            PropertyDTO propertyDTO = propertyI18nRedisService.get(cs.getPropertyId());
            if (approveLevel.getApproveIndex().equals(BaseConstant.ZERO)) {
                lineMsg = Lists.newArrayList(generateNotifyLineMsg(propertyDTO, cs));
            } else {
                JSONObject msg = generateApproveLineMsg(propertyDTO, cs, ApproveStatusEnum.APPROVEING.getStatus());
                lineMsg = Lists.newArrayList(msg);
            }

            String subject = getNotifyHandleSubject(cs);
            Context context = packageEmailContent(cs, propertyDTO, null, true);

            context.setVariable("notifyHandle", true);
            context.setVariable("waitHandle", approveLevel.getApproveIndex() == BaseConstant.ZERO);
            String content = renderingEmails(context, SINGLE_CASE_NOTIFY_HTML, propertyDTO.getLanguage(), subject);

            handleNotify(propertyDTO.getId(), FunctionPointEnum.CASE_NOTIFY.getFunctionPoint(), subject, userIds, lineMsg, content, null);
        }

        /**
         * 获取提醒处理 subject
         *
         * @param cs case
         * @return subject
         * @author yyt
         * @date 2024/2/22
         **/
        private String getNotifyHandleSubject(CbCase cs) {
            String subject = StringUtils.EMPTY;
            PropertyDTO propertyDTO = propertyI18nRedisService.get(cs.getPropertyId());
            String caseNumber = cs.getCaseNumber();
            Integer result = cs.getResult();
            String abbr = StringUtils.upperCase(propertyDTO.getAbbr());
            String language = propertyDTO.getLanguage();
            Map<Integer, Integer> actionByStage = cbCaseActionService.list(Lists.newArrayList(cs.getId()),
                            Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE))
                    .stream().collect(Collectors.toMap(CbCaseAction::getStage, CbCaseAction::getAction));
            Integer overdueAction = actionByStage.get(StageCons.OVERDUE);
            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(cs.getLevelId());
            // true: 待处理，false：待审批
            boolean waitHandle = approveLevel.getApproveIndex() == BaseConstant.ZERO;
            switch (cs.getStage()) {
                case StageCons.INITIAL_CASE -> {
                    // CPE  Chargeback TEST002 Please Handler
                    // 待处理
                    if (waitHandle) {
                        subject = i18nMessageUtils.message(CaseNotifyCons.WAIT_HANDLE, language, abbr, caseNumber);
                    } else {
                        // 待审批
                        subject = i18nMessageUtils.message(CaseNotifyCons.WAIT_APPROVE, language, abbr, caseNumber);
                    }
                }
                case StageCons.HOTEL_OPERATION -> {
                    if (waitHandle) {
                        // CPE  Chargeback TEST002 Please Confirm
                        subject = i18nMessageUtils.message(CaseNotifyCons.WAIT_CONFIRM, language, abbr, caseNumber);
                    } else {
                        if (Objects.isNull(overdueAction)) {
                            subject = i18nMessageUtils.message(CaseNotifyCons.WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_WAIT_APPROVE, language, abbr, caseNumber);
                        }
                    }
                }
                case StageCons.OVERDUE -> {
                    // CPE Chargeback TEST004 Waiting For Approval
                    subject = i18nMessageUtils.message(CaseNotifyCons.WAIT_APPROVE, language, abbr, caseNumber);
                }
                case StageCons.PORTAL -> {
                    if(waitHandle){
                        // dispute - CPE Chargeback TEST002 Please Process
                        // overdue dispute - CPE  Chargeback TEST006 Overdue-Approved
                        subject = i18nMessageUtils.message(Objects.isNull(overdueAction) ? CaseNotifyCons.DISPUTE :
                                CaseNotifyCons.OVERDUE_HOTEL_OPERATIONS_APPROVED, language, abbr, caseNumber);
                    } else {
                        if (result == CaseResultEnum.WIN.getCode()) {
                            subject = i18nMessageUtils.message(CaseNotifyCons.WIN_WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(CaseNotifyCons.LOSS_WAIT_APPROVE, language, abbr, caseNumber);
                        }
                    }
                }
                case StageCons.PATCH -> {
                    if (waitHandle) {
                        // loss - CPE  Chargeback TEST001 Please Fill out Adjustment Form
                        // overdue loss - CPE  Chargeback TEST006 Overdue-Please Fill Out Adjustment Form
                        subject = i18nMessageUtils.message(Objects.isNull(overdueAction) ? CaseNotifyCons.LOSS :
                                CaseNotifyCons.OVERDUE_LOSS, language, abbr, caseNumber);
                    } else {
                        if (Objects.isNull(overdueAction)) {
                            subject = i18nMessageUtils.message(CaseNotifyCons.ADJ_WAIT_APPROVE, language, abbr, caseNumber);
                        } else {
                            subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_ADJ_WAIT_APPROVE, language, abbr, caseNumber);
                        }
                    }
                }
                case StageCons.FINAL -> {
                    if (waitHandle) {
                        if (Objects.isNull(overdueAction)) {
                            if (cs.getAction() == CaseActionEnum.ACCEPT.getCode()) {
                                // accept - CPE  Chargeback TEST002 Please Process Write-Off
                                subject = i18nMessageUtils.message(CaseNotifyCons.ACCEPT, language, abbr, caseNumber);
                            } else {
                                // dispute win - CPE  Chargeback TEST003 Case Win
                                // dispute loss - CPE Chargeback TEST001 Please Process Write-Off
                                subject = i18nMessageUtils.message(result == CaseResultEnum.WIN.getCode() ? CaseNotifyCons.WIN :
                                        CaseNotifyCons.ACCEPT, language, abbr, caseNumber);
                            }
                        } else {
                            if (overdueAction == CaseActionEnum.ACCEPT.getCode()) {
                                // overdue accept - CPE  Chargeback TEST004 Overdue-Approved
                                subject = i18nMessageUtils.message(CaseNotifyCons.OVERDUE_HOTEL_OPERATIONS_APPROVED, language, abbr, caseNumber);
                            } else {
                                // overdue dispute win - CPE  Chargeback TEST007 Overdue-Win
                                // overdue dispute loss - CPE  Chargeback TEST006 Overdue- Please Process Write-Off
                                subject = i18nMessageUtils.message(result == CaseResultEnum.WIN.getCode() ? CaseNotifyCons.OVERDUE_WIN :
                                        CaseNotifyCons.OVERDUE_PATCH_ACCEPT, language, abbr, caseNumber);
                            }
                        }
                    } else {
                        // CPE  Chargeback TEST002 Close Was Waiting For Approval
                        subject = i18nMessageUtils.message(CaseNotifyCons.CLOSE_WAIT_APPROVE, language, abbr, caseNumber);
                    }
                }
            }

            return subject;
        }

        /**
         * 处理发送通知
         *
         * @param propertyId    资产 id
         * @param functionPoint 功能点
         * @param subject       标题
         * @param toUserIds     to user id
         * @param lineMsg       line msg
         * @param content       邮件内容
         * @param cc            抄送人
         * @author yyt
         * @date 2023/11/28
         */
        @Override
        public void handleNotify(Integer propertyId, Integer functionPoint, String subject,
                                 List<Integer> toUserIds, List<JSONObject> lineMsg, String content, String cc) {
            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyId);
            notifyDTO.setFunctionPoint(functionPoint);
            notifyDTO.setSubject(subject);

            if(StringUtils.isEmpty(cc) && CollectionUtils.isEmpty(toUserIds)){
                return;
            }

            List<NotifyIdentityDTO> notifyIdentityDTOs = Lists.newArrayListWithExpectedSize(toUserIds.size());
            notifyIdentityDTOs.addAll(JsonUtils.convertStringToList(objectMapper, cc, NotifyIdentityDTO.class));
            notifyIdentityDTOs.forEach(item-> item.setTargetType(EmailConfigCons.TARGET_TYPE_CC));

            notifyIdentityDTOs.addAll(toUserIds.stream().map(item -> new NotifyIdentityDTO().setSource(EmailConfigCons.SOURCE_USER)
                    .setTargetType(EmailConfigCons.TARGET_TYPE_TO).setIdentityId(item).setEmail(StringUtils.EMPTY)).toList());
            notifyDTO.setIdentityList(notifyIdentityDTOs);

            notifyDTO.setLineContent(lineMsg);

            notifyDTO.setEmailContent(content);
            notifyInternalService.sendNotify(notifyDTO);
        }

    }