# GmwrMonthlyOtbPaceServiceImpl.java

    package cloud.wconnect.workperformance.service.gmwr.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.workperformance.cons.gmwr.monthlyotbpace.GmwrMonthlyOtbPaceEnum;
    import cloud.wconnect.workperformance.entity.gmwr.GmwrMonthlyOtbPace;
    import cloud.wconnect.workperformance.entity.gmwr.GmwrMonthlyOtbPaceNote;
    import cloud.wconnect.workperformance.entity.gmwr.GmwrWeeks;
    import cloud.wconnect.workperformance.mapper.gmwr.GmwrMonthlyOtbPaceMapper;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.GmwrMonthlyOtbPaceDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.GmwrMonthlyOtbPaceValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.basic.GmwrMonthlyOtbPaceBasicDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.basic.GmwrMonthlyOtbPaceBasicValueDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.save.GmwrMonthlyOtbPaceSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.monthlyotbpace.save.GmwrMonthlyOtbPaceValueSaveDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.note.GmwrNoteDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrBasicIntegerValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrBasicValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrIntegerValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.gmwr.valuedetail.GmwrValueDetailDTO;
    import cloud.wconnect.workperformance.pojo.dto.internal.MonthlyRevenueInternalDTO;
    import cloud.wconnect.workperformance.service.MonthlyRevenueInternalService;
    import cloud.wconnect.workperformance.service.gmwr.IGmwrMonthlyOtbPaceNoteService;
    import cloud.wconnect.workperformance.service.gmwr.IGmwrMonthlyOtbPaceService;
    import cloud.wconnect.workperformance.service.gmwr.IGmwrWeeksService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    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.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.math.BigDecimal;
    import java.math.RoundingMode;
    import java.time.LocalDate;
    import java.time.YearMonth;
    import java.time.temporal.TemporalAdjusters;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;
    import java.util.stream.IntStream;

    /**
     * <p>
     * GM Weekly Report Monthly OTB Pace 服务实现类
     * </p>
     *
     * @author zc
     * @since 2024-10-12
     */
    @Service
    @DS("work_performance")
    public class GmwrMonthlyOtbPaceServiceImpl extends ServiceImpl<GmwrMonthlyOtbPaceMapper, GmwrMonthlyOtbPace> implements IGmwrMonthlyOtbPaceService {
        @Resource
        private IGmwrWeeksService weeksService;
        @Resource
        private IGmwrMonthlyOtbPaceNoteService monthlyOtbPaceNoteService;
        @Resource
        private MonthlyRevenueInternalService monthlyRevenueInternalService;
        @Resource
        private UserInternalService userInternalService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private BeanMapper beanMapper;

        /**
         * 根据周报 id 获取 otb pace 数据
         *
         * @param weeklyReportId 周报 id
         * @return GmwrMonthlyOtbPace 集合
         * @author zc
         * @date 2024/11/6
         */
        @Override
        public List<GmwrMonthlyOtbPace> listByReportId(int weeklyReportId) {
            return list(new LambdaQueryWrapper<GmwrMonthlyOtbPace>()
                    .eq(GmwrMonthlyOtbPace::getReportId, weeklyReportId));
        }

        /**
         * 获取 Monthly OTB Pace
         *
         * @param propertyId 资产 id
         * @param gmwrWeeks  周报
         * @return GmwrMonthlyOtbPaceListDTO 集合
         * @author zc
         * @date 2024/10/16
         */
        @Override
        public List<GmwrMonthlyOtbPaceDTO> get(int propertyId, GmwrWeeks gmwrWeeks) {
            LocalDate reportLocalDate = DateTimeUtils.strToLocalDate(gmwrWeeks.getWeek().toString(),
                    BaseConstant.DATE_FORMAT_YYYYMMDD);
            YearMonth currentYearMonth = YearMonthUtils.getYearMonth(DateTimeUtils.localDateToStr(reportLocalDate,
                    BaseConstant.DATE_FORMAT_YYYYMM));

            List<GmwrMonthlyOtbPaceDTO> currentOtbPaceDTOList;
            // 获取当前周数据
            List<GmwrMonthlyOtbPaceBasicDTO> currentOtbPaceBasicDTOList = listOtbPaceDTOByReportId(gmwrWeeks.getId());
            if (CollectionUtils.isNotEmpty(currentOtbPaceBasicDTOList)) {
                currentOtbPaceDTOList = getCurrentOtbPaceDTOList(propertyId, reportLocalDate, currentYearMonth, currentOtbPaceBasicDTOList);
            } else {
                currentOtbPaceDTOList = generateInitBasicDTOList(currentYearMonth);
            }

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 获取备注数据
            List<GmwrMonthlyOtbPaceNote> noteList = monthlyOtbPaceNoteService.listByReportId(gmwrWeeks.getId());
            Map<Long, Map<Integer, GmwrMonthlyOtbPaceNote>> category2NoteByMonth = noteList.stream()
                    .collect(Collectors.groupingBy(GmwrMonthlyOtbPaceNote::getMonth,
                            Collectors.toMap(GmwrMonthlyOtbPaceNote::getCategory, Function.identity())));

            // 提取用户名
            Map<Integer, String> userId2Name = extractUserName(propertyDTO.getTenantId(), currentOtbPaceBasicDTOList, noteList);
            currentOtbPaceDTOList.forEach(otbPaceDTO -> {
                Map<Integer, GmwrMonthlyOtbPaceNote> category2Note = category2NoteByMonth.getOrDefault(otbPaceDTO.getMonth(), Collections.emptyMap());
                otbPaceDTO.getList().forEach(otbPaceValueDTO -> {
                    // 设置备注
                    GmwrMonthlyOtbPaceNote note = category2Note.get(otbPaceValueDTO.getCategory());
                    if (Objects.nonNull(note)) {
                        GmwrNoteDTO noteDTO = beanMapper.map(note, GmwrNoteDTO.class, dto ->
                                dto.setUpdatedName(userId2Name.get(dto.getUpdatedBy())));
                        otbPaceValueDTO.setNote(noteDTO);
                    }
                    // 设置用户名
                    otbPaceValueDTO.getOtbRoomSold().setUpdatedName(userId2Name.get(otbPaceValueDTO.getOtbRoomSold().getUpdatedBy()));
                    otbPaceValueDTO.getOtbRoomSoldLastYear().setUpdatedName(userId2Name.get(otbPaceValueDTO.getOtbRoomSoldLastYear().getUpdatedBy()));
                    otbPaceValueDTO.getOtbRevenue().setUpdatedName(userId2Name.get(otbPaceValueDTO.getOtbRevenue().getUpdatedBy()));
                    otbPaceValueDTO.getOtbRevenueLastYear().setUpdatedName(userId2Name.get(otbPaceValueDTO.getOtbRevenueLastYear().getUpdatedBy()));
                });
            });
            return currentOtbPaceDTOList;
        }

        /**
         * 提取用户名
         *
         * @param tenantId              租户 id
         * @param monthlyOtbPaceDTOList 数据
         * @param noteList              备注
         * @return java.util.Map<java.lang.Integer, java.lang.String>
         * @author zc
         * @date 2025/5/19
         */
        private Map<Integer, String> extractUserName(int tenantId, List<GmwrMonthlyOtbPaceBasicDTO> monthlyOtbPaceDTOList,
                                                     List<GmwrMonthlyOtbPaceNote> noteList) {
            Set<Integer> userIds = Sets.newHashSet();
            monthlyOtbPaceDTOList.forEach(otbPaceDTO ->
                    otbPaceDTO.getList().forEach(otbPaceValueDTO -> {
                        userIds.add(otbPaceValueDTO.getOtbRoomSold().getUpdatedBy());
                        userIds.add(otbPaceValueDTO.getOtbRoomSoldLastYear().getUpdatedBy());
                        userIds.add(otbPaceValueDTO.getOtbRevenue().getUpdatedBy());
                        userIds.add(otbPaceValueDTO.getOtbRevenueLastYear().getUpdatedBy());
                    })
            );
            noteList.forEach(note -> userIds.add(note.getUpdatedBy()));
            // 获取用户名
            return userInternalService.getNameMapByIds(tenantId, userIds);
        }

        /**
         * 获取当前周数据
         *
         * @param propertyId                 资产 id
         * @param reportLocalDate            报告日期
         * @param currentYearMonth           当前年月
         * @param currentOtbPaceBasicDTOList 当前周数据
         * @return GmwrMonthlyOtbPaceDTO 集合
         * @author zc
         * @date 2025/3/4
         */
        private List<GmwrMonthlyOtbPaceDTO> getCurrentOtbPaceDTOList(int propertyId, LocalDate reportLocalDate, YearMonth currentYearMonth,
                                                                     List<GmwrMonthlyOtbPaceBasicDTO> currentOtbPaceBasicDTOList) {
            List<GmwrMonthlyOtbPaceDTO> currentOtbPaceDTOList = currentOtbPaceBasicDTOList.stream().map(basicDto -> {
                List<GmwrMonthlyOtbPaceValueDTO> valueDTOList = beanMapper.mapList(basicDto.getList(), GmwrMonthlyOtbPaceValueDTO.class,
                        (basicValueDto, valueDto) ->
                                valueDto.setCategoryName(GmwrMonthlyOtbPaceEnum.getCategoryName(basicValueDto.getCategory())));
                packageMonthlyOtbPaceValue(valueDTOList);
                GmwrMonthlyOtbPaceDTO otbPaceDTO = new GmwrMonthlyOtbPaceDTO();
                otbPaceDTO.setMonth(basicDto.getMonth());
                otbPaceDTO.setList(valueDTOList);
                return otbPaceDTO;
            }).toList();
            // 获取上一周期数据
            long lastWeekDate = Long.parseLong(DateTimeUtils.localDateToStr(reportLocalDate.minusDays(7),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            GmwrWeeks lastGmwrWeeks = weeksService.getByDate(propertyId, lastWeekDate);

            List<GmwrMonthlyOtbPaceDTO> lastOtbPaceDTOList = Lists.newArrayList();
            if (Objects.nonNull(lastGmwrWeeks)) {
                List<GmwrMonthlyOtbPaceBasicDTO> lastOtbPaceBasicDTOList = listOtbPaceDTOByReportId(lastGmwrWeeks.getId());
                lastOtbPaceDTOList = lastOtbPaceBasicDTOList.stream().map(basicDto -> {
                    List<GmwrMonthlyOtbPaceValueDTO> valueDTOList = beanMapper.mapList(basicDto.getList(), GmwrMonthlyOtbPaceValueDTO.class);
                    packageMonthlyOtbPaceValue(valueDTOList);
                    GmwrMonthlyOtbPaceDTO otbPaceDTO = new GmwrMonthlyOtbPaceDTO();
                    otbPaceDTO.setMonth(basicDto.getMonth());
                    otbPaceDTO.setList(valueDTOList);
                    return otbPaceDTO;
                }).toList();
            }
            // 计算 VAR TO LW
            calculateVarToLastWeek(currentOtbPaceDTOList, lastOtbPaceDTOList);
            // 截取前三个月数据
            long endMonth = YearMonthUtils.parseYearMonthToLong(currentYearMonth.plusMonths(2));
            return currentOtbPaceDTOList.stream().filter(dto -> dto.getMonth() <= endMonth)
                    .collect(Collectors.toList());
        }

        /**
         * 生成 BasicDTO 初始值
         *
         * @return List<GmwrMonthlyOtbPaceDTO>
         * @author zc
         * @date 2025/3/4
         */
        private List<GmwrMonthlyOtbPaceDTO> generateInitBasicDTOList(YearMonth currentYearMonth) {
            return IntStream.range(0, 3).mapToObj(i -> {
                long indexMonth = YearMonthUtils.parseYearMonthToLong(currentYearMonth.plusMonths(i));
                GmwrMonthlyOtbPaceDTO otbPaceListDTO = new GmwrMonthlyOtbPaceDTO();
                otbPaceListDTO.setMonth(indexMonth);
                return otbPaceListDTO;
            }).toList();
        }

        /**
         * 计算 VAR TO LW
         *
         * @param currentOtbPaceList 当前周期数据
         * @param lastOtbPaceList    上一周期数据
         * @author zc
         * @date 2024/10/17
         */
        private void calculateVarToLastWeek(List<GmwrMonthlyOtbPaceDTO> currentOtbPaceList, List<GmwrMonthlyOtbPaceDTO> lastOtbPaceList) {
            Map<Long, List<GmwrMonthlyOtbPaceValueDTO>> month2LastValueList = lastOtbPaceList.stream()
                    .collect(Collectors.toMap(GmwrMonthlyOtbPaceDTO::getMonth, GmwrMonthlyOtbPaceDTO::getList));

            currentOtbPaceList.forEach(currentOtbPaceDTO -> {
                Map<Integer, GmwrMonthlyOtbPaceValueDTO> category2CurrentValueDTO = currentOtbPaceDTO.getList().stream()
                        .collect(Collectors.toMap(GmwrMonthlyOtbPaceValueDTO::getCategory, Function.identity()));
                Map<Integer, GmwrMonthlyOtbPaceValueDTO> category2LastValueDTO = month2LastValueList.getOrDefault(currentOtbPaceDTO.getMonth(),
                        Collections.emptyList()).stream().collect(Collectors.toMap(GmwrMonthlyOtbPaceValueDTO::getCategory, Function.identity()));

                GmwrMonthlyOtbPaceValueDTO currentOtbPaceTotal = category2CurrentValueDTO.getOrDefault(GmwrMonthlyOtbPaceEnum.TOTAL.getCategory(),
                        new GmwrMonthlyOtbPaceValueDTO());
                GmwrMonthlyOtbPaceValueDTO lastOtbPaceTotal = category2LastValueDTO.getOrDefault(GmwrMonthlyOtbPaceEnum.TOTAL.getCategory(),
                        new GmwrMonthlyOtbPaceValueDTO());
                GmwrMonthlyOtbPaceValueDTO currentVarToLastWeek = new GmwrMonthlyOtbPaceValueDTO();
                currentVarToLastWeek.setCategory(GmwrMonthlyOtbPaceEnum.VAR_TO_LW.getCategory());
                currentVarToLastWeek.setCategoryName(GmwrMonthlyOtbPaceEnum.VAR_TO_LW.getCategoryName());
                currentVarToLastWeek.setOtbRoomSold(new GmwrIntegerValueDetailDTO().setValue(
                        calculateSubtractInteger(currentOtbPaceTotal.getOtbRoomSold().getValue(), lastOtbPaceTotal.getOtbRoomSold().getValue())));
                currentVarToLastWeek.setOtbRoomSoldLastYear(new GmwrIntegerValueDetailDTO().setValue(
                        calculateSubtractInteger(currentOtbPaceTotal.getOtbRoomSoldLastYear().getValue(), lastOtbPaceTotal.getOtbRoomSoldLastYear().getValue())));
                currentVarToLastWeek.setOtbPaceRoomSold(calculateSubtractInteger(currentOtbPaceTotal.getOtbPaceRoomSold(), lastOtbPaceTotal.getOtbPaceRoomSold()));
                currentVarToLastWeek.setOtbRevenue(new GmwrValueDetailDTO().setValue(
                        calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbRevenue().getValue(), lastOtbPaceTotal.getOtbRevenue().getValue())));
                currentVarToLastWeek.setOtbRevenueLastYear(new GmwrValueDetailDTO().setValue(
                        calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbRevenueLastYear().getValue(), lastOtbPaceTotal.getOtbRevenueLastYear().getValue())));
                currentVarToLastWeek.setOtbPaceRevenue(calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbPaceRevenue(), lastOtbPaceTotal.getOtbPaceRevenue()));
                currentVarToLastWeek.setOtbAdr(calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbAdr(), lastOtbPaceTotal.getOtbAdr()));
                currentVarToLastWeek.setOtbAdrLastYear(calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbAdrLastYear(), lastOtbPaceTotal.getOtbAdrLastYear()));
                currentVarToLastWeek.setOtbPaceAdr(calculateSubtractBigDecimal(currentOtbPaceTotal.getOtbPaceAdr(), lastOtbPaceTotal.getOtbPaceAdr()));
                currentOtbPaceDTO.getList().add(currentVarToLastWeek);
            });
        }

        /**
         * 获取指定周报的 Monthly OTB Pace DTO
         *
         * @param reportId 周报 id
         * @return GmwrMonthlyOtbPaceBasicDTO 集合
         * @author zc
         * @date 2024/10/17
         */
        private List<GmwrMonthlyOtbPaceBasicDTO> listOtbPaceDTOByReportId(int reportId) {
            List<GmwrMonthlyOtbPace> monthlyOtbPaceList = listByReportId(reportId);
            if (CollectionUtils.isEmpty(monthlyOtbPaceList)) {
                return Lists.newArrayList();
            }
            return monthlyOtbPaceList.stream().map(item -> {
                GmwrMonthlyOtbPaceBasicDTO monthlyOtbPaceBasicDTO = new GmwrMonthlyOtbPaceBasicDTO();
                monthlyOtbPaceBasicDTO.setMonth(item.getMonth());
                monthlyOtbPaceBasicDTO.setList(JsonUtils.convertStringToList(objectMapper,
                        item.getDetail(), GmwrMonthlyOtbPaceBasicValueDTO.class));
                return monthlyOtbPaceBasicDTO;
            }).collect(Collectors.toList());
        }

        /**
         * 组装 Monthly OTB Pace Value
         *
         * @param otbPaceValueDTOList Otb Pace Detail DTO 集合
         * @author zc
         * @date 2024/11/5
         */
        private void packageMonthlyOtbPaceValue(List<GmwrMonthlyOtbPaceValueDTO> otbPaceValueDTOList) {
            // 设置 Monthly OTB Pace Value
            otbPaceValueDTOList.forEach(item -> {
                item.setOtbPaceRoomSold(calculateSubtractInteger(item.getOtbRoomSold().getValue(), item.getOtbRoomSoldLastYear().getValue()));
                item.setOtbPaceRevenue(calculateSubtractBigDecimal(item.getOtbRevenue().getValue(), item.getOtbRevenueLastYear().getValue()));
                item.setOtbPaceAdr(calculateSubtractBigDecimal(item.getOtbAdr(), item.getOtbAdrLastYear()));
            });
            // 设置 Monthly OTB Pace Total
            GmwrMonthlyOtbPaceValueDTO total = new GmwrMonthlyOtbPaceValueDTO();
            total.setCategory(GmwrMonthlyOtbPaceEnum.TOTAL.getCategory());
            total.setCategoryName(GmwrMonthlyOtbPaceEnum.TOTAL.getCategoryName());
            total.setOtbRoomSold(new GmwrIntegerValueDetailDTO().setValue(
                    otbPaceValueDTOList.stream().map(item -> item.getOtbRoomSold().getValue())
                            .filter(Objects::nonNull).reduce(Integer::sum).orElse(null)));
            total.setOtbRoomSoldLastYear(new GmwrIntegerValueDetailDTO().setValue(
                    otbPaceValueDTOList.stream().map(item -> item.getOtbRoomSoldLastYear().getValue())
                            .filter(Objects::nonNull).reduce(Integer::sum).orElse(null)));
            total.setOtbPaceRoomSold(calculateSubtractInteger(total.getOtbRoomSold().getValue(), total.getOtbRoomSoldLastYear().getValue()));
            total.setOtbRevenue(new GmwrValueDetailDTO().setValue(
                    otbPaceValueDTOList.stream().map(item -> item.getOtbRevenue().getValue())
                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null)));
            total.setOtbRevenueLastYear(new GmwrValueDetailDTO().setValue(
                    otbPaceValueDTOList.stream().map(item -> item.getOtbRevenueLastYear().getValue())
                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null)));
            total.setOtbPaceRevenue(calculateSubtractBigDecimal(total.getOtbRevenue().getValue(), total.getOtbRevenueLastYear().getValue()));
            total.setOtbAdr(otbPaceValueDTOList.stream().map(GmwrMonthlyOtbPaceValueDTO::getOtbAdr)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null));
            total.setOtbAdrLastYear(otbPaceValueDTOList.stream().map(GmwrMonthlyOtbPaceValueDTO::getOtbAdrLastYear)
                    .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null));
            total.setOtbPaceAdr(calculateSubtractBigDecimal(total.getOtbAdr(), total.getOtbAdrLastYear()));
            otbPaceValueDTOList.add(total);
        }

        /**
         * 计算 Integer 减法
         *
         * @param value1 value1
         * @param value2 value2
         * @return java.lang.Integer
         * @author zc
         * @date 2024/10/16
         */
        private Integer calculateSubtractInteger(Integer value1, Integer value2) {
            if (Objects.isNull(value1) || Objects.isNull(value2)) {
                return null;
            }
            return value1 - value2;
        }

        /**
         * 计算 BigDecimal 减法
         *
         * @param value1 value1
         * @param value2 value2
         * @return java.math.BigDecimal
         * @author zc
         * @date 2024/10/16
         */
        private BigDecimal calculateSubtractBigDecimal(BigDecimal value1, BigDecimal value2) {
            if (Objects.isNull(value1) || Objects.isNull(value2)) {
                return null;
            }
            return value1.subtract(value2);
        }

        /**
         * 生成 Monthly OTB Pace 数据
         *
         * @param propertyDTO 资产
         * @param gmwrWeeks   周报
         * @author zc
         * @date 2024/10/17
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void generate(PropertyDTO propertyDTO, GmwrWeeks gmwrWeeks) {
            LocalDate weekStartDate = DateTimeUtils.strToLocalDate(gmwrWeeks.getWeek().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            long versionDate = Long.parseLong(DateTimeUtils.localDateToStr(weekStartDate.plusWeeks(1),
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            // 获取当年当月和未来三个月的数据
            LocalDate currentFirstDateOfMonth = weekStartDate.withDayOfMonth(1);
            long currentYearStartDate = Long.parseLong(DateTimeUtils.localDateToStr(currentFirstDateOfMonth,
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            LocalDate currentEndDateOfMonth = weekStartDate.plusMonths(3).with(TemporalAdjusters.lastDayOfMonth());
            long currentYearEndDate = Long.parseLong(DateTimeUtils.localDateToStr(currentEndDateOfMonth, BaseConstant.DATE_FORMAT_YYYYMMDD));
            List<MonthlyRevenueInternalDTO> currentYearSegmentList = monthlyRevenueInternalService.listMonthlyRevenue(propertyDTO.getId(),
                    versionDate, currentYearStartDate, currentYearEndDate);

            // 获取去年同期日期
            LocalDate sameDateLastYear = DateTimeUtils.getSameDateLastYear(weekStartDate);

            // 获取去年同周期的 Monthly OTB Pace 数据
            long lyWeekStartDate = Long.parseLong(DateTimeUtils.localDateToStr(sameDateLastYear,
                    BaseConstant.DATE_FORMAT_YYYYMMDD));
            GmwrWeeks lyGmwrWeeks = weeksService.getByDate(propertyDTO.getId(), lyWeekStartDate);
            List<GmwrMonthlyOtbPaceBasicDTO> lastYearSegmentList = Lists.newArrayList();
            if (Objects.nonNull(lyGmwrWeeks)) {
                lastYearSegmentList = listOtbPaceDTOByReportId(lyGmwrWeeks.getId());
            }

            /*
             * 若获取的去年同期数据不是同一个月，则获取去年同月的 Monthly Revenue 数据
             * 1、2025年7月31日 ~ 8月6日周期会展示 2025年7月、8月、9月 的 OTB 数据；
             * 2、去年同周期是 2024年8月1日 ~ 8月7日，会展示 2024年8月、9月、10月 的 OTB 数据；
             * 3、2025年7月31日周期需要获取 “去年同期（Same Time Last Year）” 的 OTB 数据，但是（2024年8月1日~7日）没有7月份的数据，因为它已经进入8月；
             * 4、方案：在这种“跨月导致去年没有对应月份 OTB”的情况下，应该改用 2024年7月的 Final 数据（即7月最终确定的完整数据，
             *    需要从 Monthly Revenue 功能获取）作为去年同期的参考
             */
            if (weekStartDate.getMonthValue() != sameDateLastYear.getMonthValue()) {
                // 例：当前周期所在月份是 7 月，则获取去年 7 月数据
                LocalDate lyFirstDateOfMonth = currentFirstDateOfMonth.minusYears(1);
                long lastYearStartDate = Long.parseLong(DateTimeUtils.localDateToStr(lyFirstDateOfMonth,
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                long lastYearEndDate = Long.parseLong(DateTimeUtils.localDateToStr(lyFirstDateOfMonth.with(TemporalAdjusters.lastDayOfMonth()),
                        BaseConstant.DATE_FORMAT_YYYYMMDD));
                List<MonthlyRevenueInternalDTO> lastYearSegmentFinalList = monthlyRevenueInternalService.listMonthlyRevenue(propertyDTO.getId(),
                        versionDate, lastYearStartDate, lastYearEndDate);

                // 因为跨月导致去年同期没有对应月份的 OTB 数据，所以获取对应月份的 Final 数据，并添加到去年同期集合中，供后续逻辑使用
                lastYearSegmentList.add(covertMonthlyOtbPaceBasicDTO(lyFirstDateOfMonth, lastYearSegmentFinalList));
            }

            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());
            // 生成 Monthly OTB Pace Basic DTO 数据
            List<GmwrMonthlyOtbPaceBasicDTO> basicDTOList = generateBaiscDTOList(weekStartDate,
                    currentYearSegmentList, lastYearSegmentList, roundingMode);

            List<GmwrMonthlyOtbPace> monthlyOtbPaceList = beanMapper.mapList(basicDTOList, GmwrMonthlyOtbPace.class,
                    (basicDTO, item) -> {
                        item.setPropertyId(propertyDTO.getId());
                        item.setReportId(gmwrWeeks.getId());
                        item.setMonth(basicDTO.getMonth());
                        item.setDetail(JsonUtils.convertObjListToString(objectMapper, basicDTO.getList()));
                    }
            );
            // 批量保存数据
            insertOrUpdateBatchByUnique(monthlyOtbPaceList);
        }

        /**
         * 转换 Monthly Revenue Internal DTO 为 Monthly OTB Pace Basic DTO
         *
         * @param localDate 日期
         * @param list      待转换数据
         * @return GmwrMonthlyOtbPaceBasicDTO
         * @author zc
         * @date 2025/12/10
         */
        private GmwrMonthlyOtbPaceBasicDTO covertMonthlyOtbPaceBasicDTO(LocalDate localDate, List<MonthlyRevenueInternalDTO> list) {
            int transientSold = 0;
            int groupSold = 0;
            int contractSold = 0;
            BigDecimal transientRevenue = BigDecimal.ZERO;
            BigDecimal groupRevenue = BigDecimal.ZERO;
            BigDecimal contractRevenue = BigDecimal.ZERO;
            for (MonthlyRevenueInternalDTO item : list) {
                transientSold += item.getTransientSold();
                transientRevenue = transientRevenue.add(item.getTransientRevenue());
                groupSold += item.getGroupSold();
                groupRevenue = groupRevenue.add(item.getGroupRevenue());
                contractSold += item.getContractSold();
                contractRevenue = contractRevenue.add(item.getContractRevenue());
            }
            GmwrMonthlyOtbPaceBasicDTO resultDTO = new GmwrMonthlyOtbPaceBasicDTO();
            resultDTO.setMonth(Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM)));
            resultDTO.setList(Lists.newArrayList(
                    new GmwrMonthlyOtbPaceBasicValueDTO()
                            .setCategory(GmwrMonthlyOtbPaceEnum.TRANSIENT.getCategory())
                            .setOtbRoomSold(new GmwrBasicIntegerValueDetailDTO()
                                    .setValue(transientSold))
                            .setOtbRevenue(new GmwrBasicValueDetailDTO()
                                    .setValue(transientRevenue)),
                    new GmwrMonthlyOtbPaceBasicValueDTO()
                            .setCategory(GmwrMonthlyOtbPaceEnum.GROUP_DEF_TEN.getCategory())
                            .setOtbRoomSold(new GmwrBasicIntegerValueDetailDTO()
                                    .setValue(groupSold))
                            .setOtbRevenue(new GmwrBasicValueDetailDTO()
                                    .setValue(groupRevenue)),
                    new GmwrMonthlyOtbPaceBasicValueDTO()
                            .setCategory(GmwrMonthlyOtbPaceEnum.CONTRACT.getCategory())
                            .setOtbRoomSold(new GmwrBasicIntegerValueDetailDTO()
                                    .setValue(contractSold))
                            .setOtbRevenue(new GmwrBasicValueDetailDTO()
                                    .setValue(contractRevenue))));

            return resultDTO;
        }

        /**
         * 生成 Monthly OTB Pace Basic DTO 数据
         *
         * @param targetDate             目标日期
         * @param currentYearSegmentList 当年数据
         * @param lastYearSegmentList    去年数据
         * @param roundingMode           舍入模式
         * @return GmwrMonthlyOtbPace 集合
         * @author zc
         * @date 2024/10/17
         */
        private List<GmwrMonthlyOtbPaceBasicDTO> generateBaiscDTOList(LocalDate targetDate, List<MonthlyRevenueInternalDTO> currentYearSegmentList,
                                                                      List<GmwrMonthlyOtbPaceBasicDTO> lastYearSegmentList, RoundingMode roundingMode) {
            Map<Integer, List<MonthlyRevenueInternalDTO>> month2CurrentDTOList = currentYearSegmentList.stream()
                    .collect(Collectors.groupingBy(item -> DateTimeUtils.strToLocalDate(String.valueOf(item.getDate()),
                            BaseConstant.DATE_FORMAT_YYYYMMDD).getMonthValue()));

            Map<Integer, Map<Integer, GmwrMonthlyOtbPaceBasicValueDTO>> lyCategory2OtbDTOByMonth = lastYearSegmentList.stream()
                    .collect(Collectors.toMap(item ->
                                    YearMonthUtils.getYearMonth(item.getMonth().toString()).getMonthValue(),
                            item -> item.getList().stream()
                                    .collect(Collectors.toMap(GmwrMonthlyOtbPaceBasicValueDTO::getCategory, Function.identity()))));

            List<GmwrMonthlyOtbPaceBasicDTO> basicDTOList = Lists.newArrayList();
            // 由于存在跨月情况，需要取四个月数据
            for (int i = 0; i < 4; i++) {
                LocalDate localDate = targetDate.plusMonths(i);
                int monthValue = localDate.getMonthValue();
                List<MonthlyRevenueInternalDTO> currentDTOList = month2CurrentDTOList.getOrDefault(monthValue, Collections.emptyList());
                // 去年同周期数据
                Map<Integer, GmwrMonthlyOtbPaceBasicValueDTO> lyCategory2OtbDTO = lyCategory2OtbDTOByMonth.getOrDefault(monthValue, Collections.emptyMap());

                GmwrMonthlyOtbPaceBasicDTO basicDTO = new GmwrMonthlyOtbPaceBasicDTO();
                basicDTO.setMonth(Long.parseLong(DateTimeUtils.localDateToStr(localDate, BaseConstant.DATE_FORMAT_YYYYMM)));

                List<GmwrMonthlyOtbPaceBasicValueDTO> basicValueDTOList = basicDTO.getList().stream().map(item -> {
                    GmwrMonthlyOtbPaceBasicValueDTO basicValueDTO = new GmwrMonthlyOtbPaceBasicValueDTO();
                    basicValueDTO.setCategory(item.getCategory());

                    GmwrMonthlyOtbPaceBasicValueDTO lyOtbDTO = lyCategory2OtbDTO.getOrDefault(item.getCategory(),
                            new GmwrMonthlyOtbPaceBasicValueDTO());
                    if (item.getCategory() == GmwrMonthlyOtbPaceEnum.TRANSIENT.getCategory()) {
                        packageDetailValue(currentDTOList, lyOtbDTO, MonthlyRevenueInternalDTO::getTransientSold,
                                MonthlyRevenueInternalDTO::getTransientRevenue, basicValueDTO, roundingMode);
                    } else if (item.getCategory() == GmwrMonthlyOtbPaceEnum.GROUP_DEF_TEN.getCategory()) {
                        packageDetailValue(currentDTOList, lyOtbDTO, MonthlyRevenueInternalDTO::getGroupSold,
                                MonthlyRevenueInternalDTO::getGroupRevenue, basicValueDTO, roundingMode);
                    } else if (item.getCategory() == GmwrMonthlyOtbPaceEnum.CONTRACT.getCategory()) {
                        packageDetailValue(currentDTOList, lyOtbDTO, MonthlyRevenueInternalDTO::getContractSold,
                                MonthlyRevenueInternalDTO::getContractRevenue, basicValueDTO, roundingMode);
                    }
                    return basicValueDTO;
                }).toList();
                basicDTO.setList(basicValueDTOList);

                basicDTOList.add(basicDTO);
            }
            return basicDTOList;
        }

        /**
         * 组装详情数据 DTO
         *
         * @param currentDTOList  当前周期数据
         * @param lyOtbDTO        去年同周期数据
         * @param soldFunction    soldFunction
         * @param revenueFunction revenueFunction
         * @param detailValueDTO  待组装 DTO
         * @param roundingMode    舍入模式
         * @author zc
         * @date 2024/11/16
         */
        private void packageDetailValue(List<MonthlyRevenueInternalDTO> currentDTOList,
                                        GmwrMonthlyOtbPaceBasicValueDTO lyOtbDTO,
                                        Function<MonthlyRevenueInternalDTO, Integer> soldFunction,
                                        Function<MonthlyRevenueInternalDTO, BigDecimal> revenueFunction,
                                        GmwrMonthlyOtbPaceBasicValueDTO detailValueDTO, RoundingMode roundingMode) {
            detailValueDTO.setOtbRoomSold(new GmwrBasicIntegerValueDetailDTO()
                    .setValue(currentDTOList.stream().map(soldFunction)
                            .filter(Objects::nonNull).reduce(Integer::sum).orElse(null)));

            detailValueDTO.setOtbRoomSoldLastYear(new GmwrBasicIntegerValueDetailDTO()
                    .setValue(lyOtbDTO.getOtbRoomSold().getValue()));

            detailValueDTO.setOtbRevenue(new GmwrBasicValueDetailDTO()
                    .setValue(currentDTOList.stream().map(revenueFunction)
                            .filter(Objects::nonNull).reduce(BigDecimal::add).orElse(null)));

            detailValueDTO.setOtbRevenueLastYear(new GmwrBasicValueDetailDTO()
                    .setValue(lyOtbDTO.getOtbRevenue().getValue()));

            // 计算 adr
            calculateAdrForBasicValueDTO(detailValueDTO, roundingMode);
        }

        /**
         * 计算 adr for Basic Value DTO
         *
         * @param basicValueDTO 待计算 DTO
         * @param roundingMode  舍入模式
         * @author zc
         * @date 2025/12/11
         */
        private void calculateAdrForBasicValueDTO(GmwrMonthlyOtbPaceBasicValueDTO basicValueDTO, RoundingMode roundingMode) {
            if (Objects.nonNull(basicValueDTO.getOtbRevenue().getValue())
                    && Objects.nonNull(basicValueDTO.getOtbRoomSold().getValue())
                    && basicValueDTO.getOtbRoomSold().getValue() != 0) {
                basicValueDTO.setOtbAdr(basicValueDTO.getOtbRevenue().getValue()
                        .divide(new BigDecimal(basicValueDTO.getOtbRoomSold().getValue().toString()),
                                BaseConstant.DATA_PRECISION, roundingMode));
            }
            if (Objects.nonNull(basicValueDTO.getOtbRevenueLastYear().getValue())
                    && Objects.nonNull(basicValueDTO.getOtbRoomSoldLastYear().getValue())
                    && basicValueDTO.getOtbRoomSoldLastYear().getValue() != 0) {
                basicValueDTO.setOtbAdrLastYear(basicValueDTO.getOtbRevenueLastYear().getValue()
                        .divide(new BigDecimal(basicValueDTO.getOtbRoomSoldLastYear().getValue().toString()),
                                BaseConstant.DATA_PRECISION, roundingMode));
            }
        }

        /**
         * 保存 Monthly OTB Pace 数据
         *
         * @param propertyId  资产 id
         * @param gmwrWeeks   周报
         * @param saveDTOList 保存数据
         * @param operatorId  操作人 id
         * @author zc
         * @date 2025/5/16
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(int propertyId, GmwrWeeks gmwrWeeks, List<GmwrMonthlyOtbPaceSaveDTO> saveDTOList, int operatorId) {
            if (CollectionUtils.isEmpty(saveDTOList)) {
                return;
            }

            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            RoundingMode roundingMode = InternationalizationUtils.getRoundingMode(propertyDTO
                    .getInternationalization().getNumber().getCalculationRule());

            Map<Long, Map<Integer, GmwrMonthlyOtbPaceValueSaveDTO>> category2NewValueDTOByMonth = saveDTOList.stream()
                    .collect(Collectors.toMap(GmwrMonthlyOtbPaceSaveDTO::getMonth,
                            item -> item.getList().stream()
                                    .collect(Collectors.toMap(GmwrMonthlyOtbPaceValueSaveDTO::getCategory, Function.identity()))));
            // 获取当前 Monthly OTB Pace 数据
            Map<Long, Map<Integer, GmwrMonthlyOtbPaceBasicValueDTO>> category2OriginValueDTOByMonth =
                    listOtbPaceDTOByReportId(gmwrWeeks.getId()).stream().collect(Collectors.toMap(GmwrMonthlyOtbPaceBasicDTO::getMonth,
                            item -> item.getList().stream()
                                    .collect(Collectors.toMap(GmwrMonthlyOtbPaceBasicValueDTO::getCategory, Function.identity()))));

            long nowTimestamp = DateTimeUtils.nowTimestamp();
            LocalDate localDate = DateTimeUtils.strToLocalDate(gmwrWeeks.getWeek().toString(), BaseConstant.DATE_FORMAT_YYYYMMDD);
            List<GmwrMonthlyOtbPace> updateOtbPaceList = Lists.newArrayListWithExpectedSize(4);
            for (int i = 0; i < 4; i++) {
                long month = Long.parseLong(DateTimeUtils.localDateToStr(localDate.plusMonths(i),
                        BaseConstant.DATE_FORMAT_YYYYMM));

                Map<Integer, GmwrMonthlyOtbPaceValueSaveDTO> category2NewValueDTO =
                        category2NewValueDTOByMonth.getOrDefault(month, Maps.newHashMap());

                Map<Integer, GmwrMonthlyOtbPaceBasicValueDTO> category2OriginValueDTO =
                        category2OriginValueDTOByMonth.getOrDefault(month, Maps.newHashMap());

                List<GmwrMonthlyOtbPaceBasicValueDTO> basicValueDTOList = Lists.newArrayListWithExpectedSize(3);
                for (int category = 1; category < 4; category++) {
                    GmwrMonthlyOtbPaceValueSaveDTO newValueDTO = category2NewValueDTO.getOrDefault(category,
                            new GmwrMonthlyOtbPaceValueSaveDTO());
                    GmwrMonthlyOtbPaceBasicValueDTO basicValueDTO = category2OriginValueDTO.getOrDefault(category,
                            new GmwrMonthlyOtbPaceBasicValueDTO());
                    basicValueDTO.setCategory(category);
                    weeksService.editValue(basicValueDTO.getOtbRoomSold(), newValueDTO.getOtbRoomSold(), operatorId, nowTimestamp);
                    weeksService.editValue(basicValueDTO.getOtbRoomSoldLastYear(), newValueDTO.getOtbRoomSoldLastYear(), operatorId, nowTimestamp);
                    weeksService.editValue(basicValueDTO.getOtbRevenue(), newValueDTO.getOtbRevenue(), operatorId, nowTimestamp);
                    weeksService.editValue(basicValueDTO.getOtbRevenueLastYear(), newValueDTO.getOtbRevenueLastYear(), operatorId, nowTimestamp);

                    // 计算 adr
                    calculateAdrForBasicValueDTO(basicValueDTO, roundingMode);

                    basicValueDTOList.add(basicValueDTO);
                }
                updateOtbPaceList.add(new GmwrMonthlyOtbPace().setPropertyId(propertyId).setReportId(gmwrWeeks.getId())
                        .setMonth(month).setDetail(JsonUtils.convertObjListToString(objectMapper, basicValueDTOList)));
            }
            // 批量保存数据
            insertOrUpdateBatchByUnique(updateOtbPaceList);
        }

        /**
         * 批量保存数据
         *
         * @param list 数据
         * @author zc
         * @date 2026/1/27
         */
        private void insertOrUpdateBatchByUnique(List<GmwrMonthlyOtbPace> list) {
            if (CollectionUtils.isEmpty(list)) {
                return;
            }
            // 按唯一索引字段排序，避免死锁
            list.sort(Comparator.comparing(GmwrMonthlyOtbPace::getReportId)
                    .thenComparing(GmwrMonthlyOtbPace::getMonth));

            baseMapper.insertOrUpdateBatchByUnique(list);
        }
    }