# SaleForecastServiceImpl.java

    package cloud.wconnect.workperformance.service.sva.saleforecast.impl;

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.MoudleEventSubjectEnum;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.JsonUtils;
    import cloud.wconnect.commonlib.util.TransactionUtils;
    import cloud.wconnect.sns.http.processor.Sender;
    import cloud.wconnect.workperformance.entity.sva.saleforecast.SaleForecast;
    import cloud.wconnect.workperformance.mapper.sva.saleforecast.SaleForecastMapper;
    import cloud.wconnect.workperformance.pojo.bo.modelevent.dephourstatistics.SvaDepHourStatisticsEventBO;
    import cloud.wconnect.workperformance.pojo.dto.sva.saleforecast.SaleForecastExceptionDTO;
    import cloud.wconnect.workperformance.pojo.dto.sva.saleforecast.SaleForecastInfoDTO;
    import cloud.wconnect.workperformance.service.sva.saleforecast.ISaleForecastService;
    import com.baomidou.dynamic.datasource.annotation.DS;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.time.ZonedDateTime;
    import java.time.temporal.TemporalAdjusters;
    import java.util.Collections;
    import java.util.Comparator;
    import java.util.List;
    import java.util.Map;
    import java.util.concurrent.Executor;
    import java.util.stream.Collectors;

    /**
     * <p>
     * 服务实现类：Sale Forecast信息
     * </p>
     *
     * @author zc
     * @since 2023-03-27
     */
    @Service
    @DS("work_performance")
    public class SaleForecastServiceImpl extends ServiceImpl<SaleForecastMapper, SaleForecast> implements ISaleForecastService {
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ObjectMapper objectMapper;
        @Resource
        private Executor executor;
        @Resource
        @Qualifier("moduleEventSender")
        private Sender moduleEventSender;

        /**
         * 获取指定月份内的 Sale Forecast 信息集合
         *
         * @param propertyDTO 资产
         * @param targetDate  目标日期（月初时间戳）
         * @return Sale Forecast 信息集合
         * @author zc
         * @date 2023/3/27
         */
        @Override
        public List<SaleForecast> list(PropertyDTO propertyDTO, long targetDate) {
            ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(targetDate, propertyDTO.getTimezone());
            long fromDate = zonedDateTime.with(TemporalAdjusters.firstDayOfMonth()).toEpochSecond();
            long toDate = zonedDateTime.with(TemporalAdjusters.lastDayOfMonth()).toEpochSecond();
            return listByDateRange(propertyDTO.getId(), fromDate, toDate);
        }

        /**
         * 获取指定时间范围内的 Sale Forecast 信息集合
         *
         * @param propertyId property id
         * @param fromDate   开始日期
         * @param toDate     结束日期
         * @return Sale Forecast 信息集合
         * @author zc
         * @date 2023/3/28
         */
        @Override
        public List<SaleForecast> listByDateRange(int propertyId, long fromDate, long toDate) {
            return list(new LambdaQueryWrapper<SaleForecast>()
                    .eq(SaleForecast::getPropertyId, propertyId)
                    .between(SaleForecast::getTheDate, fromDate, toDate));
        }

        /**
         * 保存 Sale Forecast
         *
         * @param propertyDTO         资产
         * @param saleForecastInfoDTO SaleForecast 内容
         * @param operatorId          操作人 id
         * @author zc
         * @date 2025/4/10
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void save(PropertyDTO propertyDTO, SaleForecastInfoDTO saleForecastInfoDTO, int operatorId) {
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            SaleForecast saleForecast = beanMapper.map(saleForecastInfoDTO, SaleForecast.class);
            saleForecast.setPropertyId(propertyDTO.getId());
            saleForecast.setNotWriteInTime(Boolean.FALSE);
            saleForecast.setCreatedBy(operatorId);
            saleForecast.setCreatedTime(nowTimestamp);
            saleForecast.setLastUpdatedBy(operatorId);
            saleForecast.setLastUpdatedTime(nowTimestamp);
            baseMapper.insertOrUpdateByUnique(saleForecast);

            // 触发 Hotel KPI - FTE 指标的重新计算，指定日期范围内数据发生变化时，发送 SNS 事件
            TransactionUtils.afterCommitAsyncExecute(executor, () -> {
                long date = DateTimeUtils.parseToLong(DateTimeUtils.toZonedDateTime(saleForecastInfoDTO.getTheDate(),
                        propertyDTO.getTimezone()), BaseConstant.DATE_FORMAT_YYYYMMDD);
                sendModuleEvent(propertyDTO.getId(), Lists.newArrayList(date, date));
            });
        }

        /**
         * 发送数据变更事件
         *
         * @param propertyId 资产 id
         * @param dateRange  日期范围(yyyyMMdd)
         * @author zc
         * @date 2025/4/4
         */
        private void sendModuleEvent(int propertyId, List<Long> dateRange) {
            SvaDepHourStatisticsEventBO eventBO = SvaDepHourStatisticsEventBO.builder()
                    .propertyId(propertyId)
                    .startDate(dateRange.get(0))
                    .endDate(dateRange.get(1))
                    .build();
            String messages = JsonUtils.convertObjListToString(objectMapper, Lists.newArrayList(eventBO));
            moduleEventSender.publish(MoudleEventSubjectEnum.SVA_DEPARTMENT_HOUR_STATISTICS.getSubject(), messages);
        }

        /**
         * 获取 Sale Forecast 未按时填写数据的异常集合
         *
         * @param propertyDTO 资产
         * @return Sale Forecast 集合
         * @author zc
         * @date 2025/4/10
         */
        @Override
        public List<SaleForecastExceptionDTO> listExceptions(PropertyDTO propertyDTO) {
            List<SaleForecast> saleForecastList = list(new LambdaQueryWrapper<SaleForecast>()
                    .eq(SaleForecast::getPropertyId, propertyDTO.getId())
                    .eq(SaleForecast::getNotWriteInTime, Boolean.TRUE));
            if (CollectionUtils.isEmpty(saleForecastList)) {
                return Collections.emptyList();
            }

            String timezone = propertyDTO.getTimezone();
            Map<Long, Long> date2Count = saleForecastList.stream().collect(Collectors.groupingBy(item -> {
                ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(item.getTheDate(), timezone);
                return zonedDateTime.with(TemporalAdjusters.firstDayOfMonth()).toEpochSecond();
            }, Collectors.counting()));

            return date2Count.entrySet().stream().map(entry -> {
                SaleForecastExceptionDTO exceptionDTO = new SaleForecastExceptionDTO();
                exceptionDTO.setTheDate(entry.getKey());
                exceptionDTO.setDayNum(entry.getValue().intValue());
                return exceptionDTO;
            }).sorted(Comparator.comparing(SaleForecastExceptionDTO::getTheDate)).toList();
        }

        /**
         * 忽略 Sale Forecast 未按时填写数据的异常
         *
         * @param propertyDTO 资产
         * @param targetDate  目标日期（月初时间戳）
         * @param operatorId  操作人 id
         * @author zc
         * @date 2025/4/10
         */
        @Override
        public void ignore(PropertyDTO propertyDTO, long targetDate, int operatorId) {
            ZonedDateTime zonedDateTime = DateTimeUtils.toZonedDateTime(targetDate, propertyDTO.getTimezone());
            long fromDate = zonedDateTime.with(TemporalAdjusters.firstDayOfMonth()).toEpochSecond();
            long toDate = zonedDateTime.with(TemporalAdjusters.lastDayOfMonth()).toEpochSecond();
            update(new LambdaUpdateWrapper<SaleForecast>()
                    .eq(SaleForecast::getPropertyId, propertyDTO.getId())
                    .eq(SaleForecast::getNotWriteInTime, Boolean.TRUE)
                    .between(SaleForecast::getTheDate, fromDate, toDate)
                    .set(SaleForecast::getNotWriteInTime, Boolean.FALSE)
                    .set(SaleForecast::getIgnoredBy, operatorId)
                    .set(SaleForecast::getIgnoredTime, DateTimeUtils.nowTimestamp()));
        }
    }