# GstLedgOpBalanceReservationServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.QuarterUtil;
    import cloud.wconnect.frontoffice.cons.guestledgercheck.GuestLedgerCheckErrorI18nConstant;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgOpBalRvNote;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgOpBalanceReservation;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgPaidAmount;
    import cloud.wconnect.frontoffice.entity.guestledgercheck.GstLedgTotalAmount;
    import cloud.wconnect.frontoffice.mapper.guestledgercheck.GstLedgOpBalanceReservationMapper;
    import cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.AmountPaidDTO;
    import cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.OpBalanceRvDTO;
    import cloud.wconnect.frontoffice.pojo.query.guestledgercheck.OpBalanceRvQuery;
    import cloud.wconnect.frontoffice.pojo.query.guestledgercheck.PropertyLossAmountTrendQuery;
    import cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.LossAmountTrendVO;
    import cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.OpBalanceRvSummaryVO;
    import cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.PropertyLossAmountTrendVO;
    import cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.PropertyLossAmountVO;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgOpBalRvNoteService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgOpBalanceReservationService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgPaidAmountService;
    import cloud.wconnect.frontoffice.service.guestledgercheck.IGstLedgTotalAmountService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.google.common.collect.Lists;
    import com.google.common.collect.Maps;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.collections4.MapUtils;
    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.BigDecimal;
    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.*;
    import static java.util.stream.Collectors.toList;

    /**
     * <p>
     * Guest Ledger Check: Reservations with open balance 服务实现类
     * </p>
     *
     * @author lichen
     * @since 2023-03-17
     */
    @Service
    @DS("logbook")
    public class GstLedgOpBalanceReservationServiceImpl extends ServiceImpl<GstLedgOpBalanceReservationMapper, GstLedgOpBalanceReservation> implements IGstLedgOpBalanceReservationService {
        @Autowired
        private IGstLedgOpBalRvNoteService opBalRvNoteService;
        @Autowired
        private IGstLedgTotalAmountService totalAmountService;
        @Autowired
        private IGstLedgPaidAmountService paidAmountService;
        @Autowired
        private GstLedgOpBalanceReservationMapper opBalanceReservationMapper;
        @Autowired
        private BeanMapper beanMapper;
        @Autowired
        private UserInternalService userInternalService;

        /**
         * 保存新增的 Reservation 信息
         *
         * @param propertyDTO 资产DTO
         * @param opBalanceRv Reservation信息
         * @param note        备注
         * @param loginUserId 登录用户id
         * @return void
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(PropertyDTO propertyDTO, GstLedgOpBalanceReservation opBalanceRv, String note, int loginUserId) {
            //累计Total Amount
            BigDecimal writeOffAmount = opBalanceRv.getWriteOffAmount();
            totalAmountService.save(propertyDTO.getId(), writeOffAmount);
            //保存GstLedgOpBalanceReservation opBalanceRv
            long timeNow = DateTimeUtils.dateTimeNowOfSecond(propertyDTO.getTimezone());
            opBalanceRv.setOriBalAmount(opBalanceRv.getBalanceAmount());
            opBalanceRv.setCreatedBy(loginUserId);
            opBalanceRv.setCreatedTime(timeNow);
            opBalanceRv.setBalAmntUpdatedBy(loginUserId);
            opBalanceRv.setBalAmntUpdatedTime(timeNow);
            opBalanceRv.setWrOffAmntUpdatedBy(loginUserId);
            opBalanceRv.setWrOffAmntUpdatedTime(timeNow);
            //保存
            save(opBalanceRv);

            //保存Note
            if (StringUtils.isNotBlank(note)) {
                GstLedgOpBalRvNote gstLedgOpBalRvNote = new GstLedgOpBalRvNote();
                gstLedgOpBalRvNote.setPropertyId(opBalanceRv.getPropertyId());
                gstLedgOpBalRvNote.setOpBalRvId(opBalanceRv.getId());
                gstLedgOpBalRvNote.setNote(note);
                gstLedgOpBalRvNote.setCreatedBy(loginUserId);
                gstLedgOpBalRvNote.setCreatedTime(timeNow);
                opBalRvNoteService.save(gstLedgOpBalRvNote);
            }
        }

        @Override
        @Transactional(rollbackFor = Exception.class)
        public void remove(GstLedgOpBalanceReservation opBalanceRv) {
            //Write off 统计值中扣减 Reservation 的 Write off 值
            //negate 取反。例如：-1030 -> 1030 , 1030 -> -1030
            totalAmountService.save(opBalanceRv.getPropertyId(), opBalanceRv.getWriteOffAmount().negate());

            //删除Reservation的信息
            removeById(opBalanceRv.getId());

            //删除Reservation的Note信息
            LambdaQueryWrapper<GstLedgOpBalRvNote> noteDelQueryWrapper = new LambdaQueryWrapper<>();
            noteDelQueryWrapper.eq(GstLedgOpBalRvNote::getOpBalRvId, opBalanceRv.getId());
            opBalRvNoteService.remove(noteDelQueryWrapper);

            //删除Paid Amount记录
            LambdaQueryWrapper<GstLedgPaidAmount> paidAmountDelQueryWrapper = new LambdaQueryWrapper<>();
            paidAmountDelQueryWrapper.eq(GstLedgPaidAmount::getOpBalRvId, opBalanceRv.getId());
            paidAmountService.remove(paidAmountDelQueryWrapper);
        }

        /**
         * 更新BalanceAmount
         *
         * @param rvId          Reservation信息的id
         * @param balanceAmount BalanceAmount 的值
         * @param loginUserId   登录用户id
         * @return void
         * @author lichen
         * @date 2025/8/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateBalanceAmount(long rvId, BigDecimal balanceAmount, int loginUserId) {
            GstLedgOpBalanceReservation forUpdateById = opBalanceReservationMapper.getForUpdateById(rvId);
            //Balance Amount 的修改不能小于原值
            if (balanceAmount.compareTo(forUpdateById.getBalanceAmount()) < 0) {
                throw new BadRequestException(GuestLedgerCheckErrorI18nConstant.BALANCE_AMOUNT_CANNOT_BE_LOWER_THAN_CURRENT_BALANCE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The new balance amount should be greater than or equal to the old amount. newAmount: " +
                                balanceAmount + ", oldAmount: " + forUpdateById.getBalanceAmount());
            }
            //更新Balance Amount
            GstLedgOpBalanceReservation opBalanceReservation = new GstLedgOpBalanceReservation();
            opBalanceReservation.setId(rvId);
            opBalanceReservation.setBalanceAmount(balanceAmount);
            opBalanceReservation.setBalAmntUpdatedBy(loginUserId);
            opBalanceReservation.setBalAmntUpdatedTime(DateTimeUtils.nowTimestamp());
            updateById(opBalanceReservation);
        }

        /**
         * 更新writeOffAmount
         *
         * @param rvId              Reservation信息的id
         * @param newWriteOffAmount WriteOffAmount 的值
         * @param propertyId        资产id
         * @param loginUserId       登录用户id
         * @return void
         * @author lichen
         * @date 2025/8/25
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateWriteOffAmount(long rvId, BigDecimal newWriteOffAmount, int propertyId, int loginUserId) {
            GstLedgOpBalanceReservation forUpdateById = opBalanceReservationMapper.getForUpdateById(rvId);
            //writeOffAmount有变化才更新数据库
            if (newWriteOffAmount.compareTo(forUpdateById.getWriteOffAmount()) != 0) {
                GstLedgOpBalanceReservation opBalanceReservation = new GstLedgOpBalanceReservation();
                //writeOffAmount
                opBalanceReservation.setId(rvId);
                opBalanceReservation.setWriteOffAmount(newWriteOffAmount);
                opBalanceReservation.setWrOffAmntUpdatedBy(loginUserId);
                opBalanceReservation.setWrOffAmntUpdatedTime(DateTimeUtils.nowTimestamp());
                //Balance Amount 的值 = 原 Balance Amount + 原 Write off Amount - 新 Write off Amount
                BigDecimal oriWriteOffAmount = forUpdateById.getWriteOffAmount();
                BigDecimal newBalanceAmount = forUpdateById.getBalanceAmount().add(oriWriteOffAmount).subtract(newWriteOffAmount);
                opBalanceReservation.setBalanceAmount(newBalanceAmount);
                opBalanceReservation.setBalAmntUpdatedBy(loginUserId);
                opBalanceReservation.setBalAmntUpdatedTime(DateTimeUtils.nowTimestamp());
                //更新
                updateById(opBalanceReservation);
                //累计 Total Write off Amount
                totalAmountService.save(propertyId, newWriteOffAmount.subtract(oriWriteOffAmount));
            }
        }

        /**
         * 根据创建时间和是否解决，查询OpenBalance数据
         *
         * @param propertyId 资产ID
         * @param isResolve  是否解决 true:是 false:否
         * @return java.util.List<cloud.wconnect.frontoffice.entity.guestledger.GstLedgOpBalanceReservation>
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public List<GstLedgOpBalanceReservation> listByResolved(Integer propertyId, boolean isResolve) {
            return list(new LambdaQueryWrapper<GstLedgOpBalanceReservation>()
                    .eq(GstLedgOpBalanceReservation::getPropertyId, propertyId)
                    .eq(GstLedgOpBalanceReservation::getResolved, isResolve)
                    .orderByDesc(GstLedgOpBalanceReservation::getId));
        }

        /**
         * 查询总经理未知晓条数
         *
         * @param propertyId 资产ID
         * @return Long
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public Long getGMUnKnownCount(Integer propertyId) {
            return count(new LambdaQueryWrapper<GstLedgOpBalanceReservation>()
                    .eq(GstLedgOpBalanceReservation::getPropertyId, propertyId)
                    .eq(GstLedgOpBalanceReservation::getGmInformed, false));
        }

        /**
         * 查询资产的总损失金额
         *
         * @param startDate    开始时间，20231201
         * @param endDate      结束时间，20231231
         * @param propertyDTOS 资产集合
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.vo.guestledger.PropertyLossAmountVO>
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public List<PropertyLossAmountVO> listPropertyTotalLossAmount(Integer startDate, Integer endDate, List<PropertyDTO> propertyDTOS) {
            if (CollectionUtils.isEmpty(propertyDTOS)) return Collections.emptyList();
            Map<Integer, String> propertyId2Abbr = propertyDTOS.stream().collect(Collectors.toMap(PropertyDTO::getId, PropertyDTO::getAbbr));
            Map<Integer, String> propertyId2Timezone = propertyDTOS.stream().collect(Collectors.toMap(PropertyDTO::getId, PropertyDTO::getTimezone));

            List<PropertyLossAmountVO> propertyLossAmountVOS = Lists.newArrayListWithCapacity(propertyDTOS.size());

            for (PropertyDTO propertyDTO : propertyDTOS) {
                Integer propertyId = propertyDTO.getId();
                String timezone = propertyId2Timezone.get(propertyId);

                //资产时区下的开始时间戳
                long startTimestampByPropertyId = DateTimeUtils.dateStrToEpochSecond(startDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD, timezone);
                //资产时区下的结束时间戳。往后推一天的原因：为了查询出结束时间当天的数据
                LocalDate endDateLocalDateByPropertyId = DateTimeUtils.strToLocalDate(endDate.toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
                long nextDateTimestampByPropertyId = DateTimeUtils.localDateToLong(endDateLocalDateByPropertyId.plusDays(1), timezone);

                //查询总损失金额
                BigDecimal totalLossAmount = getSumLossAmountByCreated(propertyId, startTimestampByPropertyId, nextDateTimestampByPropertyId);

                propertyLossAmountVOS.add(new PropertyLossAmountVO(propertyId, propertyId2Abbr.get(propertyId), totalLossAmount));

            }
            return propertyLossAmountVOS;
        }

        /**
         * 查询多资产【总损失金额】的趋势
         *
         * @param query        查询对象
         * @param propertyDTOS 资产集合
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.vo.guestledger.PropertyLossAmountTrendVO>
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public List<PropertyLossAmountTrendVO> listPropertyTotalLossAmountTrend(PropertyLossAmountTrendQuery query, List<PropertyDTO> propertyDTOS) {
            List<PropertyLossAmountTrendVO> voList = Lists.newArrayListWithCapacity(propertyDTOS.size());

            for (PropertyDTO propertyDTO : propertyDTOS) {
                //根据统计单位计算统计日期
                Map<String, LocalDate> statisticDate = calculatestatisticDate(query.getUnit(), query.getSpan(), propertyDTO.getTimezone());
                LocalDate startLocalDate = statisticDate.get("startLocalDate");
                LocalDate endLocalDate = statisticDate.get("endLocalDate");

                //资产时区下的开始时间戳
                long startTimestampByPropertyId = DateTimeUtils.localDateToLong(startLocalDate, propertyDTO.getTimezone());
                //资产时区下的结束时间戳。往后推一天的原因：为了查询出结束时间当天的数据
                long endTimestampByPropertyId = DateTimeUtils.localDateToLong(endLocalDate.plusDays(1), propertyDTO.getTimezone());

                //查询资产的损失金额
                List<GstLedgOpBalanceReservation> opBalanceReservations = list(new LambdaQueryWrapper<GstLedgOpBalanceReservation>()
                        .select(GstLedgOpBalanceReservation::getPropertyId, GstLedgOpBalanceReservation::getWriteOffAmount, GstLedgOpBalanceReservation::getCreatedTime)
                        .eq(GstLedgOpBalanceReservation::getPropertyId, propertyDTO.getId())
                        .ge(GstLedgOpBalanceReservation::getCreatedTime, startTimestampByPropertyId)
                        .lt(GstLedgOpBalanceReservation::getCreatedTime, endTimestampByPropertyId));

                //根据统计单位和统计时间生成损失金额趋势集合
                List<LossAmountTrendVO> lossAmountTrendVOS = generateLossAmountTrendVO(opBalanceReservations, query.getUnit(), query.getSpan(), propertyDTO);
                //排序
                Collections.sort(lossAmountTrendVOS, Comparator.comparing(LossAmountTrendVO::getDate));

                voList.add(new PropertyLossAmountTrendVO(propertyDTO.getId(), propertyDTO.getAbbr(), lossAmountTrendVOS));
            }
            return voList;
        }

        /**
         * 查询资产在某时间段内的总损失金额
         *
         * @param propertyId     资产id
         * @param startTimestamp 开始时间戳
         * @param endTimestamp   结束时间戳
         * @return java.math.BigDecimal
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public BigDecimal getSumLossAmountByCreated(Integer propertyId, long startTimestamp, long endTimestamp) {
            QueryWrapper<GstLedgOpBalanceReservation> queryWrapper = new QueryWrapper<>();
            queryWrapper.select("sum(write_off_amount) as total_amount");
            queryWrapper.lambda()
                    .eq(GstLedgOpBalanceReservation::getPropertyId, propertyId)
                    .ge(GstLedgOpBalanceReservation::getCreatedTime, startTimestamp)
                    .lt(GstLedgOpBalanceReservation::getCreatedTime, endTimestamp);
            Map<String, Object> map = getMap(queryWrapper);
            return MapUtils.isEmpty(map) ? BigDecimal.ZERO : new BigDecimal(map.get("total_amount").toString());
        }

        /**
         * 获取资产下的Reservation列表
         *
         * @param propertyDTO 资产DTO
         * @param query       查询对象
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.OpBalanceRvDTO>
         * @author lichen
         * @date 2023/12/9
         */
        @Override
        public ListPagedDTO<OpBalanceRvDTO> pageList(PropertyDTO propertyDTO, OpBalanceRvQuery query) {
            Integer propertyId = propertyDTO.getId();

            Long fromDate = query.getFromDate();
            Long toDate = query.getToDate();

            //构建查询条件
            LambdaQueryWrapper<GstLedgOpBalanceReservation> queryWrapper = new LambdaQueryWrapper<GstLedgOpBalanceReservation>().eq(GstLedgOpBalanceReservation::getPropertyId, propertyId)
                    .like(StringUtils.isNotBlank(query.getCrsNumber()), GstLedgOpBalanceReservation::getCrsNumbers, query.getCrsNumber())
                    .eq(StringUtils.isNotBlank(query.getRoomNumber()), GstLedgOpBalanceReservation::getRoomNumber, query.getRoomNumber())
                    .eq(Objects.nonNull(query.getResolved()), GstLedgOpBalanceReservation::getResolved, query.getResolved())
                    .between(Objects.nonNull(fromDate) && Objects.nonNull(toDate), GstLedgOpBalanceReservation::getCheckInDate, fromDate, toDate)
                    .and(StringUtils.isNotBlank(query.getGuestName()),
                            wrapper -> wrapper.like(GstLedgOpBalanceReservation::getFirstName, query.getGuestName())
                                    .or().like(GstLedgOpBalanceReservation::getLastName, query.getGuestName()))
                    .orderByDesc(GstLedgOpBalanceReservation::getId);
            Page<GstLedgOpBalanceReservation> resultPage = page(new Page<>(query.getPage(), query.getSize()), queryWrapper);

            if (resultPage.getTotal() == 0) {
                return new ListPagedDTO<>();
            }

            return new ListPagedDTO<>(resultPage.getTotal(), convertToRvDTOList(propertyDTO.getTenantId(), resultPage.getRecords()));
        }

        /**
         * 获取资产下的Reservation列表的统计信息
         *
         * @param propertyId 资产id
         * @param resolved   是否解决
         * @return cloud.wconnect.frontoffice.pojo.vo.guestledgercheck.OpBalanceRvSummaryVO
         * @author lichen
         * @date 2025/8/25
         */
        @Override
        public OpBalanceRvSummaryVO summary(int propertyId, Boolean resolved) {
            //获取totalWriteOffAmount
            LambdaQueryWrapper<GstLedgTotalAmount> lambdaQueryWrapper = new LambdaQueryWrapper<>();
            lambdaQueryWrapper.eq(GstLedgTotalAmount::getPropertyId, propertyId);
            GstLedgTotalAmount one = totalAmountService.getOne(lambdaQueryWrapper);
            BigDecimal totalWriteOffAmount = Optional.ofNullable(one).map(GstLedgTotalAmount::getWriteOffAmount).orElse(BigDecimal.ZERO);

            //获取totalOpBalanceAmount
            LambdaQueryWrapper<GstLedgOpBalanceReservation> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(GstLedgOpBalanceReservation::getPropertyId, propertyId).eq(GstLedgOpBalanceReservation::getResolved, resolved);
            List<GstLedgOpBalanceReservation> list = list(queryWrapper);
            BigDecimal totalOpBalanceAmount = BigDecimal.ZERO;
            if (CollectionUtils.isNotEmpty(list)) {
                totalOpBalanceAmount = list.stream().map(GstLedgOpBalanceReservation::getBalanceAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
            }
            return OpBalanceRvSummaryVO.builder()
                    .totalWriteOffAmount(totalWriteOffAmount)
                    .totalOpBalanceAmount(totalOpBalanceAmount)
                    .build();
        }

        /**
         * 计算统计日期
         *
         * @param unit     统计单位 1：月 2：季度
         * @param span     时间跨度 n个月/季度
         * @param timezone 时区
         * @return java.util.Map<java.lang.String, java.lang.Integer>
         * @author lichen
         * @date 2023/12/11
         */
        private Map<String, LocalDate> calculatestatisticDate(Integer unit, Integer span, String timezone) {
            HashMap<String, LocalDate> statisticDate = Maps.newHashMapWithExpectedSize(2);
            LocalDate currentLocalDate = LocalDate.now(ZoneId.of(timezone));
            if (PropertyLossAmountTrendQuery.MONTH == unit) {
                //当前月份的第一天往前推（n：时间跨度-1）个月
                statisticDate.put("startLocalDate", currentLocalDate.withDayOfMonth(1).minusMonths(span - 1));
                //当月的最后一天
                statisticDate.put("endLocalDate", currentLocalDate.withDayOfMonth(currentLocalDate.lengthOfMonth()));
            } else {
                //获取n个季度前的开始日期
                statisticDate.put("startLocalDate", QuarterUtil.getBeforQuarterStartDate(currentLocalDate, span));
                //获取当前季度的结束日期
                statisticDate.put("endLocalDate", QuarterUtil.getQuarterEndDate(currentLocalDate));
            }
            return statisticDate;
        }

        /**
         * 生成损失金额趋势集合
         *
         * @param opBalanceReservations opBalance集合
         * @param unit                  统计单位 1：月 2：季度
         * @param span                  统计时间跨度，多少个月/季度
         * @param propertyDTO           资产
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.vo.guestledger.LossAmountTrendVO>
         * @author lichen
         * @date 2023/12/11
         */
        private List<LossAmountTrendVO> generateLossAmountTrendVO(List<GstLedgOpBalanceReservation> opBalanceReservations, Integer unit, Integer span, PropertyDTO propertyDTO) {
            String timezone = propertyDTO.getTimezone();
            List<LossAmountTrendVO> lossAmountTrendVOList = Lists.newArrayListWithCapacity(span);
            if (PropertyLossAmountTrendQuery.MONTH == unit) {
                LocalDate localDate = LocalDate.now(ZoneId.of(timezone));

                for (int i = 1; i <= span; i++) {
                    //获取日期,例如：2023-05
                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM");
                    String date = localDate.format(formatter);

                    //当月的开始日期
                    long startTime = DateTimeUtils.localDateToLong(localDate.withDayOfMonth(1), timezone);
                    //当月的结束日期 等于 下个月的1号零点时刻。为了查出当月最后一天的数据
                    long endTime = DateTimeUtils.localDateToLong(localDate.plusMonths(1).withDayOfMonth(1), timezone);

                    //计算总损失金额
                    BigDecimal totalLossAmount = opBalanceReservations.stream()
                            .filter(item -> item.getCreatedTime() >= startTime && item.getCreatedTime() < endTime)
                            .map(GstLedgOpBalanceReservation::getWriteOffAmount)
                            .reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
                    lossAmountTrendVOList.add(new LossAmountTrendVO(date, totalLossAmount));

                    //往前推一个月
                    localDate = localDate.minusMonths(1);
                }
                return lossAmountTrendVOList;
            } else {
                LocalDate localDate = LocalDate.now(ZoneId.of(timezone));

                for (int i = 1; i <= span; i++) {
                    int year = localDate.getYear();
                    //当前日期的所属季度
                    int quarter = (localDate.getMonthValue() - 1) / 3 + 1;

                    //季度的开始日期
                    LocalDate startDate = LocalDate.of(year, quarter * 3 - 2, 1);
                    long startTime = DateTimeUtils.localDateToLong(startDate, timezone);
                    //季度的结束日期
                    LocalDate endDate = startDate.plusMonths(3).minusDays(1);
                    //结束日期时间戳 等于 季度的结束日期往后推一天的零点时刻，为了查询出结束日期当天的数据
                    long endTime = DateTimeUtils.localDateToLong(endDate.plusDays(1), timezone);

                    //计算总损失金额
                    BigDecimal totalLossAmount = opBalanceReservations.stream()
                            .filter(item -> item.getCreatedTime() >= startTime && item.getCreatedTime() < endTime)
                            .map(GstLedgOpBalanceReservation::getWriteOffAmount)
                            .reduce(BigDecimal::add).orElse(BigDecimal.ZERO);

                    //季度的日期格式为：2023-1
                    String date = year + "-" + quarter;
                    lossAmountTrendVOList.add(new LossAmountTrendVO(date, totalLossAmount));

                    //往前推一个季度
                    localDate = localDate.minusMonths(3);
                }
                return lossAmountTrendVOList;
            }
        }

        /**
         * 将指定的 Reservation 信息集合转换为 DTO 类集合输出
         *
         * @param tenantId     租户id
         * @param reservations Reservation 信息集合
         * @return java.util.List<cloud.wconnect.frontoffice.pojo.dto.guestledgercheck.OpBalanceRvDTO>
         * @author lichen
         * @date 2025/8/23
         */
        private List<OpBalanceRvDTO> convertToRvDTOList(Integer tenantId, List<GstLedgOpBalanceReservation> reservations) {
            Set<Long> rvIds = reservations.stream().map(GstLedgOpBalanceReservation::getId).collect(toSet());

            // 查询 Reservation 中的 Amount Paid 明细列表
            LambdaQueryWrapper<GstLedgPaidAmount> paidAmountLambdaQueryWrapper = new LambdaQueryWrapper<>();
            paidAmountLambdaQueryWrapper.in(GstLedgPaidAmount::getOpBalRvId, rvIds);
            List<GstLedgPaidAmount> paidAmountList = paidAmountService.list(paidAmountLambdaQueryWrapper);
            Map<Long, List<GstLedgPaidAmount>> paidAmountMap = paidAmountList.stream().collect(groupingBy(GstLedgPaidAmount::getOpBalRvId));

            // 查询 Reservation 中的 Note总数统计
            LambdaQueryWrapper<GstLedgOpBalRvNote> opBalRvNoteLambdaQueryWrapper = new LambdaQueryWrapper<>();
            opBalRvNoteLambdaQueryWrapper.in(GstLedgOpBalRvNote::getOpBalRvId, rvIds);
            List<GstLedgOpBalRvNote> opBalRvNoteList = opBalRvNoteService.list(opBalRvNoteLambdaQueryWrapper);
            Map<Long, Long> opBalRvNoteMap = opBalRvNoteList.stream().collect(groupingBy(GstLedgOpBalRvNote::getOpBalRvId, counting()));

            //获取所有的用户id
            Set<Integer> userIdSet = reservations.stream()
                    .flatMap(rv -> Stream.of(rv.getResolveUpdatedBy(), rv.getBalAmntUpdatedBy(), rv.getWrOffAmntUpdatedBy()))
                    .collect(toSet());
            Set<Integer> paidAmountUserIds = paidAmountList.stream().map(GstLedgPaidAmount::getCreatedBy).collect(toSet());
            userIdSet.addAll(paidAmountUserIds);
            userIdSet.remove(BaseConstant.ZERO);
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(tenantId, userIdSet);
            return beanMapper.mapList(reservations, OpBalanceRvDTO.class, (rv, rvDto) -> {
                rvDto.setCrsNumberList(Stream.of(StringUtils.split(rv.getCrsNumbers(), ",")).collect(toList()));

                rvDto.setBalAmntUpdatedByName(userId2Name.get(rv.getBalAmntUpdatedBy()));
                rvDto.setWrOffAmntUpdatedByName(userId2Name.get(rv.getWrOffAmntUpdatedBy()));
                rvDto.setResolveUpdatedByName(userId2Name.get(rv.getResolveUpdatedBy()));

                Long rvId = rv.getId();
                List<GstLedgPaidAmount> amountPaidList = paidAmountMap.getOrDefault(rvId, Collections.emptyList());
                if (amountPaidList.isEmpty()) {
                    rvDto.setAmountPaidList(Collections.emptyList());
                }
                // 将 Amount Paid 的明细列表按照 Paid Date 倒序输出
                List<AmountPaidDTO> amountPaidDtos = amountPaidList.stream().map(amountPaid -> {
                    AmountPaidDTO amountPaidDto = beanMapper.map(amountPaid, AmountPaidDTO.class);
                    amountPaidDto.setCreatedByName(userId2Name.get(amountPaid.getCreatedBy()));
                    return amountPaidDto;
                }).sorted(Comparator.comparing(AmountPaidDTO::getPaidDate).reversed()).collect(toList());
                rvDto.setAmountPaidList(amountPaidDtos);

                // 设置Note总数
                rvDto.setNoteTotal(opBalRvNoteMap.get(rvId));
            });
        }
    }