# CbJournalServiceImpl.java

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

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.SystemPlatformEnum;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.finance.entity.chargeback.CbCase;
    import cloud.wconnect.finance.entity.chargeback.CbJournal;
    import cloud.wconnect.finance.mapper.chargeback.CbJournalMapper;
    import cloud.wconnect.finance.pojo.bo.chargeback.JournalListBO;
    import cloud.wconnect.finance.service.chargeback.ICbCaseService;
    import cloud.wconnect.finance.service.chargeback.ICbJournalService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.time.format.DateTimeFormatter;
    import java.util.*;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;

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

    /**
     * <p>
     * 服务实现类
     * </p>
     *
     * @author yyt
     * @since 2023-08-22
     */
    @Service
    @DS("finance")
    public class CbJournalServiceImpl extends ServiceImpl<CbJournalMapper, CbJournal> implements ICbJournalService {

        @Autowired
        private ObjectMapper objectMapper;
        @Autowired
        private ICbCaseService cbCaseService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;

        /**
         * 处理 journal 数据
         *
         * @param property 资产标识
         * @param userId   操作人
         * @param date     日期
         * @param journals journal 数据
         * @author yyt
         * @date 2023/9/8
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleJournalData(PropertyDTO property, Integer userId, String date, List<JournalListBO> journals) {
            if (CollectionUtils.isEmpty(journals)) {
                return;
            }

            Integer propertyId = property.getId();
            ZoneId zoneId = ZoneId.of(property.getTimezone());
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(BaseConstant.DATE_FORMAT_YYYY_MM_DD);
            long now = DateTimeUtils.nowTimestamp();
            InternationalizationNumberDTO number = propertyI18nRedisService.get(propertyId).getInternationalization().getNumber();
            int dataPrecision = BaseConstant.DATA_PRECISION;
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(number.getCalculationRule());

            List<CbJournal> newJournals = journals.stream().map(item -> new CbJournal().setPropertyId(propertyId)
                    .setGuestName(item.getGuestName()).setCardNumber(item.getCardNumber())
                    .setTransactionDate(LocalDate.parse(item.getTransactionDate(), dateTimeFormatter)
                            .atStartOfDay(zoneId).toEpochSecond())
                    .setRoomNumber(item.getRoomNumber()).setDebit(item.getDebit().setScale(dataPrecision, roundingMode))
                    .setDescription(item.getDescription())
                    .setCreatedBy(userId).setCreatedTime(now)
                    .setConfNumber(item.getConfNumber())).collect(Collectors.toList());
            saveBatch(newJournals);

            matchCaseFromJournals(property, LocalDate.parse(date, dateTimeFormatter).atStartOfDay(zoneId).toEpochSecond(), newJournals);
        }

        /**
         * 根据 journal 数据匹配 case，并更新 case 基础信息
         *
         * @param property 资产
         * @param trxDate  交易日期
         * @param journals 导入的Journal数据集合
         * @author yyt
         * @date 2023/9/8
         */
        private void matchCaseFromJournals(PropertyDTO property, Long trxDate, List<CbJournal> journals) {
            if (journals.isEmpty()) {
                return;
            }
            // 导入Journal数据后，将guest name不符合要求的数据过滤掉
            List<CbJournal> journalsWithGuestNames = journals.stream()
                    .filter(journal -> StringUtils.isNotBlank(journal.getGuestName()))
                    .filter(journal -> StringUtils.contains(journal.getGuestName(), ","))
                    .collect(toList());
            if (journalsWithGuestNames.isEmpty()) {
                return;
            }

            Integer propertyId = property.getId();
            // 获取相同酒店和日期下，没有匹配到客人姓名和入住房间号的Case数据，不包括匹配到多个的Case数据
            List<CbCase> casesNoGuestName = cbCaseService.listNoGuestName(propertyId, trxDate);
            if (casesNoGuestName.isEmpty()) {
                return;
            }

            // 按照卡号末4位 + 交易金额，将匹配到的 Journals 数据中的 Guest Name 和 Room Number，设置到对应的 Case 数据中
            setCasesGuestNames(casesNoGuestName, journalsWithGuestNames);

            // 筛选出已经匹配 Guest Name 和仍未匹配 Guest Name 的 Case 集合
            Map<Boolean, List<CbCase>> casesNoGuestNameMap = casesNoGuestName.stream()
                    .collect(partitioningBy(cbCase ->
                            StringUtils.isBlank(cbCase.getGuestFirstName())
                                    && StringUtils.isBlank(cbCase.getGuestLastName())
                                    && StringUtils.isBlank(cbCase.getMoreGuests())
                    ));
            List<CbCase> casesWithGuestName = casesNoGuestNameMap.get(false);
            List<CbCase> casesStillNoGuestName = casesNoGuestNameMap.get(true);

            if (!casesStillNoGuestName.isEmpty()) {
                // 取指定交易日期前一日的 Journal 数据，与仍未匹配到 Guest Name 的 Case 集合再进行一次匹配
                String timezone = property.getTimezone();
                long trxDateBefore = DateTimeUtils.toZonedDateTime(trxDate, timezone).minusDays(1L).toEpochSecond();
                List<CbJournal> journalsTrxDateBefore = baseMapper.selectForCaseGuestName(propertyId, trxDateBefore,
                                casesStillNoGuestName).stream().filter(journal -> StringUtils.isNotBlank(journal.getGuestName()))
                        .filter(journal -> StringUtils.contains(journal.getGuestName(), ","))
                        .collect(toList());

                // 按照卡号末 4 位 + 交易金额，将匹配到的 Journals 数据中的 Guest Name 和 Room Number，设置到对应的 Case 数据中
                setCasesGuestNames(casesStillNoGuestName, journalsTrxDateBefore);
            }

            // 将最终匹配到Guest Name的Case集合进行合并
            List<CbCase> allCasesWithGuestName = Stream.concat(casesWithGuestName.stream(),
                            casesStillNoGuestName.stream()
                                    .filter(cbCase ->
                                            StringUtils.isNotBlank(cbCase.getGuestFirstName()) ||
                                                    StringUtils.isNotBlank(cbCase.getGuestLastName()) ||
                                                    StringUtils.isNotBlank(cbCase.getMoreGuests())))
                    .toList();
            if (!allCasesWithGuestName.isEmpty()) {
                cbCaseService.updateBatchById(allCasesWithGuestName.stream().map(item -> new CbCase().setId(item.getId())
                        .setGuestFirstName(item.getGuestFirstName()).setGuestLastName(item.getGuestLastName())
                        .setRoomNumber(item.getRoomNumber()).setMoreGuests(item.getMoreGuests())
                        .setFolioNumber(item.getFolioNumber())
                        .setPmsTrxDate(item.getPmsTrxDate())).collect(toList()));
            }

            if (property.getPmsType().equals(SystemPlatformEnum.SHI_JI.getDesc())) {
                handleSpecialMatchingProcessForImportJournalReport(property, trxDate, journalsWithGuestNames);
            }
        }

        /**
         * 按照卡号末4位 + 交易金额，将匹配到的 Journals 数据中的 Guest Name 和 Room Number，设置到对应的 Case 数据中
         * 如果匹配到多个 Journal 数据，则生成备选项，交由财务人员手选确认
         *
         * @param casesNoGuestName case
         * @param journals         journal
         * @author yyt
         * @date 2023/9/8
         */
        private void setCasesGuestNames(List<CbCase> casesNoGuestName, List<CbJournal> journals) {
            if (casesNoGuestName.isEmpty()) {
                return;
            }

            if (journals.isEmpty()) {
                return;
            }

            // 将 Journal 数据按照卡号末 4 位和交易金额分组
            Map<String, Map<String, List<CbJournal>>> journalsMap = journals.stream()
                    .collect(groupingBy(CbJournal::getCardNumber, groupingBy(journal -> journal.getDebit()
                            .stripTrailingZeros().toPlainString())));
            casesNoGuestName.forEach((cbCase -> {
                Map<String, List<CbJournal>> journalsByDebit = journalsMap.get(cbCase.getCardNumberEnd());
                if (journalsByDebit == null) {
                    return;
                }

                List<CbJournal> journalsMatched = journalsByDebit.getOrDefault(cbCase.getTrxAmount()
                                .stripTrailingZeros().toPlainString(),
                        Collections.emptyList());
                setGuestNameAndRoom(cbCase, journalsMatched);
            }));
        }

        /**
         * 根据匹配到的 Journal 数据，设置 Case 中的客人姓名和入住房间号
         *
         * @param cbCase   case
         * @param journals 匹配的 Journal 数据集合
         * @return 设置结果，匹配一个或多个 Journal 数据，返回 true，没有匹配到 Journal 数据返回 false
         * @author yyt
         * @date 2023/9/8
         */
        private boolean setGuestNameAndRoom(CbCase cbCase, List<CbJournal> journals) {
            if (journals.isEmpty()) {
                // 没有匹配到任何Journal数据
                cbCase.setGuestFirstName("");
                cbCase.setGuestLastName("");
                cbCase.setRoomNumber("");
                cbCase.setMoreGuests("");
                cbCase.setFolioNumber("");
                return false;
            }

            boolean matchedMore = journals.size() > 1;
            List<CbJournal> distinctJournals = journals;
            if (matchedMore) {
                // 将匹配到的Journals集合按照 客人姓名+房间号 进行去重
                distinctJournals = distinctJournals.stream().collect(collectingAndThen(toCollection(() ->
                                new TreeSet<>(Comparator.comparing(journal -> journal.getGuestName() + ":" + journal.getRoomNumber()))),
                        ArrayList::new));
                if (distinctJournals.size() > 1) {
                    // 如果交易日期内匹配的 Journal 数据是多条，则存下来由财务人员手选确认
                    cbCase.setGuestFirstName("");
                    cbCase.setGuestLastName("");
                    cbCase.setRoomNumber("");
                    cbCase.setFolioNumber("");
                    List<Map<String, String>> moreGuests = distinctJournals.stream()
                            .map(journal -> {
                                HashMap<String, String> map = new HashMap<>();
                                map.put("name", journal.getGuestName());
                                map.put("room", journal.getRoomNumber());
                                map.put("confNumber", journal.getConfNumber());
                                return map;
                            }).collect(toList());
                    cbCase.setMoreGuests(JsonUtils.convertObjListToString(objectMapper, moreGuests));
                    cbCase.setPmsTrxDate(distinctJournals.get(0).getTransactionDate());

                    return true;
                }
            }

            CbJournal cbJournal = distinctJournals.get(0);
            String guestName = cbJournal.getGuestName();
            cbCase.setGuestFirstName(StringUtils.trim(StringUtils.substringAfter(guestName, ",")));
            cbCase.setGuestLastName(StringUtils.trim(StringUtils.substringBefore(guestName, ",")));
            cbCase.setRoomNumber(cbJournal.getRoomNumber());
            cbCase.setMoreGuests("");
            cbCase.setPmsTrxDate(cbJournal.getTransactionDate());
            cbCase.setFolioNumber(cbJournal.getConfNumber());
            return true;
        }

        /**
         * 处理需要特殊处理匹配规则的资产（导入 journal 报告）
         *
         * @param property 资产
         * @param trxDate  交易日期
         * @param journals journal 数据
         * @author yyt
         * @date 2025/10/15
         **/
        private void handleSpecialMatchingProcessForImportJournalReport(PropertyDTO property, Long trxDate, List<CbJournal> journals) {
            // 取交易日期是指定日期 -1 的未匹配 case
            Integer propertyId = property.getId();
            String timezone = property.getTimezone();
            long trxDateBefore = DateTimeUtils.toZonedDateTime(trxDate, timezone).minusDays(1L).toEpochSecond();
            List<CbCase> casesNoGuestName = cbCaseService.listNoGuestName(propertyId, trxDateBefore);
            if (casesNoGuestName.isEmpty()) {
                return;
            }

            // 按照卡号末4位 + 交易金额，将匹配到的 Journals 数据中的 Guest Name 和 Room Number，设置到对应的 Case 数据中
            setCasesGuestNames(casesNoGuestName, journals);

            List<CbCase> withGuestName = casesNoGuestName.stream()
                    .filter(cbCase ->
                            StringUtils.isNotBlank(cbCase.getGuestFirstName()) ||
                                    StringUtils.isNotBlank(cbCase.getGuestLastName()) ||
                                    StringUtils.isNotBlank(cbCase.getMoreGuests()))
                    .toList();
            if (!withGuestName.isEmpty()) {
                cbCaseService.updateBatchById(withGuestName.stream().map(item -> new CbCase().setId(item.getId())
                        .setGuestFirstName(item.getGuestFirstName()).setGuestLastName(item.getGuestLastName())
                        .setRoomNumber(item.getRoomNumber()).setMoreGuests(item.getMoreGuests())
                        .setFolioNumber(item.getFolioNumber())
                        .setPmsTrxDate(item.getPmsTrxDate())).collect(toList()));
            }
        }

        /**
         * 根据多个 Case 数据中资产 + 卡号末 4 位 + 交易金额 + 交易日期，匹配 Journal 数据，设置客人姓名 和 入住房间号
         * 匹配多个 Journal 时，生成备选项，交由财务人员手选确认
         *
         * @param propertyDTO 资产
         * @param cases       case
         * @author yyt
         * @date 2023/9/9
         */
        @Override
        public void matchCasesWithJournals(PropertyDTO propertyDTO, List<CbCase> cases) {
            if (CollectionUtils.isEmpty(cases)) {
                return;
            }

            // 计算交易日期的前一日
            Integer propertyId = propertyDTO.getId();
            String timezone = propertyDTO.getTimezone();
            List<CbCase> queryForJournals = Lists.newArrayListWithExpectedSize(cases.size() * 2);
            for (CbCase aCase : cases) {
                queryForJournals.add(aCase);

                CbCase newCase = new CbCase();
                newCase.setPropertyId(propertyId);
                long trxDate = aCase.getTrxDate();
                long trxDateBefore = DateTimeUtils.toZonedDateTime(trxDate, timezone).minusDays(1L).toEpochSecond();
                newCase.setCardNumberEnd(aCase.getCardNumberEnd());
                newCase.setTrxDate(trxDateBefore);
                newCase.setTrxAmount(aCase.getTrxAmount());
                queryForJournals.add(newCase);
            }

            // 将Journal数据按照 卡号末4位+交易金额+交易日期进行分组
            Map<String, Map<String, Map<Long, List<CbJournal>>>> journalsMap = baseMapper.selectForCase(queryForJournals)
                    .stream().filter(journal -> StringUtils.isNotBlank(journal.getGuestName()))
                    .filter(journal -> StringUtils.contains(journal.getGuestName(), ","))
                    .collect(groupingBy(CbJournal::getCardNumber, groupingBy(journal -> journal.getDebit()
                                    .stripTrailingZeros().toPlainString(),
                            groupingBy(CbJournal::getTransactionDate))));

            for (CbCase aCase : cases) {
                long trxDate = aCase.getTrxDate();
                String cardNumberEnd = aCase.getCardNumberEnd();
                String trxAmountStr = aCase.getTrxAmount().stripTrailingZeros().toPlainString();

                Map<String, Map<Long, List<CbJournal>>> journalsByDebitTrxDate = journalsMap.getOrDefault(cardNumberEnd,
                        Collections.emptyMap());
                if (journalsByDebitTrxDate.isEmpty()) {
                    setGuestNameAndRoom(aCase, Collections.emptyList());
                    continue;
                }

                Map<Long, List<CbJournal>> journalsByTrxDate = journalsByDebitTrxDate.getOrDefault(trxAmountStr,
                        Collections.emptyMap());
                if (journalsByTrxDate.isEmpty()) {
                    setGuestNameAndRoom(aCase, Collections.emptyList());
                    continue;
                }

                // 优先判断交易日期内是否有匹配的Journal数据
                List<CbJournal> journals = journalsByTrxDate.getOrDefault(trxDate, Collections.emptyList());
                boolean result = setGuestNameAndRoom(aCase, journals);

                if (!result) {
                    // 如果交易日日期没有匹配的Journal数据，则看前一日是否有匹配数据
                    long trxDateBefore = DateTimeUtils.toZonedDateTime(trxDate, timezone).minusDays(1L).toEpochSecond();
                    List<CbJournal> journalsBefore = journalsByTrxDate.getOrDefault(trxDateBefore, Collections.emptyList());
                    setGuestNameAndRoom(aCase, journalsBefore);
                }
            }

            if (propertyDTO.getPmsType().equals(SystemPlatformEnum.SHI_JI.getDesc())) {
                handleSpecialMatchingProcessForCollectCase(propertyDTO, cases.stream()
                        .filter(cbCase -> StringUtils.isBlank(cbCase.getGuestFirstName())
                                && StringUtils.isBlank(cbCase.getGuestLastName())
                                && StringUtils.isBlank(cbCase.getRoomNumber())
                                && StringUtils.isBlank(cbCase.getMoreGuests())).toList());
            }
        }

        /**
         * 处理需要特殊处理匹配规则的资产（保存采集 case）
         *
         * @param propertyDTO    资产
         * @param needHandleCase 需要处理的 Case
         * @author yyt
         * @date 2025/10/15
         **/
        private void handleSpecialMatchingProcessForCollectCase(PropertyDTO propertyDTO, List<CbCase> needHandleCase) {
            if (CollectionUtils.isEmpty(needHandleCase)) return;

            // 取 case 交易日期 + 1 的 journal 数据，再次匹配
            String timezone = propertyDTO.getTimezone();
            List<CbCase> queryForJournals = needHandleCase.stream().map(item ->
                    new CbCase().setPropertyId(item.getPropertyId())
                            .setCardNumberEnd(item.getCardNumberEnd())
                            .setTrxDate(DateTimeUtils.toZonedDateTime(item.getTrxDate(), timezone).plusDays(1L).toEpochSecond())
                            .setTrxAmount(item.getTrxAmount())
            ).toList();

            // 将Journal数据按照 卡号末4位+交易金额进行分组
            Map<String, Map<String, List<CbJournal>>> journalsByCardNumberEnd = baseMapper.selectForCase(queryForJournals)
                    .stream().filter(journal -> StringUtils.isNotBlank(journal.getGuestName()))
                    .filter(journal -> StringUtils.contains(journal.getGuestName(), ","))
                    .collect(groupingBy(CbJournal::getCardNumber, groupingBy(journal -> journal.getDebit()
                            .stripTrailingZeros().toPlainString())));

            for (CbCase item : needHandleCase) {
                String cardNumberEnd = item.getCardNumberEnd();
                Map<String, List<CbJournal>> journalsByTrxAmount = journalsByCardNumberEnd.getOrDefault(cardNumberEnd,
                        Collections.emptyMap());
                if (journalsByTrxAmount.isEmpty()) {
                    continue;
                }

                String trxAmountStr = item.getTrxAmount().stripTrailingZeros().toPlainString();
                List<CbJournal> journals = journalsByTrxAmount.getOrDefault(trxAmountStr, Collections.emptyList());
                if (journals.isEmpty()) {
                    continue;
                }

                setGuestNameAndRoom(item, journals);
            }
        }

    }