# CbCaseServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.component.SessionContext;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.UserDTO;
    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.UserInternalService;
    import cloud.wconnect.commonlib.util.*;
    import cloud.wconnect.finance.cons.S3BucketFunctionCons;
    import cloud.wconnect.finance.cons.chargeback.*;
    import cloud.wconnect.finance.entity.chargeback.*;
    import cloud.wconnect.finance.mapper.chargeback.CbCaseMapper;
    import cloud.wconnect.finance.pojo.bo.chargeback.CaseIssueDateBO;
    import cloud.wconnect.finance.pojo.bo.chargeback.CaseListBO;
    import cloud.wconnect.finance.pojo.dto.chargeback.*;
    import cloud.wconnect.finance.pojo.query.chargeback.CaseCommonQuery;
    import cloud.wconnect.finance.pojo.query.chargeback.CbCaseQuery;
    import cloud.wconnect.finance.pojo.query.chargeback.OtherCaseQuery;
    import cloud.wconnect.finance.pojo.vo.chargeback.ExportCaseVO;
    import cloud.wconnect.finance.service.chargeback.*;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.Wrapper;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    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 jakarta.annotation.Nullable;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.apache.ibatis.session.ExecutorType;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Lazy;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.RoundingMode;
    import java.time.Duration;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.ZonedDateTime;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.concurrent.Executor;
    import java.util.concurrent.atomic.AtomicInteger;
    import java.util.stream.Collectors;

    import static java.util.Comparator.comparingLong;
    import static java.util.stream.Collectors.*;

    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-08-22
     */
    @Service
    @DS("finance")
    public class CbCaseServiceImpl extends ServiceImpl<CbCaseMapper, CbCase> implements ICbCaseService {

        @Autowired
        private SessionContext sessionContext;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private ICbCaseOverTimeDayService cbCaseOverTimeDayService;
        @Lazy
        @Autowired
        private ICbCaseApproveLevelService cbCaseApproveLevelService;
        @Autowired
        private ICbCaseApproveLevelUserService cbCaseApproveLevelUserService;
        @Autowired
        private ICbCaseApproveLogService cbCaseApproveLogService;
        @Autowired
        private ICbCasePublishLogService cbCasePublishLogService;
        @Autowired
        private ICbCaseActionService cbCaseActionService;
        @Autowired
        private ICbCaseResultService cbCaseResultService;
        @Autowired
        private ICbCaseResultLogService cbCaseResultLogService;
        @Autowired
        private ICbCaseBankService cbCaseBankService;
        @Autowired
        private ICbAdjService cbAdjService;
        @Autowired
        private ICbCaseBankOverTimeDayService cbCaseBankOverTimeDayService;
        @Autowired
        private ICbCaseNotChangeDayService cbCaseNotChangeDayService;
        @Autowired
        private ChargebackBlacklistSendEventService chargebackBlacklistSendEventService;
        @Autowired
        private ICbCaseSendBlackRecordService cbCaseSendBlackRecordService;
        @Lazy
        @Autowired
        private ICbJournalService cbJournalService;
        @Autowired
        private UserInternalService userInternalService;
        @Autowired
        private SqlSessionFactory sqlSessionFactory;
        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private ICbCaseStageEvolutionService cbCaseStageEvolutionService;

        @Value("${here.link.chargeback.case-list}")
        private String caseLink;
        @Value("${aws-s3.region}")
        private String region;
        @Value("#{${aws-s3.function-bucket}}")
        private Map<String, Map<String, Object>> bucketMap;
        @Autowired
        private Executor executor;

        /**
         * 分页查询
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @return case
         * @author yyt
         * @date 2023/8/23
         */
        @Override
        public ListPagedDTO<CbCaseDTO> listPage(PropertyDTO propertyDTO, CbCaseQuery query) {
            Page<CbCase> page = page(new Page<>(query.getPage(), query.getSize()), packageSearchCondition(propertyDTO.getId(), query));
            return new ListPagedDTO<>(page.getTotal(), packageCaseList(page.getRecords(), propertyDTO.getTimezone()));
        }

        /**
         * 封装查询条件
         *
         * @param propertyId 资产 id
         * @param query      搜索条件
         * @return sql
         * @author yyt
         * @date 2023/8/23
         */
        private Wrapper<CbCase> packageSearchCondition(Integer propertyId, CbCaseQuery query) {
            LambdaQueryWrapper<CbCase> sql = new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyId)
                    .eq(Objects.nonNull(query.getVendor()), CbCase::getVendor, query.getVendor())
                    .eq(Objects.nonNull(query.getAction()), CbCase::getAction, query.getAction())
                    .eq(Objects.nonNull(query.getResult()), CbCase::getResult, query.getResult())
                    .eq(Objects.nonNull(query.getStage()), CbCase::getStage, query.getStage())
                    .eq(Objects.nonNull(query.getProgress()), CbCase::getLevelId, query.getProgress())
                    .eq(Objects.nonNull(query.getStatus()), CbCase::getStatus, query.getStatus())
                    .nested(StringUtils.isNotEmpty(query.getGuestName()), queryWrapper ->
                            queryWrapper.like(CbCase::getGuestFirstName, query.getGuestName())
                                    .or().
                                    like(CbCase::getGuestLastName, query.getGuestName()))
                    .like(StringUtils.isNotEmpty(query.getConfNumber()), CbCase::getFolioNumber, query.getConfNumber())
                    .eq(StringUtils.isNotEmpty(query.getCaseNumber()), CbCase::getCaseNumber, query.getCaseNumber())
                    .eq(StringUtils.isNotEmpty(query.getCardNumber()), CbCase::getCardNumberEnd, query.getCardNumber())
                    .nested(Objects.nonNull(query.getTrxDateBegin()) && Objects.nonNull(query.getTrxDateEnd()),
                            queryWrapper -> queryWrapper
                                    // 手动创建的 且 trx date 在区间内
                                    .nested(q -> q.gt(CbCase::getCreatedBy, BaseConstant.ZERO)
                                            .between(CbCase::getTrxDate, query.getTrxDateBegin(), query.getTrxDateEnd()))
                                    .or()
                                    // 采集的 且 pms trx date 在区间内
                                    .nested(q -> q.eq(CbCase::getCreatedBy, BaseConstant.ZERO)
                                            .between(CbCase::getPmsTrxDate, query.getTrxDateBegin(), query.getTrxDateEnd())))
                    .eq(Objects.nonNull(query.getNonChargeback()), CbCase::getNonChargeback, query.getNonChargeback())
                    .eq(Objects.nonNull(query.getHasOverdue()), CbCase::getHasOverdue, query.getHasOverdue());

            Boolean filterStage = query.getFilterStage();
            if (Objects.nonNull(filterStage)) {
                if (filterStage) {
                    // 有 overdue 且 酒店没有表态的 case
                    sql.eq(CbCase::getHasOverdue, true).eq(CbCase::getHasHotelOperations, false);
                } else {
                    // 没有 overdue 或 酒店有表态
                    sql.nested(item -> item.eq(CbCase::getHasOverdue, false).or().eq(CbCase::getHasHotelOperations, true));
                }
            }

            packageOrderByCondition(sql, query);
            return sql;
        }

        /**
         * 封装排序条件
         *
         * @param sql   sql
         * @param query 排序条件
         * @author yyt
         * @date 2023/8/23
         */
        private void packageOrderByCondition(LambdaQueryWrapper<CbCase> sql, CaseCommonQuery query) {
            if (Objects.nonNull(query.getSortIssueDate())) {
                if (query.getSortIssueDate() == CbCaseQuery.ASC) {
                    sql.orderByAsc(CbCase::getIssueDate);
                } else {
                    sql.orderByDesc(CbCase::getIssueDate);
                }
                return;
            }
            if (Objects.nonNull(query.getSortDueDate())) {
                if (query.getSortDueDate() == CbCaseQuery.ASC) {
                    sql.orderByAsc(CbCase::getDueDate);
                } else {
                    sql.orderByDesc(CbCase::getDueDate);
                }
                return;
            }
            if (Objects.nonNull(query.getSortTrxDate())) {
                String sortTrxDate = "CASE WHEN created_by = 0 THEN pms_trx_date ELSE trx_date END";
                if (query.getSortTrxDate() == CbCaseQuery.ASC) {
                    sql.last("ORDER BY " + sortTrxDate + " ASC");
                } else {
                    sql.last("ORDER BY " + sortTrxDate + " DESC");
                }
                return;
            }
            if (Objects.nonNull(query.getSortDisputeAmount())) {
                if (query.getSortDisputeAmount() == CbCaseQuery.ASC) {
                    sql.orderByAsc(CbCase::getDisputeAmount);
                } else {
                    sql.orderByDesc(CbCase::getDisputeAmount);
                }
                return;
            }
            // 默认
            sql.orderByDesc(CbCase::getIssueDate);
        }

        /**
         * 封装 case 列表数据
         *
         * @param cases    case
         * @param timeZone 时区
         * @return case 列表数据
         * @author yyt
         * @date 2023/8/23
         */
        private List<CbCaseDTO> packageCaseList(List<CbCase> cases, String timeZone) {
            if (CollectionUtils.isEmpty(cases)) {
                return Collections.emptyList();
            }

            Map<Long, String> leveNameById = cbCaseApproveLevelService.listApproveLevel(cases.get(0).getPropertyId())
                    .stream().collect(toMap(CbCaseApproveLevel::getId, CbCaseApproveLevel::getName));

            ZonedDateTime dateNow = DateTimeUtils.dateNow(timeZone);
            return beanMapper.mapList(cases, CbCaseDTO.class, item -> {
                // Due Date为0时不设置到期剩余天数
                Long dueDate = item.getDueDate();
                if (dueDate > 0) {
                    long day = Duration.between(dateNow, DateTimeUtils.toZonedDateTime(dueDate, timeZone)).toDays();
                    if (day < 0) {
                        day = 0;
                    }
                    item.setDaysToAct(day);
                }

                item.setLevelName(leveNameById.get(item.getLevelId()));
            });
        }

        /**
         * 获取查询超期天数 case sql
         *
         * @param propertyDTO 资产
         * @return sql
         * @author yyt
         * @date 2023/8/23
         */
        private LambdaQueryWrapper<CbCase> packageSearchOverTimeSql(PropertyDTO propertyDTO) {
            Integer propertyId = propertyDTO.getId();
            CbCaseOverTimeDay overTimeDay = cbCaseOverTimeDayService.getOverTimeDay(propertyId);
            if (Objects.isNull(overTimeDay) || Objects.isNull(overTimeDay.getDay())) {
                return null;
            }

            long overTime = DateTimeUtils.dateNow(propertyDTO.getTimezone()).plusDays(overTimeDay.getDay()).toEpochSecond();
            return new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyId)
                    .in(CbCase::getStage, Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.PATCH))
                    .nested(queryWrapper ->
                            queryWrapper.lt(CbCase::getReplyBy, overTime).gt(CbCase::getReplyBy, BaseConstant.ZERO)
                                    .or()
                                    .lt(CbCase::getAdjReplyBy, overTime).gt(CbCase::getAdjReplyBy, BaseConstant.ZERO));
        }

        /**
         * 获取状态未改变的 case sql
         *
         * @param propertyDTO 资产
         * @return sql
         * @author yyt
         * @date 2023/8/23
         */
        private LambdaQueryWrapper<CbCase> packageSearchStatusNotChangeSql(PropertyDTO propertyDTO) {
            // 根据设置状态未变更天数，查询指定时间内状态未变更的 case
            CbCaseNotChangeDay notChangeDay = cbCaseNotChangeDayService.getNotChangeDay(propertyDTO.getId());
            if (Objects.isNull(notChangeDay) || Objects.isNull(notChangeDay.getDay())) {
                return null;
            }

            long fewDaysAgo = DateTimeUtils.dateNow(propertyDTO.getTimezone()).minusDays(notChangeDay.getDay()).toEpochSecond();
            return new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyDTO.getId())
                    // 排除 close / 忽略
                    .notIn(CbCase::getStatus, Lists.newArrayList(CaseStatusEnum.CLOSE.getCode(), CaseStatusEnum.IGNORED.getCode()))
                    .le(CbCase::getStatusSetTime, fewDaysAgo).gt(CbCase::getStatusSetTime, BaseConstant.ZERO);
        }

        /**
         * 查询等待我处理的 case
         *
         * @param propertyId 资产 id
         * @param userId     操作人
         * @return 等待我处理的 case
         * @author yyt
         * @date 2023/8/23
         */
        private List<CbCase> listWaitProcessingCase(Integer propertyId, Integer userId) {
            /*
             *  根据当前用户所属的层级，查询各个阶段的 case，包含：
             *  1、如果当前用户处于某阶段处理人的层级，则返回 刚进入此阶段的 case
             *  2、如果当前用户处于审批层级，则返回 审批中的 case，且过滤掉当前用户已经审批过
             *  3、如果当前用户处于酒店表态阶段的处理人层级，则返回 酒店未表态但是已经走到后续阶段的 case
             */
            List<CbCaseApproveLevelUser> approveLevelUsers =
                    cbCaseApproveLevelUserService.list(new QueryWrapper<CbCaseApproveLevelUser>().lambda()
                            .eq(CbCaseApproveLevelUser::getPropertyId, propertyId)
                            .eq(CbCaseApproveLevelUser::getUserId, userId));
            if (approveLevelUsers.isEmpty()) {
                return Collections.emptyList();
            }

            List<CbCase> cases = new ArrayList<>();
            // 以 0级配置 分组
            Map<Boolean, List<CbCaseApproveLevelUser>> approveUsersByZeroApproveConfig = approveLevelUsers.stream()
                    .collect(Collectors.partitioningBy(item -> item.getApproveIndex() == BaseConstant.ZERO));
            List<CbCaseApproveLevelUser> zeroApproveConfigs = approveUsersByZeroApproveConfig.get(true);
            if (CollectionUtils.isNotEmpty(zeroApproveConfigs)) {
                // 查询刚进入阶段 case，排除 忽略、完结状态
                List<Long> zeroLevelIds = zeroApproveConfigs.stream().map(CbCaseApproveLevelUser::getLevelId).distinct()
                        .collect(Collectors.toList());
                cases.addAll(list(new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyId)
                        .in(CbCase::getLevelId, zeroLevelIds)
                        .notIn(CbCase::getStatus, Lists.newArrayList(CaseStatusEnum.IGNORED.getCode(), CaseStatusEnum.CLOSE.getCode()))));

                // 处理第 3 情况（如果当前用户处于酒店表态阶段的处理人层级，则返回 酒店未表态但是已经走到后续阶段的 case）
                if (zeroApproveConfigs.stream().anyMatch(item -> item.getStage() == StageCons.HOTEL_OPERATION)) {
                    // 当前资产下，has_hotel_operations 为 false 且 stage > 2
                    cases.addAll(list(new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyId)
                            .eq(CbCase::getHasHotelOperations, false)
                            .gt(CbCase::getStage, StageCons.HOTEL_OPERATION)));
                }
            }
            List<CbCaseApproveLevelUser> notZeroApproveConfigs = approveUsersByZeroApproveConfig.get(false);
            if (CollectionUtils.isNotEmpty(notZeroApproveConfigs)) {
                // 查询审批中 case
                Map<Integer, List<Long>> approveLevelIdsByStage = notZeroApproveConfigs.stream()
                        .collect(Collectors.groupingBy(CbCaseApproveLevelUser::getStage,
                                Collectors.mapping(CbCaseApproveLevelUser::getLevelId, Collectors.toList())));
                List<CbCase> cbCases = baseMapper.listByStageAndApproveIndex(propertyId, approveLevelIdsByStage);
                if (CollectionUtils.isNotEmpty(cbCases)) {
                    // 过滤掉 当前用户已经审批过的 case
                    List<Long> userApproveCaseIds = cbCaseApproveLogService.listCaseIdByCaseAndTargetUser(cbCases, userId);

                    cases.addAll(cbCases.stream().filter(item -> !userApproveCaseIds.contains(item.getId())).toList());
                }
            }

            // 去重
            return cases.stream().collect(
                    collectingAndThen(
                            toCollection(() -> new TreeSet<>(comparingLong(CbCase::getId))), ArrayList::new)
            );
        }

        /**
         * 获取未关闭的 case sql
         *
         * @param propertyId 资产 id
         * @return sql
         * @author yyt
         * @date 2024/2/27
         */
        private LambdaQueryWrapper<CbCase> packageSearchIncompleteSql(Integer propertyId) {
            // 检索状态不是 忽略、已关闭(无需审批或已审批完成的)的 case，目的是想过滤掉不需要处理的 case
            return packageSearchIncompleteSql(Lists.newArrayList(propertyId));
        }

        /**
         * 获取未关闭的 case sql
         *
         * @param propertyIds 资产 id
         * @return sql
         * @author yyt
         * @date 2024/2/27
         */
        private LambdaQueryWrapper<CbCase> packageSearchIncompleteSql(List<Integer> propertyIds) {
            // 检索状态不是 忽略、已关闭(无需审批或已审批完成的)的 case，目的是想过滤掉不需要处理的 case
            return new QueryWrapper<CbCase>().lambda().in(CbCase::getPropertyId, propertyIds)
                    .ne(CbCase::getStatus, CaseStatusEnum.IGNORED.getCode())
                    .nested(item -> item.ne(CbCase::getStatus, CaseStatusEnum.CLOSE.getCode())
                            .or()
                            .nested(child -> child.eq(CbCase::getStatus, CaseStatusEnum.CLOSE.getCode())
                                    .notIn(CbCase::getApproveStatus, Lists.newArrayList(ApproveStatusEnum.WITHOUT_APPROVAL.getStatus(),
                                            ApproveStatusEnum.APPROVED.getStatus())))
                    );
        }

        /**
         * 获取未关闭的 case
         *
         * @param propertyIds 资产 id
         * @return sql
         * @author yyt
         * @date 2024/2/27
         */
        @Override
        public List<CbCase> listIncompleteCase(List<Integer> propertyIds) {
            // 检索状态不是 忽略、已关闭(无需审批或已审批完成的)的 case，目的是想过滤掉不需要处理的 case
            return list(packageSearchIncompleteSql(propertyIds));
        }

        /**
         * 统计超期 case
         *
         * @param propertyDTO 资产
         * @return 超期 case
         * @author yyt
         * @date 2023/8/23
         */
        @Override
        public Integer statisticsOverTimeCase(PropertyDTO propertyDTO) {
            LambdaQueryWrapper<CbCase> queryWrapper = packageSearchOverTimeSql(propertyDTO);
            if (Objects.isNull(queryWrapper)) {
                return 0;
            }

            return Integer.parseInt(String.valueOf(count(queryWrapper)));
        }

        /**
         * 统计状态未改变的 case
         *
         * @param propertyDTO 资产
         * @return 状态未改变的 case
         * @author yyt
         * @date 2023/8/23
         */
        @Override
        public Integer statisticsStatusNotChangeCase(PropertyDTO propertyDTO) {
            LambdaQueryWrapper<CbCase> queryWrapper = packageSearchStatusNotChangeSql(propertyDTO);
            return Objects.isNull(queryWrapper) ? 0 : Integer.parseInt(String.valueOf(count(queryWrapper)));
        }

        /**
         * 统计等待我处理的 case
         *
         * @param propertyId 资产 id
         * @param userId     操作人
         * @return 等待我处理的 case
         * @author yyt
         * @date 2023/8/23
         */
        @Override
        public Integer statisticsWaitProcessingCase(Integer propertyId, Integer userId) {
            return listWaitProcessingCase(propertyId, userId).size();
        }

        /**
         * 统计未关闭的 case
         *
         * @param propertyId 资产
         * @return 未关闭的 case
         * @author yyt
         * @date 2024/2/27
         */
        @Override
        public Integer statisticsIncompleteCase(Integer propertyId) {
            LambdaQueryWrapper<CbCase> queryWrapper = packageSearchIncompleteSql(propertyId);
            return Integer.parseInt(String.valueOf(count(queryWrapper)));
        }

        /**
         * 排序 case
         *
         * @param cases case
         * @param query 排序条件
         * @author yyt
         * @date 2023/8/23
         */
        private List<CbCase> orderBy(List<CbCase> cases, CaseCommonQuery query) {
            if (Objects.nonNull(query.getSortIssueDate())) {
                if (query.getSortIssueDate() == CaseCommonQuery.ASC) {
                    cases.sort(Comparator.comparing(CbCase::getIssueDate));
                } else {
                    cases.sort(Comparator.comparing(CbCase::getIssueDate).reversed());
                }
                return cases;
            }
            if (Objects.nonNull(query.getSortDueDate())) {
                if (query.getSortDueDate() == CaseCommonQuery.ASC) {
                    cases.sort(Comparator.comparing(CbCase::getDueDate));
                } else {
                    cases.sort(Comparator.comparing(CbCase::getDueDate).reversed());
                }
                return cases;
            }
            if (Objects.nonNull(query.getSortTrxDate())) {
                return sortByTrxDate(cases, query.getSortTrxDate());
            }
            if (Objects.nonNull(query.getSortDisputeAmount())) {
                if (query.getSortDisputeAmount() == CaseCommonQuery.ASC) {
                    cases.sort(Comparator.comparing(CbCase::getDisputeAmount));
                } else {
                    cases.sort(Comparator.comparing(CbCase::getDisputeAmount).reversed());
                }
                return cases;
            }
            // 默认
            cases.sort(Comparator.comparing(CbCase::getIssueDate));
            return cases;
        }

        /**
         * 交易日期排序
         *
         * @param cases case
         * @param sortType 排序条类型
         * @return case
         * @author yyt
         * @date 2025/10/28
         **/
        private List<CbCase> sortByTrxDate(List<CbCase> cases, Integer sortType) {
            if (CollectionUtils.isEmpty(cases)) return Collections.emptyList();

            boolean ascending = sortType == CaseCommonQuery.ASC;

            return cases.stream()
                    .sorted((a, b) -> {
                        Long dateA = a.getCreatedBy() == 0 ? a.getPmsTrxDate() : a.getTrxDate();
                        Long dateB = b.getCreatedBy() == 0 ? b.getPmsTrxDate() : b.getTrxDate();

                        if (dateA == null && dateB == null) return 0;
                        if (dateA == null) return ascending ? -1 : 1;
                        if (dateB == null) return ascending ? 1 : -1;

                        return ascending ? dateA.compareTo(dateB) : dateB.compareTo(dateA);
                    })
                    .toList();
        }

        /**
         * 检索统计数据
         *
         * @param propertyDTO 资产
         * @param query      搜索条件
         * @param userId     操作人
         * @return case 列表
         * @author yyt
         * @date 2023/8/24
         */
        @Override
        public ListPagedDTO<CbCaseDTO> listStatisticsPage(PropertyDTO propertyDTO, CbCaseQuery query, Integer userId) {
            Integer type = query.getType();
            Page<CbCase> page;
            switch (type) {
                case CbCaseQuery.SEARCH_OVER_TIME_CASE:
                    page = listPageOverTimeCase(propertyDTO, query);
                    break;
                case CbCaseQuery.SEARCH_STATUS_NOT_CHANGE_CASE:
                    page = listPageStatusNotChangeCase(propertyDTO, query);
                    break;
                case CbCaseQuery.SEARCH_WAIT_PROCESSING_CASE:
                    page = listPageWaitProcessingCase(propertyDTO.getId(), query, userId);
                    break;
                case CbCaseQuery.SEARCH_INCOMPLETE_CASE:
                    page = listPageIncompleteCase(propertyDTO, query);
                    break;
                default:
                    return new ListPagedDTO<>();
            }
            ListPagedDTO<CbCaseDTO> listPagedDTO = new ListPagedDTO<>();
            listPagedDTO.setTotal(page.getTotal());
            listPagedDTO.setList(packageCaseList(page.getRecords(), propertyDTO.getTimezone()));
            return listPagedDTO;
        }

        /**
         * 分页查询超期 case
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @return case
         * @author yyt
         * @date 2023/8/29
         */
        private Page<CbCase> listPageOverTimeCase(PropertyDTO propertyDTO, CbCaseQuery query) {
            LambdaQueryWrapper<CbCase> sql = packageSearchOverTimeSql(propertyDTO);
            if (Objects.isNull(sql)) {
                return new Page<>();
            }

            packageOrderByCondition(sql, query);
            return page(new Page<>(query.getPage(), query.getSize()), sql);
        }

        /**
         * 分页查询长时间状态未变更 case
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @return case
         * @author yyt
         * @date 2023/8/29
         */
        private Page<CbCase> listPageStatusNotChangeCase(PropertyDTO propertyDTO, CbCaseQuery query) {
            LambdaQueryWrapper<CbCase> sql = packageSearchStatusNotChangeSql(propertyDTO);
            if (Objects.isNull(sql)) {
                return new Page<>();
            }

            packageOrderByCondition(sql, query);
            return page(new Page<>(query.getPage(), query.getSize()), sql);
        }

        /**
         * 分页查询等待我处理 case
         *
         * @param propertyId 资产 id
         * @param query      查询条件
         * @param userId     操作人
         * @return case
         * @author yyt
         * @date 2023/8/29
         */
        private Page<CbCase> listPageWaitProcessingCase(Integer propertyId, CbCaseQuery query, Integer userId) {
            List<CbCase> cases = listWaitProcessingCase(propertyId, userId);
            List<CbCase> finalCases = orderBy(cases, query);
            Page<CbCase> page = new Page<>();
            page.setTotal(finalCases.size());
            page.setRecords(finalCases.stream().skip((long) (query.getPage() - 1) * query.getSize())
                    .limit(query.getSize()).collect(Collectors.toList()));
            return page;
        }

        /**
         * 分页查询未关闭的 case
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @return case
         * @author yyt
         * @date 2024/2/27
         */
        private Page<CbCase> listPageIncompleteCase(PropertyDTO propertyDTO, CbCaseQuery query) {
            LambdaQueryWrapper<CbCase> sql = packageSearchIncompleteSql(propertyDTO.getId());
            if (Objects.isNull(sql)) {
                return new Page<>();
            }

            packageOrderByCondition(sql, query);
            return page(new Page<>(query.getPage(), query.getSize()), sql);
        }

        /**
         * 校验存在
         *
         * @param id case id
         * @return case
         * @author yyt
         * @date 2023/8/26
         */
        @Override
        public CbCase checkExist(long id) {
            CbCase cs = getById(id);
            if (Objects.isNull(cs)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the cbCase is not found;id:" + id);
            }
            return cs;
        }

        /**
         * other 列表查询
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @param cs         case
         * @return case
         * @author yyt
         * @date 2023/8/26
         */
        @Override
        public List<CbCaseDTO> listOthers(PropertyDTO propertyDTO, OtherCaseQuery query, CbCase cs) {
            LambdaQueryWrapper<CbCase> sql = packageSearchOtherCondition(propertyDTO, query.getIssueDateBegin(), query.getIssueDateEnd(),
                    cs, false);
            packageOrderByCondition(sql, query);
            return packageCaseList(list(sql), propertyDTO.getTimezone());
        }

        /**
         * 封装查询 other 列表数据条件
         *
         * @param propertyDTO     资产
         * @param issueDateBegin issue date begin
         * @param issueDateEnd   issue date end
         * @param cs             case
         * @param excludeSelf    排除自身
         * @return 查询条件
         * @author yyt
         * @date 2023/8/29
         */
        private LambdaQueryWrapper<CbCase> packageSearchOtherCondition(PropertyDTO propertyDTO, @Nullable Long issueDateBegin,
                                                                       @Nullable Long issueDateEnd, CbCase cs, Boolean excludeSelf) {
            String timeZone = propertyDTO.getTimezone();
            if (Objects.isNull(issueDateBegin) && Objects.isNull(issueDateEnd)) {
                // 默认设置时间范围为当前 Case的 issueDate 未来 2 个月和过去 4 个月
                ZonedDateTime issueDate = DateTimeUtils.toZonedDateTime(cs.getIssueDate(), timeZone);
                issueDateBegin = issueDate.minusMonths(4).toEpochSecond();
                issueDateEnd = issueDate.plusMonths(2).toEpochSecond();
            }

            // 相同卡号 / 相同姓名
            return new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyDTO.getId())
                    .between(CbCase::getIssueDate, issueDateBegin, issueDateEnd)
                    .ne(excludeSelf, CbCase::getId, cs.getId())
                    .nested(cardNumberCondition -> cardNumberCondition.eq(CbCase::getCardNumber, cs.getCardNumber())
                            .or().nested(StringUtils.isNotEmpty(cs.getGuestFirstName()) && StringUtils.isNotEmpty(cs.getGuestLastName()),
                                    nameCondition -> nameCondition.eq(CbCase::getGuestFirstName, cs.getGuestFirstName())
                                            .eq(CbCase::getGuestLastName, cs.getGuestLastName())));
        }

        /**
         * 相同 case number 查询
         *
         * @param propertyDTO 资产
         * @param query      查询条件
         * @param cs         case
         * @return case
         * @author yyt
         * @date 2023/8/26
         */
        @Override
        public List<CbCaseDTO> listSameCaseNumber(PropertyDTO propertyDTO, OtherCaseQuery query, CbCase cs) {
            LambdaQueryWrapper<CbCase> sql = packageSearchSameCaseNumberCondition(propertyDTO.getId(), cs.getCaseNumber(), null);
            // 默认 issue date 正序排序
            if (Objects.isNull(query.getSortIssueDate()) && Objects.isNull(query.getSortTrxDate()) && Objects.isNull(query.getSortDueDate())
                    && Objects.isNull(query.getSortDisputeAmount())) {
                query.setSortIssueDate(CbCaseQuery.ASC);
            }
            packageOrderByCondition(sql, query);
            return packageCaseList(list(sql), propertyDTO.getTimezone());
        }

        /**
         * 封装查询相同 case number 数据条件
         *
         * @param propertyId 资产 id
         * @param caseNumber case 单号
         * @param id         排除自身
         * @return 查询条件
         * @author yyt
         * @date 2023/8/29
         */
        private LambdaQueryWrapper<CbCase> packageSearchSameCaseNumberCondition(Integer propertyId, String caseNumber, @Nullable Long id) {
            // 相同 case number
            return new QueryWrapper<CbCase>().lambda().eq(CbCase::getPropertyId, propertyId)
                    .eq(CbCase::getCaseNumber, caseNumber).ne(Objects.nonNull(id), CbCase::getId, id);
        }

        /**
         * 获取导出 case 数据
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @param userId      操作人
         * @return case 数据
         * @author yyt
         * @date 2023/8/29
         */
        @Override
        public List<ExportCaseVO> packageExportCase(PropertyDTO propertyDTO, CbCaseQuery query, Integer userId) {
            Integer propertyId = propertyDTO.getId();
            List<CbCase> cases;
            CbCase oneCs;
            switch (query.getExportType()) {
                case CbCaseQuery.EXPORT_LIST_DATA:
                    if (Objects.nonNull(query.getOverTime()) && query.getOverTime()) {
                        // 导出 超期 case
                        LambdaQueryWrapper<CbCase> sql = packageSearchOverTimeSql(propertyDTO);
                        if (Objects.isNull(sql)) {
                            cases = Collections.emptyList();
                        } else {
                            packageOrderByCondition(sql, query);
                            cases = list(sql);
                        }
                        break;
                    }
                    if (Objects.nonNull(query.getStatusNotChange()) && query.getStatusNotChange()) {
                        // 导出 状态长时间未变更 case
                        LambdaQueryWrapper<CbCase> sql = packageSearchStatusNotChangeSql(propertyDTO);
                        if (Objects.isNull(sql)) {
                            cases = Collections.emptyList();
                        } else {
                            packageOrderByCondition(sql, query);
                            cases = list(sql);
                        }
                        break;
                    }
                    if (Objects.nonNull(query.getWaitProcessing()) && query.getWaitProcessing()) {
                        // 导出 等待我处理的 case
                        List<CbCase> notSortCases = listWaitProcessingCase(propertyId, userId);
                        cases = orderBy(notSortCases, query);
                        break;
                    }
                    if (Objects.nonNull(query.getIncomplete()) && query.getIncomplete()) {
                        // 导出 未关闭的 case
                        LambdaQueryWrapper<CbCase> sql = packageSearchIncompleteSql(propertyId);
                        packageOrderByCondition(sql, query);
                        cases = list(sql);
                        break;
                    }
                    // 导出 列表数据
                    cases = list(packageSearchCondition(propertyId, query));
                    break;
                case CbCaseQuery.EXPORT_OTHER_LIST_DATA:
                    // 导出 other 列表数据
                    oneCs = checkExist(query.getId());
                    LambdaQueryWrapper<CbCase> sql = packageSearchOtherCondition(propertyDTO, query.getIssueDateBegin(),
                            query.getIssueDateEnd(), oneCs, false);
                    packageOrderByCondition(sql, query);
                    cases = list(sql);
                    break;
                case CbCaseQuery.EXPORT_EXPORT_SAME_CASE_NUMBER_LIST_DATA:
                    // 导出 相同 case number数据
                    oneCs = checkExist(query.getId());
                    sql = packageSearchSameCaseNumberCondition(propertyId, oneCs.getCaseNumber(), null);
                    packageOrderByCondition(sql, query);
                    cases = list(sql);
                    break;
                default:
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_INVALID,
                            i18nMessageUtils.getErrorParamMapNotNeedTranslate("export type"),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "export type error.");
            }

            Map<Integer, String> bankNameById = Collections.emptyMap();
            // case result
            Map<Long, CbCaseResult> resultByCaseId = Collections.emptyMap();
            if (CollectionUtils.isNotEmpty(cases)) {
                bankNameById = cbCaseBankService.list(Sets.newHashSet(propertyId));

                resultByCaseId = cbCaseResultService.listByCaseIds(cases.stream().map(CbCase::getId).toList(), null)
                        .stream().collect(toMap(CbCaseResult::getCaseId, v -> v));
            }

            Locale locale = LocaleContextHolder.getLocale();
            String timeZone = propertyDTO.getTimezone();
            InternationalizationDateTimeDTO dateTime = propertyDTO.getInternationalization().getDateTime();
            String longDate = dateTime.getLongDate();
            String nameOrder = propertyDTO.getInternationalization().getNameOrder();
            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            String calculationRule = number.getCalculationRule();
            String currency = number.getCurrency();

            List<ExportCaseVO> exportCases = Lists.newArrayListWithExpectedSize(cases.size());
            for (CbCase cs : cases) {
                ExportCaseVO caseVO = new ExportCaseVO();
                caseVO.setCaseNumber(cs.getCaseNumber());
                String actualReason = cs.getActualReason();
                if (StringUtils.isNotEmpty(actualReason)) {
                    if (actualReason.startsWith("Other:")) {
                        caseVO.setActualReason(StringUtils.substringAfter(actualReason, "Other: "));
                    } else {
                        caseVO.setActualReason(i18nMessageUtils.message(ActualReasonEnum.getI18nCode(actualReason), locale));
                    }
                }
                caseVO.setStage(i18nMessageUtils.message(StageEnum.getStageDesc(cs.getStage()), locale));
                caseVO.setVendor(bankNameById.get(cs.getVendor()));
                caseVO.setStatus(cs.getStatus() == BaseConstant.ZERO ? StringUtils.EMPTY : i18nMessageUtils.message(CaseStatusEnum.getStatusDesc(cs.getStatus()), locale));
                // 日期 07/10/2020
                caseVO.setIssueDate(cs.getIssueDate() == BaseConstant.ZERO ? "" : InternationalizationUtils.formatDateTime(cs.getIssueDate(), longDate, null, timeZone, locale));
                Long dueDate = cs.getDueDate();
                if (dueDate > 0) {
                    caseVO.setDueDate(InternationalizationUtils.formatDateTime(dueDate, longDate, null, timeZone, locale));
                } else {
                    caseVO.setDueDate("");
                }
                caseVO.setCardNumber(cs.getCardNumber());
                Long trxDate = cs.getCreatedBy() == BaseConstant.ZERO ? cs.getPmsTrxDate() : cs.getTrxDate();
                caseVO.setTrxDate(Objects.isNull(trxDate) || trxDate == BaseConstant.ZERO ? "" : InternationalizationUtils.formatDateTime(trxDate, longDate, null, timeZone, locale));
                caseVO.setGuestName(InternationalizationUtils.formatName(cs.getGuestFirstName(), cs.getGuestLastName(), nameOrder));
                caseVO.setDisputeAmount(InternationalizationUtils.formatAmountViewValue(cs.getDisputeAmount(), calculationRule, currency));
                caseVO.setFolioNumber(cs.getFolioNumber());
                caseVO.setAction(cs.getAction() == BaseConstant.ZERO ? StringUtils.EMPTY : i18nMessageUtils.message(CaseActionEnum.getActionDesc(cs.getAction()), locale));
                caseVO.setResult(cs.getResult() == BaseConstant.ZERO ? StringUtils.EMPTY : i18nMessageUtils.message(CaseResultEnum.getResultDesc(cs.getResult()), locale));

                CbCaseResult result = resultByCaseId.getOrDefault(cs.getId(), new CbCaseResult());
                caseVO.setPortalStageComment(result.getComment());
                caseVO.setLastStageComment(result.getCommentLastStage());

                Long resultFinalDate = cs.getResultFinalDate();
                if (resultFinalDate > 0) {
                    caseVO.setResultFinalDate(InternationalizationUtils.formatDateTime(resultFinalDate, longDate, null,
                            timeZone, locale));
                } else {
                    caseVO.setResultFinalDate("");
                }
                caseVO.setCardPresent(cs.getCardPresent());
                caseVO.setHereLink(caseLink + "?property=" + propertyDTO.getAbbr() +
                        "&tenant=" + propertyDTO.getTenantSymbol() + "&toDetail=1&caseId=" + cs.getId());
                exportCases.add(caseVO);
            }
            return exportCases;
        }

        /**
         * 上一个、下一个 case
         *
         * @param propertyDTO 资产
         * @param query       查询条件
         * @param userId      操作人
         * @return case
         * @author yyt
         * @date 2023/8/29
         */
        @Override
        public Map<String, Long> handleLastNext(PropertyDTO propertyDTO, CbCaseQuery query, Integer userId) {
            if (Objects.nonNull(query.getOverTime()) && query.getOverTime()) {
                // 查询 超期 case
                List<CbCase> cases = listPageOverTimeCase(propertyDTO, query).getRecords();
                return getLastNextCase(cases, query.getIndex());
            }
            if (Objects.nonNull(query.getStatusNotChange()) && query.getStatusNotChange()) {
                // 查询 状态长时间未变更 case
                List<CbCase> cases = listPageStatusNotChangeCase(propertyDTO, query).getRecords();
                return getLastNextCase(cases, query.getIndex());
            }
            if (Objects.nonNull(query.getWaitProcessing()) && query.getWaitProcessing()) {
                // 查询 等待我处理的 case
                List<CbCase> cases = listPageWaitProcessingCase(propertyDTO.getId(), query, userId).getRecords();
                return getLastNextCase(cases, query.getIndex());
            }
            if (Objects.nonNull(query.getIncomplete()) && query.getIncomplete()) {
                // 查询 未关闭的 case
                List<CbCase> cases = listPageIncompleteCase(propertyDTO, query).getRecords();
                return getLastNextCase(cases, query.getIndex());
            }
            // 查询 列表数据
            List<CbCase> cases = page(new Page<>(query.getPage(), query.getSize()),
                    packageSearchCondition(propertyDTO.getId(), query)).getRecords();
            return getLastNextCase(cases, query.getIndex());
        }

        /**
         * 获取 上一个、下一个 case
         *
         * @param cases case
         * @param index 上一个、下一个 case 脚标
         * @return case
         * @author yyt
         * @date 2023/8/30
         */
        private Map<String, Long> getLastNextCase(List<CbCase> cases, Integer index) {
            Map<String, Long> result = Maps.newHashMapWithExpectedSize(1);
            if (CollectionUtils.isEmpty(cases)) {
                return Collections.emptyMap();
            }
            if (cases.size() - 1 < index) {
                return Collections.emptyMap();
            }
            result.put("id", cases.get(index).getId());
            return result;
        }

        /**
         * 检查指定 case 存在
         *
         * @param propertyId 资产 id
         * @param caseNumber case number
         * @param issueDate  issue date
         * @param id         排除自身
         * @return true / false
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        public boolean checkExist(Integer propertyId, String caseNumber, Long issueDate, @Nullable Long id) {
            return count(new QueryWrapper<CbCase>().lambda().eq(CbCase::getCaseNumber, caseNumber)
                    .eq(CbCase::getIssueDate, issueDate).eq(CbCase::getPropertyId, propertyId).ne(Objects.nonNull(id), CbCase::getId, id)) > 0;
        }

        /**
         * 新增 case
         *
         * @param propertyId 资产 id
         * @param caseDTO    case 信息
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public Long save(Integer propertyId, SaveCaseDTO caseDTO) {
            if (checkExist(propertyId, caseDTO.getCaseNumber(), caseDTO.getIssueDate(), null)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_EXISTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "Failed to save existing data.");
            }

            CbCaseApproveLevel zeroLevel = cbCaseApproveLevelService.checkZeroLevelExist(propertyId, StageCons.INITIAL_CASE);

            CbCase cs = beanMapper.map(caseDTO, CbCase.class);
            cs.setPropertyId(propertyId);
            cs.setCardNumberEnd(StringUtils.substring(caseDTO.getCardNumber(), -4, caseDTO.getCardNumber().length()));
            cs.setStage(StageCons.INITIAL_CASE);
            cs.setLevelId(zeroLevel.getId());
            cs.setBatchId(BaseConstant.LONG_ZERO);
            cs.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            cs.setCreatedBy(sessionContext.getLoginUserId());
            cs.setCreatedTime(DateTimeUtils.nowTimestamp());
            cs.setHandleCase(false);
            cs.setChargeBackType(caseDTO.getChargebackType());
            cs.setMoreGuests(StringUtils.EMPTY);
            save(cs);

            resetCaseOrderNumber(propertyId, Lists.newArrayList(caseDTO.getCaseNumber()));

            return cs.getId();
        }

        /**
         * 重置 case order number
         *
         * @param propertyId  资产 id
         * @param caseNumbers case number
         * @author yyt
         * @date 2023/8/30
         */
        private void resetCaseOrderNumber(Integer propertyId, List<String> caseNumbers) {
            if (caseNumbers.isEmpty()) {
                return;
            }

            Map<String, List<CbCase>> casesByCaseNumber = list(new QueryWrapper<CbCase>().lambda()
                    .eq(CbCase::getPropertyId, propertyId).in(CbCase::getCaseNumber, caseNumbers)
                    .select(CbCase::getId, CbCase::getCaseNumber, CbCase::getOrderNumber, CbCase::getIssueDate))
                    .stream().collect(Collectors.groupingBy(CbCase::getCaseNumber));
            if (casesByCaseNumber.isEmpty()) {
                return;
            }

            List<CbCase> updateCases = Lists.newArrayListWithExpectedSize(casesByCaseNumber.size());
            casesByCaseNumber.forEach((caseNumber, cases) -> {
                if (cases.size() == 1) {
                    return;
                }

                AtomicInteger atomicInteger = new AtomicInteger(1);
                cases.stream().sorted(Comparator.comparing(CbCase::getIssueDate))
                        .forEach(item -> {
                            int newOrderNumber = atomicInteger.getAndIncrement();
                            if (item.getOrderNumber() != newOrderNumber) {
                                CbCase updateCase = new CbCase();
                                updateCase.setId(item.getId());
                                updateCase.setOrderNumber(newOrderNumber);
                                updateCases.add(updateCase);
                            }
                        });
            });
            if (!updateCases.isEmpty()) {
                SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
                for (int i = 0; i < updateCases.size(); i++) {
                    updateById(updateCases.get(i));
                    // 每 50 条提交一次
                    if ((i + 1) % 50 == 0) {
                        sqlSession.flushStatements();
                    }
                }
                sqlSession.flushStatements();
            }
        }

        /**
         * 修改 case
         *
         * @param updateBeforeCase 修改之前的 case
         * @param caseDTO          修改信息
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void update(CbCase updateBeforeCase, SaveCaseDTO caseDTO) {
            // draft 状态 且 未送审/审批拒绝时 可以修改
            List<Integer> effectiveApproveStatuses = Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(),
                    ApproveStatusEnum.REJECTED.getStatus());
            if (updateBeforeCase.getStatus() != CaseStatusEnum.DRAFT.getCode()
                    || !effectiveApproveStatuses.contains(updateBeforeCase.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_FAIL_STATUS_MUST_DRAFT,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The operation can only be carried out when the draft status has not been published for approval or when the approval has been rejected.");
            }

            if (checkExist(updateBeforeCase.getPropertyId(), caseDTO.getCaseNumber(), caseDTO.getIssueDate(), updateBeforeCase.getId())) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_EXISTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "Failed to update existing data.");
            }

            CbCase updateCase = beanMapper.map(caseDTO, CbCase.class);
            updateCase.setCardNumberEnd(StringUtils.substring(caseDTO.getCardNumber(), -4, caseDTO.getCardNumber().length()));
            updateCase.setUpdatedBy(sessionContext.getLoginUserId());
            updateCase.setUpdatedTime(DateTimeUtils.nowTimestamp());
            updateCase.setId(updateBeforeCase.getId());
            updateCase.setChargeBackType(caseDTO.getChargebackType());
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateById(updateCase);

            resetCaseOrderNumber(updateBeforeCase.getPropertyId(), Lists.newArrayList(caseDTO.getCaseNumber()));

            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                //修改前的s3Key集合
                List<String> updateBeforeS3Keys = AttachmentUtil.getKeysByAbbrAttachmentArrayJson(updateBeforeCase.getAttachments(), objectMapper);
                //修改后的s3Key集合
                List<String> updateAfterS3Keys = AttachmentUtil.getKeysByAbbrAttachmentArrayJson(caseDTO.getAttachments(), objectMapper);
                //删除的附件 = 修改前的附件集合 - 修改后的附件集合
                List<String> delS3Keys = Lists.newArrayList(updateBeforeS3Keys);
                delS3Keys.removeAll(updateAfterS3Keys);
                S3Utils.batchDeleteFile(region, bucketMap.get(S3BucketFunctionCons.CHARGEBACK), delS3Keys);
            });
        }

        /**
         * 修改采集 case
         *
         * @param updateBeforeCase 修改之前的 case
         * @param caseDTO          修改信息
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateCrawlCase(CbCase updateBeforeCase, SaveCrawlCaseDTO caseDTO) {
            // draft 状态 且 未送审/审批拒绝时 可以修改
            List<Integer> effectiveApproveStatuses = Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(),
                    ApproveStatusEnum.REJECTED.getStatus());
            if (updateBeforeCase.getStatus() != CaseStatusEnum.DRAFT.getCode()
                    || !effectiveApproveStatuses.contains(updateBeforeCase.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_FAIL_STATUS_MUST_DRAFT,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The operation can only be carried out when the draft status has not been published for approval or when the approval has been rejected.");
            }

            CbCase updateCase = beanMapper.map(caseDTO, CbCase.class);
            updateCase.setUpdatedBy(sessionContext.getLoginUserId());
            updateCase.setUpdatedTime(DateTimeUtils.nowTimestamp());
            updateCase.setId(updateBeforeCase.getId());
            updateCase.setChargeBackType(caseDTO.getChargebackType());
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateById(updateCase);

            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                //修改前的s3Key集合
                List<String> updateBeforeS3Keys = AttachmentUtil.getKeysByAbbrAttachmentArrayJson(updateBeforeCase.getAttachments(), objectMapper);
                //修改后的s3Key集合
                List<String> updateAfterS3Keys = AttachmentUtil.getKeysByAbbrAttachmentArrayJson(caseDTO.getAttachments(), objectMapper);
                //删除的附件 = 修改前的附件集合 - 修改后的附件集合
                List<String> delS3Keys = Lists.newArrayList(updateBeforeS3Keys);
                delS3Keys.removeAll(updateAfterS3Keys);
                S3Utils.batchDeleteFile(region,bucketMap.get(S3BucketFunctionCons.CHARGEBACK),delS3Keys);
            });
        }

        /**
         * 忽略 case
         *
         * @param cs     case
         * @param userId 操作人
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        public void ignore(CbCase cs, int userId) {
            // draft 状态 且 未送审/审批拒绝时 可以忽略
            List<Integer> effectiveApproveStatuses = Lists.newArrayList(ApproveStatusEnum.PENDING.getStatus(),
                    ApproveStatusEnum.REJECTED.getStatus());
            if (cs.getStatus() != CaseStatusEnum.DRAFT.getCode() || !effectiveApproveStatuses.contains(cs.getApproveStatus())) {
                throw new BadRequestException(CaseErrorCons.OPERATION_FAIL_STATUS_MUST_DRAFT,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The operation can only be carried out when the draft status has not been published for approval or when the approval has been rejected.");
            }

            CbCase updateCase = new CbCase();
            updateCase.setId(cs.getId());
            updateCase.setStatus(CaseStatusEnum.IGNORED.getCode());
            updateCase.setStatusSetBy(userId);
            updateCase.setStatusSetTime(DateTimeUtils.nowTimestamp());
            updateById(updateCase);
        }

        /**
         * 撤回忽略
         *
         * @param cs     case
         * @param userId 操作人
         * @author yyt
         * @date 2024/5/7
         **/
        @Override
        public void revocationIgnore(CbCase cs, int userId) {
            // ignore 状态才可以撤回忽略
            if (cs.getStatus() != CaseStatusEnum.IGNORED.getCode()) {
                throw new BadRequestException(CaseErrorCons.OPERATION_FAIL_STATUS_MUST_IGNORED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "Only by ignoring the status can the ignore be revoked");
            }

            CbCase updateCase = new CbCase();
            updateCase.setId(cs.getId());
            updateCase.setStatus(CaseStatusEnum.DRAFT.getCode());
            updateCase.setStatusSetBy(BaseConstant.ZERO);
            updateCase.setStatusSetTime(BaseConstant.LONG_ZERO);
            updateById(updateCase);
        }

        /**
         * case 详情
         *
         * @param cs case
         * @return case 详情
         * @author yyt
         * @date 2023/8/30
         */
        @Override
        public CaseDetailDTO getCaseDetail(CbCase cs) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(cs.getPropertyId());

            CaseDetailDTO caseDetailDTO = new CaseDetailDTO();

            CaseInfoDTO caseInfoDTO = beanMapper.map(cs, CaseInfoDTO.class);
            caseInfoDTO.setIsExistCbAdj(cbAdjService.countByCaseId(cs.getId()) > 0);
            caseDetailDTO.setCaseInfo(caseInfoDTO);
            // 设置 case 超期时间 / adj 超期时间（邮件预览使用）
            CbCaseBankOverTimeDay overTimeDay = cbCaseBankOverTimeDayService.get(cs.getPropertyId(), cs.getVendor());
            caseDetailDTO.getCaseInfo().setReplyBy(Objects.isNull(overTimeDay) ? null :
                    DateTimeUtils.dateNow(propertyDTO.getTimezone()).plusDays(overTimeDay.getDay()).toEpochSecond());
            CbCaseApproveLevel approveLevel = cbCaseApproveLevelService.getById(cs.getLevelId());
            caseDetailDTO.getCaseInfo().setApproveIndex(Objects.isNull(approveLevel) ? null : approveLevel.getApproveIndex());

            Long caseId = cs.getId();
            List<CbCasePublishLog> publishLogs = cbCasePublishLogService.list(new QueryWrapper<CbCasePublishLog>().lambda()
                    .eq(CbCasePublishLog::getCaseId, caseId));
            List<CbCaseApproveLog> approveLogs = cbCaseApproveLogService.list(new QueryWrapper<CbCaseApproveLog>().lambda()
                    .eq(CbCaseApproveLog::getCaseId, caseId));
            Map<Integer, CbCaseAction> actionByStage = cbCaseActionService.list(Lists.newArrayList(caseId), null).stream()
                    .collect(Collectors.toMap(CbCaseAction::getStage, v -> v));
            CbCaseResult caseResult = cbCaseResultService.getCaseResult(cs.getId());
            List<CbCaseResultLog> resultLogs = Collections.emptyList();
            if (Objects.nonNull(caseResult)) {
                resultLogs = cbCaseResultLogService.list(new QueryWrapper<CbCaseResultLog>().lambda()
                        .eq(CbCaseResultLog::getCaseId, cs.getId()));
            }

            Map<Integer, String> nameById = extractionOperator(cs, publishLogs, approveLogs, actionByStage.get(StageCons.OVERDUE), resultLogs);

            Map<Integer, List<CbCasePublishLog>> publishLogByStage = publishLogs.stream()
                    .collect(Collectors.groupingBy(CbCasePublishLog::getStage));
            Map<Integer, Map<Long, List<CbCaseApproveLog>>> approveLogByStage = approveLogs.stream()
                    .collect(Collectors.groupingBy(CbCaseApproveLog::getStage,
                            Collectors.groupingBy(CbCaseApproveLog::getBatchId)));

            Map<Integer, String> zeroLevelNameByStage = cbCaseApproveLevelService.listZeroLevel(cs.getPropertyId()).stream()
                    .collect(toMap(CbCaseApproveLevel::getStage, CbCaseApproveLevel::getName));

            caseDetailDTO.setInitialCase(packageInitialCase(cs,
                    publishLogByStage.getOrDefault(StageCons.INITIAL_CASE, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.INITIAL_CASE, Collections.emptyMap()), nameById, zeroLevelNameByStage));

            caseDetailDTO.setHotelOperation(packageHotelOperation(cs, actionByStage.get(StageCons.HOTEL_OPERATION),
                    publishLogByStage.getOrDefault(StageCons.HOTEL_OPERATION, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.HOTEL_OPERATION, Collections.emptyMap()), nameById, zeroLevelNameByStage));

            caseDetailDTO.setOverdue(packageOverdue(actionByStage.get(StageCons.OVERDUE),
                    publishLogByStage.getOrDefault(StageCons.OVERDUE, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.OVERDUE, Collections.emptyMap()), nameById, zeroLevelNameByStage));

            caseDetailDTO.setPortal(packagePortal(caseResult, resultLogs,
                    publishLogByStage.getOrDefault(StageCons.PORTAL, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.PORTAL, Collections.emptyMap()), nameById, zeroLevelNameByStage));

            caseDetailDTO.setPatch(packagePatch(cs, actionByStage.get(StageCons.PATCH),
                    publishLogByStage.getOrDefault(StageCons.PATCH, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.PATCH, Collections.emptyMap()), nameById, zeroLevelNameByStage));

            FinalDTO finalDTO = new FinalDTO();
            finalDTO.setPublishLogs(packagePublishLog(publishLogByStage.getOrDefault(StageCons.FINAL, Collections.emptyList()),
                    approveLogByStage.getOrDefault(StageCons.FINAL, Collections.emptyMap()), nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.FINAL, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            caseDetailDTO.setFinalInfo(finalDTO);

            return caseDetailDTO;
        }

        /**
         * 提取操作人
         *
         * @param cs          case
         * @param publishLogs 通知记录
         * @param approveLogs 审批记录
         * @param caseAction  case action
         * @param resultLogs  result log
         * @return 操作人
         * @author yyt
         * @date 2023/8/30
         */
        private Map<Integer, String> extractionOperator(CbCase cs, List<CbCasePublishLog> publishLogs, List<CbCaseApproveLog> approveLogs,
                                                        CbCaseAction caseAction, List<CbCaseResultLog> resultLogs) {
            Set<Integer> userIds = Sets.newHashSetWithExpectedSize(publishLogs.size() + approveLogs.size());
            if (CollectionUtils.isNotEmpty(publishLogs)) {
                userIds.addAll(publishLogs.stream().map(CbCasePublishLog::getNotifiedBy).collect(Collectors.toSet()));
            }
            if (CollectionUtils.isNotEmpty(approveLogs)) {
                userIds.addAll(approveLogs.stream().map(CbCaseApproveLog::getApproveBy).collect(Collectors.toSet()));
            }
            if (Objects.nonNull(caseAction)) {
                userIds.add(caseAction.getCreatedBy());
            }
            if (CollectionUtils.isNotEmpty(resultLogs)) {
                userIds.addAll(resultLogs.stream().map(CbCaseResultLog::getCreatedBy).collect(Collectors.toSet()));
            }
            if (cs.getHotelOperationBy() > 0) {
                userIds.add(cs.getHotelOperationBy());
            }
            if (cs.getPatchBy() > 0) {
                userIds.add(cs.getPatchBy());
            }

            return userInternalService.getNameMapByIds(propertyI18nRedisService.get(cs.getPropertyId()).getTenantId(), userIds);
        }

        /**
         * 封装 initial case
         *
         * @param cs                   case
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelNameByStage 处理人层级名称
         * @return initial case
         * @author yyt
         * @date 2023/8/30
         */
        private InitialCaseDTO packageInitialCase(CbCase cs, List<CbCasePublishLog> publishLogs,
                                                  Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                                  Map<Integer, String> nameById, Map<Integer, String> zeroLevelNameByStage) {
            InitialCaseDTO initialCaseDTO = beanMapper.map(cs, InitialCaseDTO.class);

            // 设置相同 case Number，不包含当前 case
            Integer propertyId = cs.getPropertyId();
            List<CbCase> cases = list(packageSearchSameCaseNumberCondition(propertyId, cs.getCaseNumber(), cs.getId())
                    .select(CbCase::getId, CbCase::getCaseNumber, CbCase::getOrderNumber));
            cases.sort(Comparator.comparing(CbCase::getOrderNumber));
            initialCaseDTO.setSameCaseNumberList(beanMapper.mapList(cases, RelevanceCaseDTO.class));

            // 设置相同 card number 或 guest name 集合，不包含当前 case number 的数据，考虑数据可能会多，只显示 5个，超过显示 more，点击 more，进入 other 列表页面
            Page<CbCase> page = page(new Page<>(1, 6),
                    packageSearchOtherCondition(propertyI18nRedisService.get(propertyId), null, null,
                            cs, true));
            String cardNumber = cs.getCardNumber();
            String guestFirstName = cs.getGuestFirstName();
            String guestLastName = cs.getGuestLastName();
            initialCaseDTO.setOthersList(beanMapper.mapList(page.getRecords(), RelevanceCaseDTO.class,
                    (item, relevanceCaseDTO) -> {
                        if (StringUtils.isNotEmpty(item.getGuestFirstName()) && StringUtils.isNotEmpty(item.getGuestLastName())) {
                            if (item.getCardNumber().equals(cardNumber)) {
                                if (item.getGuestFirstName().equals(guestFirstName) && item.getGuestLastName().equals(guestLastName)) {
                                    relevanceCaseDTO.setType(CaseOthersTypeEnum.SAME_CARD_NUMBER_GUEST_NAME.getCode());
                                } else {
                                    relevanceCaseDTO.setType(CaseOthersTypeEnum.SAME_CARD_NUMBER.getCode());
                                }
                            } else {
                                relevanceCaseDTO.setType(CaseOthersTypeEnum.SAME_GUEST_NAME.getCode());
                            }
                        } else {
                            relevanceCaseDTO.setType(CaseOthersTypeEnum.SAME_CARD_NUMBER.getCode());
                        }
                    }));

            initialCaseDTO.setPublishLogs(packagePublishLog(publishLogs, approveLogsByBatchId, nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.INITIAL_CASE, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            return initialCaseDTO;
        }

        /**
         * 封装 通知记录
         *
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelName        处理人层级名称
         * @return 通知记录
         * @author yyt
         * @date 2023/8/30
         */
        private List<CasePublishLogDTO> packagePublishLog(List<CbCasePublishLog> publishLogs,
                                                          Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                                          Map<Integer, String> nameById, String zeroLevelName) {
            return publishLogs.stream().sorted(Comparator.comparing(CbCasePublishLog::getNotifiedTime).reversed())
                    .map(item -> {
                        CasePublishLogDTO casePublishLogDTO = new CasePublishLogDTO();
                        casePublishLogDTO.setId(item.getId());
                        casePublishLogDTO.setLevelName(zeroLevelName);
                        casePublishLogDTO.setNotifiedBy(item.getNotifiedBy());
                        casePublishLogDTO.setNotifiedByName(nameById.get(item.getNotifiedBy()));
                        casePublishLogDTO.setNotifiedTime(item.getNotifiedTime());
                        casePublishLogDTO.setApproveLog(approveLogsByBatchId.getOrDefault(item.getBatchId(), Collections.emptyList())
                                .stream().map(approveLog -> {
                                    CaseApproveLogDTO approveLogDTO = beanMapper.map(approveLog, CaseApproveLogDTO.class);
                                    approveLogDTO.setApproveByName(nameById.get(approveLog.getApproveBy()));
                                    approveLogDTO.setLevelName(approveLog.getName());
                                    return approveLogDTO;
                                }).collect(Collectors.toList()));
                        casePublishLogDTO.setOperationType(item.getOperationType());
                        return casePublishLogDTO;
                    }).collect(Collectors.toList());
        }

        /**
         * 封装 hotel operation
         *
         * @param cs                   case
         * @param caseAction           case action
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelNameByStage 处理人层级名称
         * @return hotel operation
         * @author yyt
         * @date 2023/8/30
         */
        private HotelOperationDTO packageHotelOperation(CbCase cs, CbCaseAction caseAction, List<CbCasePublishLog> publishLogs,
                                                        Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                                        Map<Integer, String> nameById, Map<Integer, String> zeroLevelNameByStage) {
            HotelOperationDTO hotelOperationDTO = new HotelOperationDTO();
            if (Objects.isNull(caseAction)) {
                if (cs.getStage() >= StageCons.HOTEL_OPERATION) {
                    hotelOperationDTO.setNotifiedBy(cs.getHotelOperationBy());
                    hotelOperationDTO.setNotifiedByName(nameById.get(cs.getHotelOperationBy()));
                    hotelOperationDTO.setNotifiedTime(cs.getHotelOperationTime());
                    hotelOperationDTO.setReplayBy(cs.getReplyBy());
                }
                return hotelOperationDTO;
            }

            hotelOperationDTO = beanMapper.map(caseAction, HotelOperationDTO.class);
            hotelOperationDTO.setNotifiedBy(cs.getHotelOperationBy());
            hotelOperationDTO.setNotifiedByName(nameById.get(cs.getHotelOperationBy()));
            hotelOperationDTO.setNotifiedTime(cs.getHotelOperationTime());
            hotelOperationDTO.setReplayBy(cs.getReplyBy());
            hotelOperationDTO.setPublishLogs(packagePublishLog(publishLogs, approveLogsByBatchId, nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.HOTEL_OPERATION, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            return hotelOperationDTO;
        }

        /**
         * 封装 patch
         *
         * @param cs                   case
         * @param caseAction           case action
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelNameByStage 处理人层级名称
         * @return hotel operation
         * @author yyt
         * @date 2023/9/21
         */
        private HotelOperationDTO packagePatch(CbCase cs, CbCaseAction caseAction, List<CbCasePublishLog> publishLogs,
                                                        Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                                        Map<Integer, String> nameById, Map<Integer, String> zeroLevelNameByStage) {
            HotelOperationDTO hotelOperationDTO = new HotelOperationDTO();
            if (Objects.isNull(caseAction)) {
                if (cs.getStage() >= StageCons.PATCH) {
                    hotelOperationDTO.setNotifiedBy(cs.getPatchBy());
                    hotelOperationDTO.setNotifiedByName(nameById.get(cs.getPatchBy()));
                    hotelOperationDTO.setNotifiedTime(cs.getPatchTime());
                    hotelOperationDTO.setReplayBy(cs.getAdjReplyBy());
                }
                return hotelOperationDTO;
            }

            hotelOperationDTO = beanMapper.map(caseAction, HotelOperationDTO.class);
            hotelOperationDTO.setNotifiedBy(cs.getPatchBy());
            hotelOperationDTO.setNotifiedByName(nameById.get(cs.getPatchBy()));
            hotelOperationDTO.setNotifiedTime(cs.getPatchTime());
            hotelOperationDTO.setReplayBy(cs.getAdjReplyBy());
            hotelOperationDTO.setPublishLogs(packagePublishLog(publishLogs, approveLogsByBatchId, nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.PATCH, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            return hotelOperationDTO;
        }

        /**
         * 封装 overdue
         *
         * @param caseAction           case action
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelNameByStage 处理人层级名称
         * @return hotel operation
         * @author yyt
         * @date 2023/8/30
         */
        private OverdueDTO packageOverdue(CbCaseAction caseAction, List<CbCasePublishLog> publishLogs,
                                          Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                          Map<Integer, String> nameById, Map<Integer, String> zeroLevelNameByStage) {
            if (Objects.isNull(caseAction)) {
                return null;
            }

            OverdueDTO overdueDTO = new OverdueDTO();
            overdueDTO.setAction(caseAction.getAction());
            overdueDTO.setCreatedBy(caseAction.getCreatedBy());
            overdueDTO.setCreatedByName(nameById.get(caseAction.getCreatedBy()));
            overdueDTO.setCreatedTime(caseAction.getCreatedTime());
            overdueDTO.setPublishLogs(packagePublishLog(publishLogs, approveLogsByBatchId, nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.OVERDUE, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            return overdueDTO;
        }

        /**
         * 封装 portal
         *
         * @param caseResult           case result
         * @param resultLogs           case result log
         * @param publishLogs          通知记录
         * @param approveLogsByBatchId 审批记录
         * @param nameById             操作人
         * @param zeroLevelNameByStage 处理人层级名称
         * @return portal
         * @author yyt
         * @date 2023/8/30
         */
        private PortalDTO packagePortal(CbCaseResult caseResult, List<CbCaseResultLog> resultLogs,
                                        List<CbCasePublishLog> publishLogs,
                                        Map<Long, List<CbCaseApproveLog>> approveLogsByBatchId,
                                        Map<Integer, String> nameById, Map<Integer, String> zeroLevelNameByStage) {
            if (Objects.isNull(caseResult)) {
                return null;
            }

            PortalDTO portalDTO = beanMapper.map(caseResult, PortalDTO.class);
            portalDTO.setResultLogs(resultLogs.stream().sorted(Comparator.comparing(CbCaseResultLog::getCreatedTime).reversed())
                    .map(item -> {
                        CaseResultLogDTO caseResultLogDTO = new CaseResultLogDTO();
                        caseResultLogDTO.setResult(item.getResult());
                        caseResultLogDTO.setCreatedBy(item.getCreatedBy());
                        caseResultLogDTO.setCreatedByName(nameById.get(item.getCreatedBy()));
                        caseResultLogDTO.setCreatedTime(item.getCreatedTime());
                        return caseResultLogDTO;
                    }).collect(Collectors.toList()));
            portalDTO.setPublishLogs(packagePublishLog(publishLogs, approveLogsByBatchId, nameById,
                    zeroLevelNameByStage.getOrDefault(StageCons.PORTAL, i18nMessageUtils.message(CaseNotifyCons.HANDLER, LocaleContextHolder.getLocale()))));
            return portalDTO;
        }

        /**
         * close
         *
         * @param cs case
         * @author yyt
         * @date 2023/9/7
         */
        @Override
        public void close(CbCase cs) {
            CbCaseResult caseResult = cbCaseResultService.getCaseResult(cs.getId());
            if (cs.getStage() != StageCons.FINAL || cs.getStatus() != BaseConstant.ZERO
                    || (cs.getResultFinalDate() == BaseConstant.LONG_ZERO && !cs.getNonChargeback())
                    || (cs.getResultFinalDate() == BaseConstant.LONG_ZERO && Objects.nonNull(caseResult) && !caseResult.getNoWithdrawal())) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_UPDATE_FAILED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Failed to update the data that maybe removed or updated by others");
            }

            CbCase updateCase = new CbCase();
            updateCase.setStatus(CaseStatusEnum.CLOSE.getCode());
            updateCase.setStatusSetBy(sessionContext.getLoginUserId());
            updateCase.setStatusSetTime(DateTimeUtils.nowTimestamp());
            updateCase.setId(cs.getId());
            updateById(updateCase);
        }

        /**
         * 变更 stage
         *
         * @param cs       case
         * @param newStage stage（1:init，2:hotel operation，3:overdue，4:portal，5:patch，6:final）
         * @author yyt
         * @date 2023/9/7
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void changeStage(CbCase cs, int newStage) {
            Integer currentStage = cs.getStage();
            if (currentStage >= newStage) {
                // 变更阶段，从后往前变
                changeStageToBefore(cs, newStage, currentStage);
            } else {
                // 从前往后
                changeStageToAfter(cs, newStage, currentStage);
            }
        }

        /**
         * 变更阶段至前
         *
         * @param cs           case
         * @param newStage     新阶段（1:init，2:hotel operation，3:overdue，4:portal，5:patch，6:final）
         * @param currentStage 当前阶段
         * @author yyt
         * @date 2023/9/7
         */
        private void changeStageToBefore(CbCase cs, int newStage, Integer currentStage) {
            List<Integer> stagesForLog;
            List<Integer> stagesForAction;
            // 变更阶段，从后往前变
            Long caseId = cs.getId();
            CbCase updateCase = new CbCase();
            int newStatus = BaseConstant.ZERO;
            switch (newStage) {
                case StageCons.INITIAL_CASE:
                    newStatus = CaseStatusEnum.DRAFT.getCode();

                    // 删除 adj
                    cbAdjService.handleDeleteAdj(cs);

                    // 清空推送记录，清空审批日志
                    cbCasePublishLogService.clearLog(caseId, null);
                    cbCaseApproveLogService.clearLog(caseId, null);

                    // 清空 action 数据
                    cbCaseActionService.clearAction(cs, null);
                    // 删除黑名单
                    handleSendDeleteBlackEvent(cs);

                    // 清空 result
                    cbCaseResultService.clearResult(caseId);

                    updateCase.setReplyBy(BaseConstant.LONG_ZERO);
                    updateCase.setAdjReplyBy(BaseConstant.LONG_ZERO);
                    updateCase.setHotelOperationBy(BaseConstant.ZERO);
                    updateCase.setHotelOperationTime(BaseConstant.LONG_ZERO);
                    updateCase.setPatchBy(BaseConstant.ZERO);
                    updateCase.setPatchTime(BaseConstant.LONG_ZERO);

                    break;
                case StageCons.HOTEL_OPERATION:
                    // 删除 adj
                    cbAdjService.handleDeleteAdj(cs);

                    // 清空推送记录，清空审批日志
                    stagesForLog = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE,
                            StageCons.PORTAL, StageCons.PATCH, StageCons.FINAL);
                    cbCasePublishLogService.clearLog(caseId, stagesForLog);
                    cbCaseApproveLogService.clearLog(caseId, stagesForLog);

                    // 清空 action 数据
                    stagesForAction = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE,
                            StageCons.PATCH);
                    cbCaseActionService.clearAction(cs, stagesForAction);
                    updateCase.setAction(0);

                    // 删除黑名单
                    handleSendDeleteBlackEvent(cs);

                    // 清空 result
                    cbCaseResultService.clearResult(caseId);

                    updateCase.setPatchBy(BaseConstant.ZERO);
                    updateCase.setPatchTime(BaseConstant.LONG_ZERO);
                    updateCase.setAdjReplyBy(BaseConstant.LONG_ZERO);
                    break;
                case StageCons.OVERDUE:
                    if (currentStage != StageCons.OVERDUE) {
                        // 检查是否有 overdue 阶段
                        CbCaseAction overdueAction = cbCaseActionService.getCaseAction(caseId, StageCons.OVERDUE);
                        if (Objects.isNull(overdueAction)) {
                            throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                                    i18nMessageUtils.getErrorParamMapNotNeedTranslate("overdue action"),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the overdue action is empty");
                        }
                    }

                    // 删除 adj
                    cbAdjService.handleDeleteAdj(cs);

                    // 清空推送记录，清空审批日志
                    stagesForLog = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE, StageCons.PORTAL, StageCons.PATCH, StageCons.FINAL);
                    cbCasePublishLogService.clearLog(caseId, stagesForLog);
                    cbCaseApproveLogService.clearLog(caseId, stagesForLog);

                    stagesForAction = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE, StageCons.PATCH);
                    // 清空 action 数据
                    cbCaseActionService.clearAction(cs, stagesForAction);

                    // 清空 result
                    cbCaseResultService.clearResult(caseId);

                    updateCase.setAction(0);
                    updateCase.setPatchBy(BaseConstant.ZERO);
                    updateCase.setPatchTime(BaseConstant.LONG_ZERO);
                    updateCase.setAdjReplyBy(BaseConstant.LONG_ZERO);
                    break;
                case StageCons.PORTAL:
                    if (currentStage != StageCons.PORTAL) {
                        CbCaseResult caseResult = cbCaseResultService.getCaseResult(caseId);
                        if (Objects.isNull(caseResult)) {
                            throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                                    i18nMessageUtils.getErrorParamMapNotNeedTranslate("case result"),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the case result is empty");
                        }
                    }

                    // 删除 adj
                    cbAdjService.handleDeletePatchStageAdj(cs);

                    // 清空推送记录，清空审批日志
                    stagesForLog = Lists.newArrayList(StageCons.PORTAL, StageCons.PATCH, StageCons.FINAL);
                    cbCasePublishLogService.clearLog(caseId, stagesForLog);
                    cbCaseApproveLogService.clearLog(caseId, stagesForLog);

                    // 清空 action 数据
                    cbCaseActionService.clearAction(cs, Lists.newArrayList(StageCons.PATCH));
                    // 清空 result
                    cbCaseResultService.clearResult(caseId);

                    updateCase.setPatchBy(BaseConstant.ZERO);
                    updateCase.setPatchTime(BaseConstant.LONG_ZERO);
                    updateCase.setAdjReplyBy(BaseConstant.LONG_ZERO);
                    break;
                case StageCons.PATCH:
                    if (currentStage != StageCons.PATCH) {
                        CbCaseAction caseAction = cbCaseActionService.getCaseAction(caseId, StageCons.PATCH);
                        if (Objects.isNull(caseAction)) {
                            throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                                     i18nMessageUtils.getErrorParamMapNotNeedTranslate("patch action"),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the patch action is empty");
                        }
                    }

                    // 删除 adj
                    cbAdjService.handleDeletePatchStageAdj(cs);

                    // 清空推送记录，清空审批日志
                    stagesForLog = Lists.newArrayList(StageCons.PATCH, StageCons.FINAL);
                    cbCasePublishLogService.clearLog(caseId, stagesForLog);
                    cbCaseApproveLogService.clearLog(caseId, stagesForLog);

                    // 清空 last 阶段填写的 result 信息
                    if (cs.getStage() == StageCons.FINAL) {
                        cbCaseResultService.clearResultForLastStage(caseId);
                    }

                    // 清空 action 数据
                    cbCaseActionService.clearAction(cs, Lists.newArrayList(StageCons.PATCH));

                    break;
                case StageCons.FINAL:
                    // 清空推送记录，清空审批日志
                    cbCasePublishLogService.clearLog(caseId, Lists.newArrayList(StageCons.FINAL));
                    cbCaseApproveLogService.clearLog(caseId, Lists.newArrayList(StageCons.FINAL));
                    break;
            }

            Integer propertyId = cs.getPropertyId();
            CbCaseApproveLevel zeroLevel = cbCaseApproveLevelService.checkZeroLevelExist(propertyId, newStage);

            // 修改 case 信息
            updateCase.setId(caseId);
            updateCase.setStatus(newStatus);
            updateCase.setStatusSetBy(sessionContext.getLoginUserId());
            updateCase.setStatusSetTime(DateTimeUtils.nowTimestamp());
            updateCase.setStage(newStage);
            updateCase.setLevelId(zeroLevel.getId());
            updateCase.setBatchId(BaseConstant.LONG_ZERO);
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateById(updateCase);

            // 清除下 状态演变记录，防止酒店表态阶段推送下个阶段时出现问题
            cbCaseStageEvolutionService.remove(new QueryWrapper<CbCaseStageEvolution>().lambda()
                    .eq(CbCaseStageEvolution::getCaseId, caseId));
        }

        /**
         * 处理发送删除黑名单事件
         *
         * @param cs case
         * @author yyt
         * @date 2024/4/30
         **/
        private void handleSendDeleteBlackEvent(CbCase cs) {
            CbCaseSendBlackRecord sendBlackRecord = cbCaseSendBlackRecordService.getOne(new QueryWrapper<CbCaseSendBlackRecord>().lambda()
                    .eq(CbCaseSendBlackRecord::getCaseId, cs.getId()));
            if (Objects.nonNull(sendBlackRecord)) {
                chargebackBlacklistSendEventService.sendDeleteBlackEvent(cs);
            }
        }

        /**
         * 变更阶段至后
         *
         * @param cs           case
         * @param newStage     新阶段 （1:init，2:hotel operation，3:overdue，4:portal，5:patch，6:final）
         * @param currentStage 当前阶段
         * @author yyt
         * @date 2023/9/7
         */
        private void changeStageToAfter(CbCase cs, int newStage, Integer currentStage) {
            Long caseId = cs.getId();
            Integer propertyId = cs.getPropertyId();
            CbCase updateCase = new CbCase();
            List<Integer> stages = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.FINAL);
            switch (currentStage) {
                case StageCons.INITIAL_CASE:
                    // 只能跳 酒店表态 / final 阶段
                    if (!stages.contains(newStage)) {
                        throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                i18nMessageUtils.getErrorParamMapNotNeedTranslate( "new stage", stages),
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                "the new stage must in [hotel operation, final]");
                    }
                    break;
                case StageCons.HOTEL_OPERATION:
                    /*
                     * 酒店没有表态时，可以跳 overdue / portal / final
                     * 有表态
                     *  - 未 overdue
                     * 		- accept -> final / dispute -> portal / final
                     * 	- overdue
                     * 		- overdue accept，酒店 accept 只能跳 final，酒店 dispute 能跳 patch / final
                     * 		- overdue dispute，都能跳 portal / final
                     */
                    // 未表态
                    if (cs.getStatus() == BaseConstant.ZERO) {
                        stages = Lists.newArrayList(StageCons.OVERDUE, StageCons.PORTAL, StageCons.FINAL);
                        // 只能跳 overdue / portal / final 阶段
                        if (!stages.contains(newStage)) {
                            throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                     i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                    "the new stage must in [overdue, portal, final]");
                        }
                    } else {
                        Map<Integer, Integer> actionByStage = cbCaseActionService.list(Lists.newArrayList(caseId),
                                        Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.OVERDUE)).stream()
                                .collect(Collectors.toMap(CbCaseAction::getStage, CbCaseAction::getAction));
                        Integer hotelOperationChooseAction = actionByStage.get(StageCons.HOTEL_OPERATION);
                        Integer overdueChooseAction = actionByStage.get(StageCons.OVERDUE);
                        // 未 overdue
                        if (Objects.isNull(overdueChooseAction)) {
                            if (hotelOperationChooseAction == CaseActionEnum.ACCEPT.getCode()) {
                                // 只能跳 final 阶段
                                stages =  Lists.newArrayList(StageCons.FINAL);
                                if (!stages.contains(newStage)) {
                                    throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                             i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                            "the new stage must in [final]");
                                }
                            } else {
                                // 只能跳 portal / final 阶段
                                stages = Lists.newArrayList(StageCons.PORTAL, StageCons.FINAL);
                                if (!stages.contains(newStage)) {
                                    throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                             i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                            "the new stage must in [portal, final]");
                                }
                            }
                        } else {
                            /*
                             * - overdue
                             *   - overdue accept，酒店 accept 只能跳 final，酒店 dispute 能跳 patch / final
                             *   - overdue dispute，都能跳 portal / final
                             */
                            if (overdueChooseAction == CaseActionEnum.ACCEPT.getCode()) {
                                if (hotelOperationChooseAction == CaseActionEnum.ACCEPT.getCode()) {
                                    // 只能跳 final 阶段
                                     stages = Lists.newArrayList(StageCons.FINAL);
                                    if (!stages.contains(newStage)) {
                                        throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                                 i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                                "the new stage must in [final]");
                                    }
                                } else {
                                    // 只能跳 patch / final 阶段
                                    stages = Lists.newArrayList(StageCons.PATCH, StageCons.FINAL);
                                    if (!stages.contains(newStage)) {
                                        throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                                i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                                "the new stage must in [patch, final]");
                                    }
                                }
                            } else {
                                // 只能跳 portal / final 阶段
                                stages = Lists.newArrayList(StageCons.PORTAL, StageCons.FINAL);
                                if (!stages.contains(newStage)) {
                                    throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                            i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                            "the new stage must in [portal, final]");
                                }
                            }
                        }
                    }

                    // 发送审批 adj 事件
                    cbAdjService.handleApproveAdjBatch(Lists.newArrayList(cs));
                    break;
                case StageCons.OVERDUE:
                    CbCaseAction overdueAction = cbCaseActionService.getCaseAction(caseId, StageCons.OVERDUE);
                    if (overdueAction.getAction() == CaseActionEnum.ACCEPT.getCode()) {
                        // accept 时，只能调 hotel operation (删除 overdue 信息) / final
                         stages = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.FINAL);
                        if (!stages.contains(newStage)) {
                            throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                     i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                    "the new stage must in [hotel operation, final]");
                        }

                    } else {
                        // dispute 时，只能调 hotel operation (删除 overdue 信息) / portal / final
                        stages = Lists.newArrayList(StageCons.HOTEL_OPERATION, StageCons.PORTAL, StageCons.FINAL);
                        if (!stages.contains(newStage)) {
                            throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                    i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                    "the new stage must in [hotel operation, portal, final]");
                        }
                    }

                    // 清除 overdue 数据
                    cbCaseActionService.removeById(overdueAction.getId());
                    updateCase.setAction(0);
                    // 清空推送记录，清空审批日志
                    List<Integer> stagesForLog = Lists.newArrayList(StageCons.OVERDUE);
                    cbCasePublishLogService.clearLog(caseId, stagesForLog);
                    cbCaseApproveLogService.clearLog(caseId, stagesForLog);
                    break;
                case StageCons.PORTAL:
                    // 如果 有酒店表态 且选择的 accept，则只能跳 final
                    CbCaseAction hotelOperationAction = cbCaseActionService.getCaseAction(caseId, StageCons.HOTEL_OPERATION);
                    if (Objects.nonNull(hotelOperationAction) && hotelOperationAction.getAction() == CaseActionEnum.ACCEPT.getCode()) {
                         stages = Lists.newArrayList(StageCons.FINAL);
                        if (!stages.contains(newStage)) {
                            throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                     i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                    BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                    "the new stage must in [final]");
                        }
                    } else {
                        // 否则根据 result ，选择 win，只能调 final，选择 lose，能跳 patch / final，
                        // 选择其他（3:Frozen，4:Processed），只能跳 final
                        // 如果没有选择 result，那只能调 final
                        CbCaseResult caseResult = cbCaseResultService.getCaseResult(caseId);
                        if (Objects.nonNull(caseResult) && caseResult.getResult() == CaseResultEnum.LOSS.getCode()) {
                            stages = Lists.newArrayList(StageCons.PATCH, StageCons.FINAL);
                            if (!stages.contains(newStage)) {
                                throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                        "the new stage must in [patch, final]");
                            }
                        } else {
                            stages = Lists.newArrayList(StageCons.FINAL);
                            if (!stages.contains(newStage)) {
                                throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                        "the new stage must in [final]");
                            }
                        }
                    }
                    break;
                case StageCons.PATCH:
                    // 能跳 final
                    stages = Lists.newArrayList(StageCons.FINAL);
                    if (!stages.contains(newStage)) {
                        throw new BadRequestException(CommonErrorI18nConstant.MUST_IN_LIST,
                                 i18nMessageUtils.getErrorParamMapNotNeedTranslate("new stage", stages),
                                BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                                "the new stage must in [final]");
                    }

                    // 发送审批 adj 事件
                    cbAdjService.handleApproveAdjBatch(Lists.newArrayList(cs));
                    break;
            }

            CbCaseApproveLevel zeroLevel = cbCaseApproveLevelService.checkZeroLevelExist(propertyId, newStage);

            // 修改 case 初始阶段信息
            updateCase.setId(caseId);
            updateCase.setStatus(BaseConstant.ZERO);
            updateCase.setStatusSetBy(sessionContext.getLoginUserId());
            updateCase.setStatusSetTime(DateTimeUtils.nowTimestamp());
            updateCase.setStage(newStage);
            updateCase.setLevelId(zeroLevel.getId());
            updateCase.setBatchId(BaseConstant.LONG_ZERO);
            updateCase.setApproveStatus(ApproveStatusEnum.PENDING.getStatus());
            updateById(updateCase);
        }

        /**
         * 查询没有姓名的 case
         *
         * @param propertyId 资产 id
         * @param trxDate    交易日期
         * @author yyt
         * @date 2023/9/8
         */
        @Override
        public List<CbCase> listNoGuestName(Integer propertyId, Long trxDate) {
            return list(new QueryWrapper<CbCase>().lambda().eq(CbCase::getTrxDate, trxDate)
                    .eq(CbCase::getPropertyId, propertyId).eq(CbCase::getGuestFirstName, StringUtils.EMPTY)
                    .eq(CbCase::getGuestLastName, StringUtils.EMPTY).eq(CbCase::getMoreGuests, StringUtils.EMPTY)
                    .select(CbCase::getId, CbCase::getPropertyId, CbCase::getTrxDate, CbCase::getCardNumberEnd, CbCase::getTrxAmount));
        }

        /**
         * 处理保存 case 数据
         *
         * @param propertyDTO 资产
         * @param userId      操作人
         * @param cases       case
         * @author yyt
         * @date 2023/9/8
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleCaseData(PropertyDTO propertyDTO, int userId, List<CaseListBO> cases) {
            Integer propertyId = propertyDTO.getId();
            ZoneId zoneId = ZoneId.of(propertyDTO.getTimezone());
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_TIME_FORMAT_SLASH);
            long now = DateTimeUtils.nowTimestamp();

            List<CaseIssueDateBO> caseIssueDateBOS = beanMapper.mapList(cases, CaseIssueDateBO.class, (itemBO, item) -> {
                item.setIssueDateLong(LocalDate.parse(itemBO.getIssueDate(), dateTimeFormatter)
                        .atStartOfDay(zoneId).toEpochSecond());
            });
            List<String> existCases = baseMapper.listByCaseNumberAndIssueDate(propertyId, caseIssueDateBOS)
                    .stream().map(CbCase::getUniqueStr).toList();

            InternationalizationNumberDTO number = propertyDTO.getInternationalization().getNumber();
            int dataPrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());
            CbCaseApproveLevel zeroLevel = cbCaseApproveLevelService.checkZeroLevelExist(propertyId, StageCons.INITIAL_CASE);
            List<CbCase> newCases = cases.stream().map(item ->
                            new CbCase().setPropertyId(propertyId).setCaseNumber(item.getCaseNumber())
                                    .setVendor(item.getVendor()).setCardType(item.getCardType())
                                    .setCardNumber(item.getCardNumber())
                                    .setCardNumberEnd(StringUtils.substring(item.getCardNumber(), -4, item.getCardNumber().length()))
                                    .setIssueDate(LocalDate.parse(item.getIssueDate(), dateTimeFormatter)
                                            .atStartOfDay(zoneId).toEpochSecond())
                                    .setDueDate(StringUtils.isBlank(item.getDueDate()) ? BaseConstant.LONG_ZERO :
                                            LocalDate.parse(item.getDueDate(), dateTimeFormatter).atStartOfDay(zoneId).toEpochSecond())
                                    .setTrxDate(LocalDate.parse(item.getTrxDate(), dateTimeFormatter)
                                            .atStartOfDay(zoneId).toEpochSecond())
                                    .setTrxAmount(item.getTrxAmount().setScale(dataPrecision, roundingMode))
                                    .setDisputeAmount(item.getDisputeAmount().setScale(dataPrecision, roundingMode))
                                    .setReason(item.getReason()).setChargeBackType(item.getChargebackType())
                                    .setCardPresent(item.getCardPresent()).setAuthCode(item.getAuthCode())
                                    .setAttachments(item.getAttachments())
                                    .setCreatedBy(userId).setCreatedTime(now).setHandleCase(false)
                                    .setStage(StageCons.INITIAL_CASE).setLevelId(zeroLevel.getId())
                                    .setBatchId(BaseConstant.LONG_ZERO).setApproveStatus(ApproveStatusEnum.PENDING.getStatus())
                                    .setMoreGuests(StringUtils.EMPTY))
                    .filter(item -> !existCases.contains(item.getUniqueStr()))
                    .collect(Collectors.toList());
            if (CollectionUtils.isEmpty(newCases)) {
                return;
            }

            cbJournalService.matchCasesWithJournals(propertyDTO, newCases);

            saveCasesBatch(newCases);

            // 更新 case order number
            resetCaseOrderNumber(propertyId, newCases.stream().map(CbCase::getCaseNumber).distinct().collect(toList()));
        }

        /**
         * 批量保存
         *
         * @param cases case
         * @author yyt
         * @date 2023/9/9
         */
        private void saveCasesBatch(List<CbCase> cases) {
            if (cases.isEmpty()) {
                return;
            }

            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            CbCaseMapper mapper = sqlSession.getMapper(CbCaseMapper.class);
            for (int i = 0; i < cases.size(); i++) {
                CbCase cbCase = cases.get(i);
                mapper.insert(cbCase);

                //每 50 条提交一次
                if ((i + 1) % 50 == 0) {
                    sqlSession.flushStatements();
                }
            }

            sqlSession.flushStatements();
        }

        /**
         * 获取超期 case
         *
         * @param propertyDTO 资产
         * @param stage      阶段
         * @return case
         * @author yyt
         * @date 2023/9/9
         */
        @Override
        public List<CbCase> listNotApprovedOverTimeCase(PropertyDTO propertyDTO, int stage) {
            Long date = DateTimeUtils.dateNowTimestamp(propertyDTO.getTimezone());
            LambdaQueryWrapper<CbCase> queryWrapper = new QueryWrapper<CbCase>().lambda().in(CbCase::getStage, stage)
                    .eq(CbCase::getPropertyId, propertyDTO.getId());
            if (stage == StageCons.HOTEL_OPERATION) {
                queryWrapper.gt(CbCase::getReplyBy, BaseConstant.LONG_ZERO)
                        .lt(CbCase::getReplyBy, date);
            }
            if (stage == StageCons.PATCH) {
                queryWrapper.gt(CbCase::getAdjReplyBy, BaseConstant.LONG_ZERO)
                        .lt(CbCase::getAdjReplyBy, date);
            }
            return list(queryWrapper);
        }

        /**
         * 获取超期 case
         *
         * @param propertyIds 资产
         * @param stage       阶段
         * @param date        时间
         * @return case
         * @author yyt
         * @date 2025/12/9
         */
        @Override
        public List<CbCase> listNotApprovedOverTimeCase(List<Integer> propertyIds, int stage, Long date) {
            LambdaQueryWrapper<CbCase> queryWrapper = new QueryWrapper<CbCase>().lambda()
                    .eq(CbCase::getStage, stage)
                    .in(CbCase::getPropertyId, propertyIds);
            if (stage == StageCons.HOTEL_OPERATION) {
                queryWrapper.gt(CbCase::getReplyBy, BaseConstant.LONG_ZERO)
                        .lt(CbCase::getReplyBy, date);
            }
            if (stage == StageCons.PATCH) {
                queryWrapper.gt(CbCase::getAdjReplyBy, BaseConstant.LONG_ZERO)
                        .lt(CbCase::getAdjReplyBy, date);
            }
            return list(queryWrapper);
        }

        /**
         * 获取 case 当前阶段审批日志
         *
         * @param cs       case
         * @param property 资产
         * @return 审批日志
         * @author yyt
         * @date 2023/9/9
         */
        @Override
        public List<CaseStageApproveLevelDTO> listApproveRecord(CbCase cs, PropertyDTO property) {
            // 审批层级
            Integer propertyId = property.getId();
            Integer stage = cs.getStage();
            List<CbCaseApproveLevel> approveLevels = cbCaseApproveLevelService.listApproveLevel(propertyId, stage);

            //审批层级下审批人
            List<CbCaseApproveLevelUser> approveLevelUsers = Collections.emptyList();
            Map<Long, List<CbCaseApproveLevelUser>> approveUsersByApproveLevel = Collections.emptyMap();
            if (CollectionUtils.isNotEmpty(approveLevels)) {
                List<Long> levelIds = approveLevels.stream().map(CbCaseApproveLevel::getId).collect(toList());
                approveLevelUsers = cbCaseApproveLevelUserService.listApproveUser(propertyId, levelIds);
                approveUsersByApproveLevel = approveLevelUsers.stream().collect(groupingBy(CbCaseApproveLevelUser::getLevelId));
            }

            // 当前批次的审批日志
            List<CbCaseApproveLog> caseApproveLogs = cbCaseApproveLogService.listStageApproveLog(cs);

            if (CollectionUtils.isEmpty(approveLevels) && CollectionUtils.isEmpty(caseApproveLogs)) {
                return Collections.emptyList();
            }

            // 查询用户信息
            List<Integer> userIds = Lists.newArrayList();
            if (CollectionUtils.isNotEmpty(approveLevelUsers)) {
                userIds.addAll(approveLevelUsers.stream().map(CbCaseApproveLevelUser::getUserId).toList());
            }
            if (CollectionUtils.isNotEmpty(caseApproveLogs)) {
                userIds.addAll(caseApproveLogs.stream().map(CbCaseApproveLog::getApproveBy).toList());

            }
            Map<Integer, UserDTO> userByUserId = Collections.emptyMap();
            if (CollectionUtils.isNotEmpty(userIds)) {
                userByUserId = userInternalService.getUserMapByIds(property.getTenantId(), userIds);
            }

            Map<Long, CbCaseApproveLevel> approveLevelById = approveLevels.stream().collect(Collectors.toMap(CbCaseApproveLevel::getId, v -> v));
            if (cs.getApproveStatus() == ApproveStatusEnum.APPROVED.getStatus()) {
                return createApprovedLog(caseApproveLogs, approveLevelById, userByUserId, property.getTimezone());
            } else if (cs.getApproveStatus() == ApproveStatusEnum.REJECTED.getStatus()) {
                return createRejectedLog(caseApproveLogs, approveLevelById, userByUserId, property.getTimezone());
            } else if (cs.getApproveStatus() == ApproveStatusEnum.APPROVEING.getStatus()) {
                return createApprovingLog(cs, caseApproveLogs, approveLevels, approveLevelById, approveUsersByApproveLevel,
                        userByUserId, property.getTimezone());
            } else if(cs.getApproveStatus() == ApproveStatusEnum.PENDING.getStatus()) {
                return createPendingLog(approveLevels, approveUsersByApproveLevel, userByUserId);
            }
            return Collections.emptyList();
        }

        /**
         * 创建审批完成状态下的审批日志
         *
         * @param caseApproveLogs  case 审批日志
         * @param approveLevelById 审批层级
         * @param userByUserId     用户信息
         * @param timezone         时区
         * @return 审批日志
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveLevelDTO> createApprovedLog(List<CbCaseApproveLog> caseApproveLogs,
                                                                 Map<Long, CbCaseApproveLevel> approveLevelById,
                                                                 Map<Integer, UserDTO> userByUserId, String timezone) {
            if (CollectionUtils.isEmpty(caseApproveLogs)) {
                return Collections.emptyList();
            }
            List<CbCaseApproveLevel> levelList = summaryApproveLevels(caseApproveLogs, approveLevelById);
            Map<Long, List<CbCaseApproveLog>> approveLogByApproveLevel = caseApproveLogs.stream()
                    .collect(groupingBy(CbCaseApproveLog::getLevelId));

            return beanMapper.mapList(levelList, CaseStageApproveLevelDTO.class, item -> {
                item.setApproveStatus(ApproveStatusEnum.APPROVED.getStatus());
                item.setUsers(convertApproveUser(approveLogByApproveLevel.get(item.getId()), userByUserId, timezone));
            });
        }

        /**
         * 创建审批拒绝状态下的审批日志
         *
         * @param caseApproveLogs  case 审批日志
         * @param approveLevelById 审批层级
         * @param userByUserId     用户信息
         * @param timezone         时区
         * @return 审批日志
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveLevelDTO> createRejectedLog(List<CbCaseApproveLog> caseApproveLogs,
                                                                 Map<Long, CbCaseApproveLevel> approveLevelById,
                                                                 Map<Integer, UserDTO> userByUserId, String timezone) {
            List<CbCaseApproveLevel> levelList = summaryApproveLevels(caseApproveLogs, approveLevelById);
            Map<Long, List<CbCaseApproveLog>> approveLogByApproveLevel = caseApproveLogs.stream()
                    .collect(groupingBy(CbCaseApproveLog::getLevelId));

            Integer rejectedApproveIndex = caseApproveLogs.stream().filter(log -> !log.getIsApproved()).findFirst().get().getApproveIndex();

            return beanMapper.mapList(levelList, CaseStageApproveLevelDTO.class, item -> {
                if (item.getApproveIndex() < rejectedApproveIndex) {
                    item.setApproveStatus(ApproveStatusEnum.APPROVED.getStatus());
                } else if (item.getApproveIndex().equals(rejectedApproveIndex)) {
                    item.setApproveStatus(ApproveStatusEnum.REJECTED.getStatus());
                } else {
                    // 拒绝以后的 审批节点不显示
                    return;
                }
                item.setUsers(convertApproveUser(approveLogByApproveLevel.get(item.getId()), userByUserId, timezone));
            });
        }

        /**
         * 创建审批中状态下的审批日志
         *
         * @param cs                           case
         * @param caseApproveLogs              case 审批日志
         * @param approveLevels                审批层级
         * @param approveLevelById             审批层级
         * @param approveUsersByApproveLevelId 审批层级下审批人
         * @param userByUserId                 用户信息
         * @param timezone                     时区
         * @return 审批日志
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveLevelDTO> createApprovingLog(CbCase cs, List<CbCaseApproveLog> caseApproveLogs,
                                                                  List<CbCaseApproveLevel> approveLevels,
                                                                  Map<Long, CbCaseApproveLevel> approveLevelById,
                                                                  Map<Long, List<CbCaseApproveLevelUser>> approveUsersByApproveLevelId,
                                                                  Map<Integer, UserDTO> userByUserId, String timezone) {
            List<CaseStageApproveLevelDTO> approveLevelList = Lists.newArrayListWithCapacity(approveLevels.size());

            Integer currentApproveIndex = approveLevelById.get(cs.getLevelId()).getApproveIndex();
            // 审批日志以 当前审批索引 分组
            Map<Boolean, List<CbCaseApproveLog>> approveLogsByIsCurrentApproveIndex = caseApproveLogs.stream()
                    .collect(partitioningBy(item -> item.getApproveIndex().equals(currentApproveIndex)));

            // 获取小于当前审批索引的审批日志，这部分审批日志已经审批通过，生成审批通过的树
            approveLevelList.addAll(createApprovedLog(approveLogsByIsCurrentApproveIndex.getOrDefault(false, Collections.emptyList())
                    , approveLevelById, userByUserId, timezone));

            // 当前审批层级的人包括：已经审批的人 + 未审批的人
            List<CaseStageApproveUserDTO> approvingUsers = Lists.newArrayList();
            // 已经审批的人
            List<CbCaseApproveLog> currentApproveIndexApproveLogs = approveLogsByIsCurrentApproveIndex.getOrDefault(true, Collections.emptyList());
            approvingUsers.addAll(convertApproveUser(currentApproveIndexApproveLogs, userByUserId, timezone));
            // 未审批的人
            CbCaseApproveLevel currentApproveLevel = approveLevelById.get(cs.getLevelId());
            List<Integer> alreadyApproveUserIds = currentApproveIndexApproveLogs.stream().map(CbCaseApproveLog::getApproveBy).toList();
            List<CbCaseApproveLevelUser> currentApproveLevelUnApprover = approveUsersByApproveLevelId.getOrDefault(currentApproveLevel.getId(), Collections.emptyList())
                    .stream().filter(item -> !alreadyApproveUserIds.contains(item.getUserId())).collect(toList());
            approvingUsers.addAll(convertApproveUser(currentApproveLevelUnApprover, userByUserId, BaseConstant.ZERO));
            // 生成当前审批层级
            approveLevelList.add(beanMapper.map(currentApproveLevel, CaseStageApproveLevelDTO.class, approveLevelDTO -> {
                approveLevelDTO.setApproveStatus(ApproveStatusEnum.APPROVEING.getStatus());
                approveLevelDTO.setUsers(approvingUsers);
            }));

            // 未审批的审批层级：大于当前审批索引的审批层级集合
            List<CbCaseApproveLevel> unApproveLevelList = approveLevels.stream()
                    .filter(item -> item.getApproveIndex() > currentApproveIndex).collect(toList());
            approveLevelList.addAll(beanMapper.mapList(unApproveLevelList, CaseStageApproveLevelDTO.class, item -> {
                item.setApproveStatus(BaseConstant.ZERO);
                //设置审批配置人员
                item.setUsers(convertApproveUser(approveUsersByApproveLevelId.getOrDefault(item.getId(), Collections.emptyList()), userByUserId, BaseConstant.ZERO));
            }));

            approveLevelList.sort(Comparator.comparing(CaseStageApproveLevelDTO::getApproveIndex));
            return approveLevelList;
        }

        /**
         * 创建待送审状态下的审批日志
         *
         * @param approveLevels              审批层级
         * @param approveUsersByApproveLevel 审批层架下审批人
         * @param userByUserId               用户信息
         * @return 审批日志
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveLevelDTO> createPendingLog(List<CbCaseApproveLevel> approveLevels,
                                                                Map<Long, List<CbCaseApproveLevelUser>> approveUsersByApproveLevel,
                                                                Map<Integer, UserDTO> userByUserId) {
            return beanMapper.mapList(approveLevels, CaseStageApproveLevelDTO.class, item -> {
                item.setApproveStatus(BaseConstant.ZERO);
                //设置审批配置人员
                item.setUsers(convertApproveUser(approveUsersByApproveLevel.getOrDefault(item.getId(), Collections.emptyList()), userByUserId, BaseConstant.ZERO));
            });
        }

        /**
         * 根据审批日志、当前审批层级，汇总出最终的审批层级
         *
         * @param caseApproveLogs  审批日志
         * @param approveLevelById 审批层级
         * @return 审批层级
         * @author yyt
         * @date 2023/9/9
         */
        private List<CbCaseApproveLevel> summaryApproveLevels(List<CbCaseApproveLog> caseApproveLogs,
                                                              Map<Long, CbCaseApproveLevel> approveLevelById) {
            if (CollectionUtils.isEmpty(caseApproveLogs)) {
                return new ArrayList<>(approveLevelById.values());
            }

            return caseApproveLogs.stream().map(log -> {
                //获取当前资产配置的最新审批层级
                CbCaseApproveLevel latestApproveLevel = approveLevelById.get(log.getLevelId());
                if (Objects.nonNull(latestApproveLevel)) {
                    return latestApproveLevel;
                }
                //审批层级被删除，生成审批层级对象
                return new CbCaseApproveLevel()
                        .setId(log.getLevelId())
                        .setPropertyId(log.getPropertyId())
                        .setStage(log.getStage())
                        .setName(log.getName())
                        .setApproveIndex(log.getApproveIndex());
            }).distinct().sorted(Comparator.comparing(CbCaseApproveLevel::getApproveIndex)).collect(toList());
        }

        /**
         * 转换审批人
         *
         * @param approveLogs  审批日志
         * @param userByUserId 用户信息
         * @param timezone     时区
         * @return 审批日志 DTO
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveUserDTO> convertApproveUser(List<CbCaseApproveLog> approveLogs,
                                                                 Map<Integer, UserDTO> userByUserId, String timezone) {
            //日期格式化
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_USA, Locale.ENGLISH);
            return approveLogs.stream().map(approveLog -> {
                CaseStageApproveUserDTO approveUser = new CaseStageApproveUserDTO();
                approveUser.setUserId(approveLog.getApproveBy());
                approveUser.setApproveStatus(approveLog.getIsApproved() ? ApproveStatusEnum.APPROVED.getStatus()
                        : ApproveStatusEnum.REJECTED.getStatus());
                approveUser.setApproveOpinion(approveLog.getApproveOpinion());
                approveUser.setApproveTime(approveLog.getApproveTime());

                //设置审核时间
                if (approveLog.getApproveTime() > 0) {
                    ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(approveLog.getApproveTime(), timezone);
                    approveUser.setApproveTimeStr(dateTimeFormatter.format(zonedDateTime));
                }

                UserDTO userDTO = userByUserId.get(approveLog.getApproveBy());
                if (Objects.nonNull(userDTO)) {
                    approveUser.setName(userDTO.getName());
                    approveUser.setAccount(userDTO.getAccount());
                    approveUser.setEmail(userDTO.getEmail());
                    approveUser.setPhone(userDTO.getPhone());
                }
                return approveUser;
            }).collect(toList());
        }

        /**
         * 转换审批人员
         *
         * @param approveUsers  审批人
         * @param userByUserId  用户信息
         * @param approveStatus 审批状态
         * @return 审批人员 DTO
         * @author yyt
         * @date 2023/9/9
         */
        private List<CaseStageApproveUserDTO> convertApproveUser(List<CbCaseApproveLevelUser> approveUsers,
                                                                 Map<Integer, UserDTO> userByUserId, int approveStatus) {
            return approveUsers.stream().map(approveConfig -> {
                CaseStageApproveUserDTO approveUser = new CaseStageApproveUserDTO();
                approveUser.setUserId(approveConfig.getUserId());
                approveUser.setApproveStatus(approveStatus);
                UserDTO userDTO = userByUserId.get(approveConfig.getUserId());
                if (Objects.nonNull(userDTO)) {
                    approveUser.setName(userDTO.getName());
                    approveUser.setAccount(userDTO.getAccount());
                    approveUser.setEmail(userDTO.getEmail());
                    approveUser.setPhone(userDTO.getPhone());
                }
                return approveUser;
            }).collect(toList());
        }

        /**
         * 查询指定时间段内的 case
         *
         * @param propertyId 资产 id
         * @param startDate  开始时间
         * @param endDate    结束时间
         * @return case
         * @author yyt
         * @date 2025/12/8
         **/
        @Override
        public List<CbCase> listByIssueDate(int propertyId, long startDate, long endDate) {
            return list(new QueryWrapper<CbCase>().lambda()
                    .eq(CbCase::getPropertyId, propertyId)
                    .between(CbCase::getIssueDate, startDate, endDate));
        }

        /**
         * 获取上一个月的 case
         *
         * @param propertyIds 资产 id
         * @return case
         * @author yyt
         * @date 2025/12/8
         **/
        @Override
        public List<CbCase> listLastMonthCaseByIssueDate(List<Integer> propertyIds) {
            List<PropertyDTO> propertyDTOS = propertyI18nRedisService.list(propertyIds);
            // 按时区分组
            Map<String, List<Integer>> groupByTimezone = groupPropertyIdsByTimezone(propertyDTOS);

            List<CbCase> result = new ArrayList<>();
            for (Map.Entry<String, List<Integer>> entry : groupByTimezone.entrySet()) {
                // 计算该组统一的时间戳
                String timezone = entry.getKey();
                ZonedDateTime startDate = DateTimeUtils.dateNow(timezone).withDayOfMonth(1).minusMonths(1);
                ZonedDateTime endDate = startDate.plusMonths(1).minusDays(1);

                LambdaQueryWrapper<CbCase> wrapper = new QueryWrapper<CbCase>().lambda()
                        .in(CbCase::getPropertyId, entry.getValue())
                        .between(CbCase::getIssueDate, startDate.toEpochSecond(), endDate.toEpochSecond());
                result.addAll(list(wrapper));
            }

            return result;
        }

        /**
         * 获取指定时间段内的 case
         *
         * @param propertyDTOS   资产
         * @param startLocalDate 开始时间
         * @param endLocalDate   结束时间
         * @return case
         * @author yyt
         * @date 2025/12/8
         **/
        @Override
        public List<CbCase> listByIssueDate(List<PropertyDTO> propertyDTOS, LocalDate startLocalDate, LocalDate endLocalDate) {
            // 按时区分组
            Map<String, List<Integer>> groupByTimezone = groupPropertyIdsByTimezone(propertyDTOS);

            List<CbCase> result = new ArrayList<>();
            for (Map.Entry<String, List<Integer>> entry : groupByTimezone.entrySet()) {
                // 计算该组统一的时间戳
                String timezone = entry.getKey();
                long startDate = DateTimeUtils.localDateToLong(startLocalDate, timezone);
                long endDate = DateTimeUtils.localDateToLong(endLocalDate, timezone);

                LambdaQueryWrapper<CbCase> wrapper = new QueryWrapper<CbCase>().lambda()
                        .in(CbCase::getPropertyId, entry.getValue())
                        .between(CbCase::getIssueDate, startDate, endDate);
                result.addAll(list(wrapper));
            }

            return result;
        }

        /**
         * 获取超期未处理的 case
         *
         * @param propertyIds 资产 id
         * @return case
         * @author yyt
         * @date 2025/12/8
         **/
        @Override
        public List<CbCase> listOverdueUnprocess(List<Integer> propertyIds) {
            List<PropertyDTO> propertyDTOS = propertyI18nRedisService.list(propertyIds);
            // 按时区分组
            Map<String, List<Integer>> groupByTimezone = groupPropertyIdsByTimezone(propertyDTOS);

            List<CbCase> result = new ArrayList<>();
            for (Map.Entry<String, List<Integer>> entry : groupByTimezone.entrySet()) {
                // 提取该组所有的 propertyId
                List<Integer> propertyIdsForCurrentTimeZone = entry.getValue();
                // 计算该组统一的时间戳
                String timezone = entry.getKey();
                long currentDate = DateTimeUtils.dateNowTimestamp(timezone);
                result.addAll(listNotApprovedOverTimeCase(propertyIdsForCurrentTimeZone, StageEnum.HOTEL_OPERATION.getCode(), currentDate));
                result.addAll(listNotApprovedOverTimeCase(propertyIdsForCurrentTimeZone, StageEnum.PATCH.getCode(), currentDate));
            }

            return result;
        }

        /**
         * 资产按时区分组
         *
         * @param propertyDTOS 资产
         * @return 不同时区下的资产
         * @author yyt
         * @date 2025/12/10
         **/
        private Map<String, List<Integer>> groupPropertyIdsByTimezone(List<PropertyDTO> propertyDTOS) {
            return propertyDTOS.stream().collect(Collectors.groupingBy(PropertyDTO::getTimezone,
                    Collectors.collectingAndThen(Collectors.toList(), list -> list.stream().map(PropertyDTO::getId).toList())));
        }

    }