# EndOfShiftServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.FunctionCons;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    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.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.PrecisionCons;
    import cloud.wconnect.frontoffice.cons.endofshift.*;
    import cloud.wconnect.frontoffice.entity.endofshift.*;
    import cloud.wconnect.frontoffice.pojo.dto.EosShiftDetailDTO;
    import cloud.wconnect.frontoffice.pojo.dto.endofshift.detail.*;
    import cloud.wconnect.frontoffice.pojo.dto.endofshift.notify.*;
    import cloud.wconnect.frontoffice.service.endofshift.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    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.context.annotation.Lazy;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import org.thymeleaf.TemplateEngine;
    import org.thymeleaf.context.Context;

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

    /**
     * End of Shift 报告业务层
     */
    @Service
    @Slf4j
    @DS("ratelog_manage")
    public class EndOfShiftServiceImpl implements IEndOfShiftService {

        @Autowired
        private IShiftReportInfoService shiftReportInfoService;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private IShiftDigitalCheckInService shiftDigitalCheckInService;
        @Autowired
        private IEosEmailConfigService eosEmailConfigService;
        @Autowired
        private IShiftComplaintRequestService shiftComplaintRequestService;
        @Autowired
        private IShiftWakeupCallService shiftWakeupCallService;
        @Autowired
        private IShiftOperationRecordService shiftOperationRecordService;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private TemplateEngine templateEngine;
        @Value("${here.link.end-of-shift-report}")
        private String hereLink;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private NotifyInternalService notifyInternalService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private IEosEmailTargetService emailTargetService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        @Lazy
        private IEosShiftConfigService shiftConfigService;

        /**
         * 查询排班详情
         *
         * @param propertyId      资产id
         * @param shiftReportInfo 排班详情
         * @return cloud.wconnect.frontoffice.pojo.dto.endofshift.detail.ShiftReportDetailDTO
         * @author lichen
         * @date 2025/6/19
         */
        @Override
        public ShiftReportDetailDTO getShiftReportInfoDetail(Integer propertyId, ShiftReportInfo shiftReportInfo) {
            //获取排班数据对应的排班配置
            EosShiftConfig shiftConfig = shiftConfigService.getById(shiftReportInfo.getShiftConfigId());
            //获取排班数据对应的班次信息
            Map<Integer, EosShiftDetailDTO> shift2ShiftDetailDTO = JsonUtils.convertStringToList(objectMapper, shiftConfig.getShiftConfig(), EosShiftDetailDTO.class)
                    .stream().collect(Collectors.toMap(EosShiftDetailDTO::getShift, Function.identity()));
            EosShiftDetailDTO shiftDetailDTO = shift2ShiftDetailDTO.get(shiftReportInfo.getShift());

            // 用户id集合
            Set<Integer> userIds = Sets.newHashSet();

            // 排班详情DTO
            ShiftReportDetailDTO shiftReportDetailDTO = new ShiftReportDetailDTO();
            shiftReportDetailDTO.setId(shiftReportInfo.getId());
            shiftReportDetailDTO.setTargetDate(shiftReportInfo.getTargetDate());
            shiftReportDetailDTO.setShiftName(shiftDetailDTO.getShiftName());
            shiftReportDetailDTO.setStartTime(shiftDetailDTO.getStartTime());
            shiftReportDetailDTO.setEndTime(shiftDetailDTO.getEndTime());
            shiftReportDetailDTO.setCanSend(shiftReportInfo.getCanSend());
            shiftReportDetailDTO.setSendBy(shiftReportInfo.getSendBy());
            shiftReportDetailDTO.setSendTime(shiftReportInfo.getSendTime());

            //收集操作人
            userIds.add(shiftReportInfo.getSendBy());

            // info sheet 信息
            shiftReportDetailDTO.setInfoSheet(beanMapper.map(shiftReportInfo, ShiftInfoSheetDetailDTO.class));

            // Incidents/Property Issues 信息
            if (StringUtils.isNotBlank(shiftReportInfo.getIssuesJson())) {
                ShiftIncidentsPropertyIssuesDetailDTO shiftIncidentsPropertyIssuesDetailDTO = JsonUtils.convertStringToObject(objectMapper, shiftReportInfo.getIssuesJson(),
                        ShiftIncidentsPropertyIssuesDetailDTO.class);
                shiftReportDetailDTO.setIncidentsPropertyIssues(shiftIncidentsPropertyIssuesDetailDTO);
            }

            // Enrollment Total 信息
            if (StringUtils.isNotBlank(shiftReportInfo.getEnrollmentJson())) {
                ShiftEnrollmentDetailDTO shiftEnrollmentDetailDTO = JsonUtils.convertStringToObject(objectMapper, shiftReportInfo.getEnrollmentJson(), ShiftEnrollmentDetailDTO.class);
                shiftReportDetailDTO.setEnrollment(shiftEnrollmentDetailDTO);
            }

            // Sales Resume 信息
            if (StringUtils.isNotBlank(shiftReportInfo.getResumeJson())) {
                ShiftResumeDetailDTO shiftResumeDetailDTO = JsonUtils.convertStringToObject(objectMapper, shiftReportInfo.getResumeJson(), ShiftResumeDetailDTO.class);
                shiftReportDetailDTO.setResume(shiftResumeDetailDTO);
            }

            // Pass On 信息
            if (StringUtils.isNotBlank(shiftReportInfo.getPassOnJson())) {
                ShiftPassOnDetailDTO shiftPassOnDetailDTO = JsonUtils.convertStringToObject(objectMapper, shiftReportInfo.getPassOnJson(), ShiftPassOnDetailDTO.class);
                shiftReportDetailDTO.setPassOn(shiftPassOnDetailDTO);
            }

            // Digital Check-in 信息
            List<ShiftDigitalCheckIn> shiftDigitalCheckInList = shiftDigitalCheckInService.listByShiftId(shiftReportInfo.getId());
            List<ShiftDigitalCheckInDetailDTO> shiftDigitalCheckInDetailDTOS = beanMapper.mapList(shiftDigitalCheckInList, ShiftDigitalCheckInDetailDTO.class, (entity, dto) -> {
                dto.setUid(entity.getOperateUid());
                dto.setUpdateTime(entity.getOperateTime());
            });
            shiftReportDetailDTO.setDigitalCheckIn(shiftDigitalCheckInDetailDTOS);
            //收集操作人
            if (CollectionUtils.isNotEmpty(shiftDigitalCheckInDetailDTOS)) {
                userIds.addAll(shiftDigitalCheckInDetailDTOS.stream().map(ShiftDigitalCheckInDetailDTO::getUid).toList());
            }

            // Guest Complaint 信息
            List<ShiftComplaintRequest> shiftComplaintRequestList = shiftComplaintRequestService.listByShiftId(shiftReportInfo.getId());
            List<ShiftGuestComplaintDetailDTO> shiftGuestComplaintDetailDTOS = beanMapper.mapList(shiftComplaintRequestList, ShiftGuestComplaintDetailDTO.class, (entity, dto) -> {
                if (EndOfShiftCons.SHIFT_COMPLAINT_TYPE == entity.getType()) {
                    dto.setUid(entity.getOperateUid());
                    dto.setUpdateTime(entity.getOperateTime());
                }
            });
            shiftReportDetailDTO.setComplaint(shiftGuestComplaintDetailDTOS);
            //收集操作人
            if (CollectionUtils.isNotEmpty(shiftGuestComplaintDetailDTOS)) {
                userIds.addAll(shiftGuestComplaintDetailDTOS.stream().map(ShiftGuestComplaintDetailDTO::getUid).toList());
            }

            // Wakeup Call 信息,alarmTime升序排序
            List<ShiftWakeupCall> shiftWakeupCallList = shiftWakeupCallService.listByShiftId(shiftReportInfo.getId());
            List<ShiftWakeupCallDetailDTO> shiftWakeupCallDetailDTOS = beanMapper.mapList(shiftWakeupCallList, ShiftWakeupCallDetailDTO.class, (entity, dto) -> {
                dto.setAlarm(entity.getAlarmTime());
            });
            shiftReportDetailDTO.setWakeUpCall(shiftWakeupCallDetailDTOS);

            //填充操作人姓名
            return populateUserName(propertyId, userIds, shiftReportDetailDTO);
        }

        /**
         * 填充操作人姓名
         *
         * @param propertyId           资产id
         * @param userIds              用户id集合
         * @param shiftReportDetailDTO 排班详情DTO
         * @return cloud.wconnect.frontoffice.pojo.dto.endofshiftreport.detail.ShiftReportDetailDTO
         * @author lichen
         * @date 2024/1/10
         */
        private ShiftReportDetailDTO populateUserName(Integer propertyId, Set<Integer> userIds, ShiftReportDetailDTO shiftReportDetailDTO) {
            //删除0
            userIds.remove(0);
            if (CollectionUtils.isEmpty(userIds)) return shiftReportDetailDTO;

            //查询用户名称
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), userIds);

            //设置发送人名称
            shiftReportDetailDTO.setSendName(userId2Name.get(shiftReportDetailDTO.getSendBy()));

            //设置Digital check-in区域操作人名称
            List<ShiftDigitalCheckInDetailDTO> digitalCheckInDetailDTOS = shiftReportDetailDTO.getDigitalCheckIn();
            digitalCheckInDetailDTOS.forEach(dto -> dto.setUpdateBy(userId2Name.get(dto.getUid())));

            //设置Guest complaint区域操作人名称
            List<ShiftGuestComplaintDetailDTO> guestComplaintDetailDTOS = shiftReportDetailDTO.getComplaint();
            guestComplaintDetailDTOS.forEach(dto -> dto.setUpdateBy(userId2Name.get(dto.getUid())));

            return shiftReportDetailDTO;
        }

        /**
         * 发送排班信息通知
         *
         * @param propertyDTO     资产
         * @param shiftReportInfo 排班基础信息
         * @param loginUserId     登录用户id
         * @return void
         * @author lichen
         * @date 2025/6/20
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void sendNotify(PropertyDTO propertyDTO, ShiftReportInfo shiftReportInfo, int loginUserId) {
            Boolean canSend = shiftReportInfo.getCanSend();
            if (!canSend) {
                throw new BadRequestException(EndOfShiftErrorI18nConstant.EMAIL_ALREADY_SEND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "The email has been sent. Do not send it again ; " +
                        "shiftId:" + shiftReportInfo.getId());
            }
            //获取邮件配置
            EosEmailConfig emailConfig = eosEmailConfigService.getOne(new LambdaQueryWrapper<EosEmailConfig>()
                    .eq(EosEmailConfig::getPropertyId, propertyDTO.getId())
                    .eq(EosEmailConfig::getType, EndOfShiftNotifyConfigTypeEnum.END_OF_SHIFT_DETAIL.getType()));
            if (Objects.isNull(emailConfig)) return;
            if (!emailConfig.getStatus()) {
                log.info("End of Shift sendNotify end, email config status is false, propertyId:{}", propertyDTO.getId());
                return;
            }

            List<EosEmailTarget> emailTargets = emailTargetService.listByConfigId(Lists.newArrayList(emailConfig.getId()));
            if (CollectionUtils.isEmpty(emailTargets)) return;

            //获取排班数据对应的排班配置
            EosShiftConfig shiftConfig = shiftConfigService.getById(shiftReportInfo.getShiftConfigId());
            //获取排班数据对应的班次信息
            Map<Integer, EosShiftDetailDTO> shift2ShiftDetailDTO = JsonUtils.convertStringToList(objectMapper, shiftConfig.getShiftConfig(), EosShiftDetailDTO.class)
                    .stream().collect(Collectors.toMap(EosShiftDetailDTO::getShift, Function.identity()));
            EosShiftDetailDTO shiftDetailDTO = shift2ShiftDetailDTO.get(shiftReportInfo.getShift());

            //转化为邮件DTO
            Map<Integer, ShiftReportNotifyDTO> shiftId2ShiftReportNotifyDTO = convertEmailDTO(propertyDTO, Lists.newArrayList(shiftReportInfo));
            ShiftReportNotifyDTO shiftReportNotifyDTO = shiftId2ShiftReportNotifyDTO.get(shiftReportInfo.getId());

            //发送通知
            doSendNotify(propertyDTO, emailConfig, emailTargets, shiftReportInfo, shiftReportNotifyDTO, shiftDetailDTO);

            //更新通知人、通知时间;是否可发送邮件更新为false
            ShiftReportInfo updateContent = new ShiftReportInfo();
            updateContent.setSendBy(loginUserId);
            updateContent.setSendTime(DateTimeUtils.nowTimestamp());
            updateContent.setCanSend(false);
            updateContent.setId(shiftReportInfo.getId());
            updateContent.setSendTimes(shiftReportInfo.getSendTimes() + 1);
            shiftReportInfoService.updateById(updateContent);
        }

        /**
         * 转化为邮件DTO
         *
         * @param propertyDTO      资产DTO
         * @param shiftReportInfos 排班集合
         * @return java.util.Map<java.lang.Integer, cloud.wconnect.frontoffice.pojo.dto.endofshiftreport.notify.ShiftReportNotifyDTO>
         * @author lichen
         * @date 2024/1/10
         */
        public Map<Integer, ShiftReportNotifyDTO> convertEmailDTO(PropertyDTO propertyDTO, List<ShiftReportInfo> shiftReportInfos) {
            String language = propertyDTO.getLanguage();
            String timezone = propertyDTO.getTimezone();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();
            String hourClock = dateTime.getHourClock();
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            String calculationRule = number.getCalculationRule();
            String i18nYes = i18nMessageUtils.message(I18nConstant.YES, language);
            String i18nNo = i18nMessageUtils.message(I18nConstant.NO, language);

            if (CollectionUtils.isEmpty(shiftReportInfos)) return Collections.emptyMap();

            //排班信息
            Map<Integer, ShiftReportNotifyDTO> shiftId2NotifyDTO = Maps.newHashMapWithExpectedSize(shiftReportInfos.size());
            //用户集合
            Set<Integer> uidSets = Sets.newHashSetWithExpectedSize(shiftReportInfos.size());
            //排班主键ID集合
            List<Integer> shiftIds = shiftReportInfos.stream().map(ShiftReportInfo::getId).collect(Collectors.toList());

            //获取 Digital Check-in 信息
            List<ShiftDigitalCheckIn> shiftDigitalCheckIns = shiftDigitalCheckInService.list(new LambdaQueryWrapper<ShiftDigitalCheckIn>()
                    .in(ShiftDigitalCheckIn::getShiftId, shiftIds));
            Map<Integer, List<ShiftDigitalCheckIn>> checkInsByShiftId = shiftDigitalCheckIns.stream()
                    .collect(Collectors.groupingBy(ShiftDigitalCheckIn::getShiftId));
            //统计操作人id
            Set<Integer> userIdsByDigitalCheckIn = shiftDigitalCheckIns.stream().map(ShiftDigitalCheckIn::getOperateUid).collect(Collectors.toSet());
            if (CollectionUtils.isNotEmpty(userIdsByDigitalCheckIn)) {
                uidSets.addAll(userIdsByDigitalCheckIn);
            }

            //获取 Guest Complaint 信息
            List<ShiftComplaintRequest> shiftComplaintRequests = shiftComplaintRequestService.list(new LambdaQueryWrapper<ShiftComplaintRequest>()
                    .in(ShiftComplaintRequest::getShiftId, shiftIds));
            Map<Integer, List<ShiftComplaintRequest>> complaintByShiftId = shiftComplaintRequests.stream()
                    .collect(Collectors.groupingBy(ShiftComplaintRequest::getShiftId));
            //统计操作人id
            Set<Integer> userIdsByShiftComplaintRequests = shiftComplaintRequests.stream().map(ShiftComplaintRequest::getOperateUid).collect(Collectors.toSet());
            if (CollectionUtils.isNotEmpty(userIdsByShiftComplaintRequests)) {
                uidSets.addAll(userIdsByShiftComplaintRequests);
            }

            // 获取 Wakeup Call 信息
            List<ShiftWakeupCall> shiftWakeupCalls = shiftWakeupCallService.list(new LambdaQueryWrapper<ShiftWakeupCall>().in(ShiftWakeupCall::getShiftId, shiftIds));
            Map<Integer, List<ShiftWakeupCall>> wakeupCallsByShiftId = shiftWakeupCalls.stream().collect(Collectors.groupingBy(ShiftWakeupCall::getShiftId));
            //统计操作人id
            Set<Integer> userIdsByWakeupCall = shiftWakeupCalls.stream().map(ShiftWakeupCall::getOperateUid).collect(Collectors.toSet());
            if (CollectionUtils.isNotEmpty(userIdsByWakeupCall)) {
                uidSets.addAll(userIdsByWakeupCall);
            }

            //查询操作记录
            List<ShiftOperationRecord> operationRecords = shiftOperationRecordService.list(new LambdaQueryWrapper<ShiftOperationRecord>()
                    .in(ShiftOperationRecord::getShiftId, shiftIds));
            Map<Integer, Map<Integer, List<ShiftOperationRecord>>> operationRecordsByShiftId = operationRecords.stream()
                    .collect(Collectors.groupingBy(ShiftOperationRecord::getShiftId, Collectors.groupingBy(ShiftOperationRecord::getOperationType)));
            //统计操作人 id
            List<Integer> userIdsByOperationRecord = operationRecords.stream().map(ShiftOperationRecord::getOperationBy).collect(Collectors.toList());
            if (CollectionUtils.isNotEmpty(userIdsByOperationRecord)) {
                uidSets.addAll(userIdsByOperationRecord);
            }

            //获取操作人名称
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(propertyDTO.getTenantId(), uidSets);

            //转化成邮件DTO
            for (ShiftReportInfo shiftReportInfo : shiftReportInfos) {

                //获取当前 shift 对应的操作记录
                Map<Integer, List<ShiftOperationRecord>> operationRecordsByType = operationRecordsByShiftId.getOrDefault(shiftReportInfo.getId(), Collections.emptyMap());

                ShiftReportNotifyDTO notifyDTO = new ShiftReportNotifyDTO();
                //包装 info sheet 信息
                ShiftInfoSheetNotifyDTO shiftInfoSheetDTO = beanMapper.map(shiftReportInfo, ShiftInfoSheetNotifyDTO.class);
                shiftInfoSheetDTO.setUid(shiftReportInfo.getOperateUid());
                shiftInfoSheetDTO.setUpdateTime(shiftReportInfo.getOperateTime());

                //把目标日期转换成国际化的日期格式
                LocalDate targetLocalDate = DateTimeUtils.strToLocalDate(shiftReportInfo.getTargetDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                shiftInfoSheetDTO.setTargetDateStr(DateTimeUtils.localDateToStr(targetLocalDate, longDate));

                // 设置 info shift 对应的操作记录
                List<ShiftOperationRecord> infoSheetRecords = operationRecordsByType.get(ShiftTypeEnum.INFO_SHEET.getCode());
                if (CollectionUtils.isNotEmpty(infoSheetRecords)) {
                    infoSheetRecords.sort(Comparator.comparing(ShiftOperationRecord::getOperationTime).reversed());
                    shiftInfoSheetDTO.setOperationRecords(beanMapper.mapList(infoSheetRecords, ShiftOperationRecordNotifyDTO.class,
                            item -> {
                                item.setOperationName(userId2Name.get(item.getOperationBy()));
                                item.setOperationTimeStr(InternationalizationUtils.formatDateTime(item.getOperationTime(),
                                        longDate, hourClock, timezone, language));
                            }));
                }

                //国际化occ字段
                shiftInfoSheetDTO.setExpOccStr(InternationalizationUtils.formatViewValue(shiftInfoSheetDTO.getExpOcc(), PrecisionCons.END_OF_SHIFT_PRECISION,
                        calculationRule, ""));
                notifyDTO.setInfoSheet(shiftInfoSheetDTO);

                // 包装 Incidents/Property Issues 信息
                String issuesJson = shiftReportInfo.getIssuesJson();
                if (StringUtils.isNotBlank(issuesJson)) {
                    ShiftIncidentsPropertyIssuesNotifyDTO shiftIncidentsPropertyIssuesNotifyDTO = JsonUtils.convertStringToObject(objectMapper, issuesJson,
                            ShiftIncidentsPropertyIssuesNotifyDTO.class);
                    //替换换行符
                    shiftIncidentsPropertyIssuesNotifyDTO.setIssues(EmailUtil.changeLine(shiftIncidentsPropertyIssuesNotifyDTO.getIssues()));

                    // 设置 issues 对应的操作记录
                    List<ShiftOperationRecord> issuesRecords = operationRecordsByType.get(ShiftTypeEnum.ISSUES.getCode());
                    if (issuesRecords != null) {
                        issuesRecords.sort(Comparator.comparing(ShiftOperationRecord::getOperationTime).reversed());
                        shiftIncidentsPropertyIssuesNotifyDTO.setOperationRecords(beanMapper.mapList(issuesRecords, ShiftOperationRecordNotifyDTO.class,
                                item -> {
                                    item.setOperationName(userId2Name.get(item.getOperationBy()));
                                    item.setOperationTimeStr(InternationalizationUtils.formatDateTime(item.getOperationTime(),
                                            longDate, hourClock, timezone, language));
                                }
                        ));
                    }
                    notifyDTO.setIncidentsPropertyIssues(shiftIncidentsPropertyIssuesNotifyDTO);
                }

                // 包装 Enrollment Total 信息
                String enrollmentJson = shiftReportInfo.getEnrollmentJson();
                if (StringUtils.isNotBlank(enrollmentJson)) {
                    ShiftEnrollmentNotifyDTO shiftEnrollmentNotifyDTO = JsonUtils.convertStringToObject(objectMapper, enrollmentJson, ShiftEnrollmentNotifyDTO.class);

                    // 设置 enrollment 对应的操作记录
                    List<ShiftOperationRecord> enrollmentRecords = operationRecordsByType.get(ShiftTypeEnum.ENROLLMENT.getCode());
                    if (enrollmentRecords != null) {
                        enrollmentRecords.sort(Comparator.comparing(ShiftOperationRecord::getOperationTime).reversed());
                        shiftEnrollmentNotifyDTO.setOperationRecords(beanMapper.mapList(enrollmentRecords, ShiftOperationRecordNotifyDTO.class,
                                item -> {
                                    item.setOperationName(userId2Name.get(item.getOperationBy()));
                                    item.setOperationTimeStr(InternationalizationUtils.formatDateTime(item.getOperationTime(),
                                            longDate, hourClock, timezone, language));
                                }
                        ));
                    }
                    notifyDTO.setEnrollment(shiftEnrollmentNotifyDTO);
                }
                // 包装 Sales Resume 信息
                String resumeJson = shiftReportInfo.getResumeJson();
                if (StringUtils.isNotBlank(resumeJson)) {
                    ShiftResumeNotifyDTO shiftResumeNotifyDTO = JsonUtils.convertStringToObject(objectMapper, resumeJson, ShiftResumeNotifyDTO.class);
                    //替换换行符
                    shiftResumeNotifyDTO.setContent(EmailUtil.changeLine(shiftResumeNotifyDTO.getContent()));

                    // 设置 resume 对应的操作记录
                    List<ShiftOperationRecord> resumeRecords = operationRecordsByType.get(ShiftTypeEnum.SALES_RESUME.getCode());
                    if (resumeRecords != null) {
                        resumeRecords.sort(Comparator.comparing(ShiftOperationRecord::getOperationTime).reversed());
                        shiftResumeNotifyDTO.setOperationRecords(beanMapper.mapList(resumeRecords, ShiftOperationRecordNotifyDTO.class,
                                item -> {
                                    item.setOperationName(userId2Name.get(item.getOperationBy()));
                                    item.setOperationTimeStr(InternationalizationUtils.formatDateTime(item.getOperationTime(),
                                            longDate, hourClock, timezone, language));
                                }
                        ));
                    }
                    notifyDTO.setResume(shiftResumeNotifyDTO);
                }
                // 包装 Pass On 信息
                String passOnJson = shiftReportInfo.getPassOnJson();
                if (StringUtils.isNotBlank(passOnJson)) {
                    ShiftPassOnNotifyDTO shiftPassOnNotifyDTO = JsonUtils.convertStringToObject(objectMapper, passOnJson, ShiftPassOnNotifyDTO.class);

                    //设置passOn的内容
                    setPassOnContent(propertyDTO, shiftPassOnNotifyDTO);

                    // 设置 pass on 对应的操作记录
                    List<ShiftOperationRecord> passOnRecords = operationRecordsByType.get(ShiftTypeEnum.PASS_ON.getCode());
                    if (passOnRecords != null) {
                        passOnRecords.sort(Comparator.comparing(ShiftOperationRecord::getOperationTime).reversed());
                        shiftPassOnNotifyDTO.setOperationRecords(beanMapper.mapList(passOnRecords, ShiftOperationRecordNotifyDTO.class,
                                item -> {
                                    item.setOperationName(userId2Name.get(item.getOperationBy()));
                                    item.setOperationTimeStr(InternationalizationUtils.formatDateTime(item.getOperationTime(),
                                            longDate, hourClock, timezone, language));
                                }
                        ));
                    }
                    notifyDTO.setPassOn(shiftPassOnNotifyDTO);
                }

                // 获取 Digital Check-in 信息
                List<ShiftDigitalCheckIn> shiftDigitalCheckInList = checkInsByShiftId.getOrDefault(shiftReportInfo.getId(), Collections.emptyList());
                // 包装 Digital Check-in 信息
                List<ShiftDigitalCheckInNotifyDTO> digitalCheckInDTOList = Lists.newArrayListWithExpectedSize(shiftDigitalCheckInList.size());
                shiftDigitalCheckInList.forEach(shiftDigitalCheckIn -> {
                    ShiftDigitalCheckInNotifyDTO shiftDigitalCheckInNotifyDTO = beanMapper.map(shiftDigitalCheckIn, ShiftDigitalCheckInNotifyDTO.class);
                    Integer operateUid = shiftDigitalCheckIn.getOperateUid();
                    shiftDigitalCheckInNotifyDTO.setUid(operateUid);
                    shiftDigitalCheckInNotifyDTO.setUpdateBy(userId2Name.get(operateUid));
                    shiftDigitalCheckInNotifyDTO.setUpdateTimeStr(InternationalizationUtils.formatDateTime(shiftDigitalCheckIn.getOperateTime(),
                            longDate, hourClock, timezone, language));

                    shiftDigitalCheckInNotifyDTO.setConcertoDesc(shiftDigitalCheckIn.getConcerto() ? i18nYes : i18nNo);
                    shiftDigitalCheckInNotifyDTO.setPreKeyDesc(shiftDigitalCheckIn.getPreKey() ? i18nYes : i18nNo);
                    shiftDigitalCheckInNotifyDTO.setPhysicalDesc(shiftDigitalCheckIn.getPhysical() ? i18nYes : i18nNo);

                    digitalCheckInDTOList.add(shiftDigitalCheckInNotifyDTO);
                });
                notifyDTO.setDigitalCheckIn(digitalCheckInDTOList);

                // 获取 Guest Complaint 信息
                List<ShiftComplaintRequest> shiftComplaintRequestList = complaintByShiftId.getOrDefault(shiftReportInfo.getId(), Collections.emptyList());
                // 包装 Guest Complaint 信息
                List<ShiftGuestComplaintNotifyDTO> shiftGuestComplaintDTOList = Lists.newArrayListWithExpectedSize(shiftComplaintRequestList.size());
                shiftComplaintRequestList.forEach(shiftComplaintRequest -> {
                    int shiftComplaintRequestType = shiftComplaintRequest.getType();
                    if (EndOfShiftCons.SHIFT_COMPLAINT_TYPE == shiftComplaintRequestType) {
                        // Guest Complaint 信息
                        ShiftGuestComplaintNotifyDTO shiftGuestComplaintNotifyDTO = beanMapper.map(shiftComplaintRequest, ShiftGuestComplaintNotifyDTO.class);
                        Integer operateUid = shiftComplaintRequest.getOperateUid();
                        shiftGuestComplaintNotifyDTO.setUid(operateUid);
                        shiftGuestComplaintNotifyDTO.setUpdateBy(userId2Name.get(operateUid));
                        shiftGuestComplaintNotifyDTO.setUpdateTimeStr(InternationalizationUtils.formatDateTime(shiftComplaintRequest.getOperateTime(),
                                longDate, hourClock, timezone, language));
                        //替换换行符
                        shiftGuestComplaintNotifyDTO.setQuestion(EmailUtil.changeLine(shiftGuestComplaintNotifyDTO.getQuestion()));
                        shiftGuestComplaintNotifyDTO.setAnswer(EmailUtil.changeLine(shiftGuestComplaintNotifyDTO.getAnswer()));
                        shiftGuestComplaintDTOList.add(shiftGuestComplaintNotifyDTO);
                    }
                });
                notifyDTO.setComplaint(shiftGuestComplaintDTOList);

                // 获取 Wakeup Call 信息，按照 alarmTime 升序排序
                List<ShiftWakeupCall> shiftWakeupCallList = wakeupCallsByShiftId.getOrDefault(shiftReportInfo.getId(), Collections.emptyList())
                        .stream().sorted(Comparator.comparing(ShiftWakeupCall::getAlarmTime)).toList();
                // 包装 Wakeup Call 信息
                List<ShiftWakeupCallNotifyDTO> shiftWakeupCallDTOList = Lists.newArrayListWithExpectedSize(shiftWakeupCallList.size());
                shiftWakeupCallList.forEach(shiftWakeupCall -> {
                    // Wakeup Call 信息
                    ShiftWakeupCallNotifyDTO shiftWakeupCallNotifyDTO = beanMapper.map(shiftWakeupCall, ShiftWakeupCallNotifyDTO.class);
                    shiftWakeupCallNotifyDTO.setAlarmStr(InternationalizationUtils.formatDateTime(shiftWakeupCall.getAlarmTime(),
                            longDate, hourClock, timezone, language));
                    Integer operateUid = shiftWakeupCall.getOperateUid();
                    shiftWakeupCallNotifyDTO.setUid(operateUid);
                    shiftWakeupCallNotifyDTO.setUpdateBy(userId2Name.get(operateUid));
                    shiftWakeupCallNotifyDTO.setUpdateTimeStr(InternationalizationUtils.formatDateTime(shiftWakeupCall.getOperateTime(),
                            longDate, hourClock, timezone, language));

                    //是否需要电话叫醒
                    shiftWakeupCallNotifyDTO.setNeedPhoneDesc(shiftWakeupCall.getSetPhone() ? i18nYes : i18nNo);

                    //是否已处理
                    String i18nProcessed = i18nMessageUtils.message(EndOfShiftFieldI18nConstant.I18N_PROCESSED, language);
                    String i18nUntreated = i18nMessageUtils.message(EndOfShiftFieldI18nConstant.I18N_UNTREATED, language);
                    shiftWakeupCallNotifyDTO.setStatusDesc(shiftWakeupCall.getStatus() ? i18nProcessed : i18nUntreated);

                    shiftWakeupCallDTOList.add(shiftWakeupCallNotifyDTO);
                });
                notifyDTO.setWakeUpCall(shiftWakeupCallDTOList);
                shiftId2NotifyDTO.put(shiftReportInfo.getId(), notifyDTO);
            }
            return shiftId2NotifyDTO;
        }

        /**
         * 设置PassOn的内容
         *
         * @param propertyDTO          资产DTO
         * @param shiftPassOnNotifyDTO passOn DTO
         * @return void
         * @author lichen
         * @date 2024/1/10
         */
        private void setPassOnContent(PropertyDTO propertyDTO, ShiftPassOnNotifyDTO shiftPassOnNotifyDTO) {
            String i18nComplete = i18nMessageUtils.message(EndOfShiftFieldI18nConstant.I18N_COMPLETE, propertyDTO.getLanguage());
            String i18nUnComplete = i18nMessageUtils.message(EndOfShiftFieldI18nConstant.I18N_UNCOMPLETE, propertyDTO.getLanguage());

            String fd = shiftPassOnNotifyDTO.getFd();
            if (StringUtils.isNotBlank(fd)) {
                List<PassOnContentNotifyDTO> fdList = JsonUtils.convertStringToList(objectMapper, fd, PassOnContentNotifyDTO.class);
                fdList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setFdList(fdList);
            }

            String manager = shiftPassOnNotifyDTO.getManager();
            if (StringUtils.isNotBlank(manager)) {
                List<PassOnContentNotifyDTO> managerList = JsonUtils.convertStringToList(objectMapper, manager, PassOnContentNotifyDTO.class);
                managerList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setManagerList(managerList);
            }

            String hk = shiftPassOnNotifyDTO.getHk();
            if (StringUtils.isNotBlank(hk)) {
                List<PassOnContentNotifyDTO> hkList = JsonUtils.convertStringToList(objectMapper, hk, PassOnContentNotifyDTO.class);
                hkList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setHkList(hkList);
            }

            String maintenance = shiftPassOnNotifyDTO.getMaintenance();
            if (StringUtils.isNotBlank(maintenance)) {
                List<PassOnContentNotifyDTO> maintenanceList = JsonUtils.convertStringToList(objectMapper, maintenance, PassOnContentNotifyDTO.class);
                maintenanceList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setMaintenanceList(maintenanceList);
            }

            String sales = shiftPassOnNotifyDTO.getSales();
            if (StringUtils.isNotBlank(sales)) {
                List<PassOnContentNotifyDTO> salesList = JsonUtils.convertStringToList(objectMapper, sales, PassOnContentNotifyDTO.class);
                salesList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setSalesList(salesList);
            }

            String accounting = shiftPassOnNotifyDTO.getAccounting();
            if (StringUtils.isNotBlank(accounting)) {
                List<PassOnContentNotifyDTO> accountingList = JsonUtils.convertStringToList(objectMapper, accounting, PassOnContentNotifyDTO.class);
                accountingList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setAccountingList(accountingList);
            }

            String fb = shiftPassOnNotifyDTO.getFb();
            if (StringUtils.isNotBlank(fb)) {
                List<PassOnContentNotifyDTO> fbList = JsonUtils.convertStringToList(objectMapper, fb, PassOnContentNotifyDTO.class);
                fbList.forEach(item -> item.setStatusDesc(item.getStatus() ? i18nComplete : i18nUnComplete));
                shiftPassOnNotifyDTO.setFbList(fbList);
            }
        }

        /**
         * 发送通知
         *
         * @param propertyDTO          资产信息
         * @param emailConfig          邮件配置
         * @param emailTargets         通知对象集合
         * @param shiftReportInfo      排班信息
         * @param shiftReportNotifyDTO 排班邮件DTO
         * @param shiftDetailDTO       班次信息
         * @return void
         * @author lichen
         * @date 2023/11/25
         */
        public void doSendNotify(PropertyDTO propertyDTO, EosEmailConfig emailConfig, List<EosEmailTarget> emailTargets,
                                 ShiftReportInfo shiftReportInfo, ShiftReportNotifyDTO shiftReportNotifyDTO, EosShiftDetailDTO shiftDetailDTO) {
            String language = propertyDTO.getLanguage();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();

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

            //跳转链接
            String urlParams = MessageFormat.format("?property={0}&tenant={1}&endOfShiftId={2}&date={3}",
                    propertyDTO.getAbbr(), propertyDTO.getTenantSymbol(), shiftReportInfo.getId().toString(), shiftReportInfo.getTargetDate().toString());
            String hereLineWithParam = hereLink + urlParams;

            //主题中的后缀：是否修改过
            String reversed = shiftReportInfo.getSendTimes() > 0 ? " - " + i18nMessageUtils.message(EndOfShiftFieldI18nConstant.I18N_REVISED, language) : "";
            //根据国际化配置处理排班日期
            String formatTargetDate = DateTimeUtils.localDateToStr(DateTimeUtils.strToLocalDate(shiftReportInfo.getTargetDate().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD),
                    longDate);
            //主题的中间部分
            String middlePartOfSubject = shiftDetailDTO.getShiftName() + "(" + formatTargetDate + ")" + reversed;
            //获取默认主题
            String defaultSubjectI18nCode = EndOfShiftNotifyConfigTypeEnum.getDefaultSubjectI18nCode(emailConfig.getType());
            String defaultSubject = i18nMessageUtils.message(defaultSubjectI18nCode, propertyDTO.getLanguage(), functionName, middlePartOfSubject, propertyDTO.getName());
            //自定义主体优先，其次默认主题
            String subject = StringUtils.isNotBlank(emailConfig.getSubject()) ? emailConfig.getSubject() : defaultSubject;

            //获取邮件内容
            Context context = new Context();
            context.setVariable("shiftReportDTO", shiftReportNotifyDTO);
            context.setVariable("propertyName", propertyDTO.getName());
            context.setVariable("functionName", functionName);
            context.setVariable("hereLink", hereLineWithParam);
            String mailBody = templateEngine.process("endofshift/endOfShift_" + language, context);
            String fullEmailContent = EmailUtil.getFullEmailContent(defaultSubject, mailBody, language);

            NotifyDTO notifyDTO = new NotifyDTO();
            notifyDTO.setPropertyId(propertyDTO.getId());
            notifyDTO.setFunctionPoint(FunctionPointEnum.END_OF_SHIFT_CHANGE.getFunctionPoint());
            notifyDTO.setSubject(subject);
            notifyDTO.setIdentityList(beanMapper.mapList(emailTargets, NotifyIdentityDTO.class));
            notifyDTO.setEmailContent(fullEmailContent);
            //不进行line通知
            notifyDTO.setLineContent(Collections.emptyList());
            notifyInternalService.sendNotify(notifyDTO);
        }

        /**
         * 删除所有的排班数据（wakeup call数据不需要删除）
         *
         * @param propertyId 资产id
         * @param shiftIds   排班id集合
         * @return void
         * @author lichen
         * @date 2025/6/24
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void removeAllEndOfShiftExcludeWakeupCall(Integer propertyId, Collection<Integer> shiftIds) {
            if (CollectionUtils.isEmpty(shiftIds)) return;

            //删除shift_report_info
            shiftReportInfoService.removeByIds(shiftIds);

            //删除shift_complaint_request
            shiftComplaintRequestService.remove(new QueryWrapper<ShiftComplaintRequest>().lambda()
                    .eq(ShiftComplaintRequest::getPropertyId, propertyId)
                    .in(ShiftComplaintRequest::getShiftId, shiftIds));

            //删除shift_digital_check_in
            shiftDigitalCheckInService.remove(new QueryWrapper<ShiftDigitalCheckIn>().lambda()
                    .eq(ShiftDigitalCheckIn::getPropertyId, propertyId)
                    .in(ShiftDigitalCheckIn::getShiftId, shiftIds));

            //删除shift_operation_record
            shiftOperationRecordService.remove(new QueryWrapper<ShiftOperationRecord>().lambda()
                    .eq(ShiftOperationRecord::getPropertyId, propertyId)
                    .in(ShiftOperationRecord::getShiftId, shiftIds));
        }

    }