# WeeklyMonthlyRevenueReportController.java

    package cloud.wconnect.rateshop.controller.weeklymonthlyrevenue;

    import cloud.wconnect.commonlib.annotation.ApiId;
    import cloud.wconnect.commonlib.component.SessionContext;
    import cloud.wconnect.commonlib.config.jxlscommand.StyleCommand;
    import cloud.wconnect.commonlib.cons.*;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.InternalServerErrorException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.rateshop.cons.S3BucketFunctionCons;
    import cloud.wconnect.rateshop.cons.weeklymonthlyrevenue.*;
    import cloud.wconnect.rateshop.entity.weeklymonthlyrevenue.WeeklyMonthlyRevenueImportLog;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.WeeklyMonthlyRevenueImportBudgetReportDTO;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.WeeklyMonthlyRevenueImportLogDTO;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.WeeklyMonthlyRevenueImportReportDTO;
    import cloud.wconnect.rateshop.pojo.dto.weeklymonthlyrevenue.WeeklyMonthlyRevenueResolveExceptionDTO;
    import cloud.wconnect.rateshop.pojo.query.weeklymonthlyrevenue.WeeklyMonthlyRevenueImportLogQuery;
    import cloud.wconnect.rateshop.service.AmazonS3Service;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IWMRevenueAutoResolveReportMissService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IWMRevenueManualImportReportHandleService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IWeeklyMonthlyRevenueImportLogService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.IWeeklyMonthlyRevenueRoomService;
    import cloud.wconnect.rateshop.service.weeklymonthlyrevenue.budget.IWMRevenueBudgetParseService;
    import com.google.common.collect.Lists;
    import jakarta.annotation.Resource;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.validation.Valid;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.jxls.builder.xls.XlsCommentAreaBuilder;
    import org.jxls.transform.poi.JxlsPoiTemplateFillerBuilder;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.IOException;
    import java.io.InputStream;
    import java.text.MessageFormat;
    import java.util.*;

    /**
     * <p>
     * Weekly Monthly Revenue Report 前端控制器
     * </p>
     *
     * @author zc
     * @since 2024-05-13
     */
    @RestController
    @RequestMapping("/weekly-monthly-revenue/report")
    public class WeeklyMonthlyRevenueReportController {
        @Resource
        private IWeeklyMonthlyRevenueImportLogService importLogService;
        @Resource
        private IWeeklyMonthlyRevenueRoomService monthlyRevenueRoomService;
        @Resource
        private PropertyI18nRedisService propertyI18nRedisService;
        @Resource
        private SessionContext sessionContext;
        @Resource
        private ApplicationContext applicationContext;
        @Resource
        private I18nMessageUtils i18nMessageUtils;
        @Resource
        private AmazonS3Service amazonS3Service;

        /**
         * 获取报告导入记录
         *
         * @param property 资产标识
         * @param query    查询条件
         * @return WeeklyMonthlyRevenueImportLogDTO 集合
         * @author zc
         * @date 2024/5/17
         */
        @ApiId(1171)
        @GetMapping("/import-log")
        public ListPagedDTO<WeeklyMonthlyRevenueImportLogDTO> listImportLog(@RequestParam String property, @Valid WeeklyMonthlyRevenueImportLogQuery query) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 校验报告类型参数
            checkReportTypeParams(propertyDTO.getPmsType(), query.getReportType());

            return importLogService.list(propertyDTO, query);
        }

        /**
         * 解决导入异常记录
         *
         * @param property            资产标识
         * @param id                  异常记录 id
         * @param resolveExceptionDTO 解决异常DTO
         * @return empty map
         * @author zc
         * @date 2024/5/17
         */
        @ApiId(1172)
        @PutMapping("/import-log/{id:\\d+}/resolve")
        public Map<Object, Object> resolve(@RequestParam String property, @PathVariable("id") long id,
                                           @Valid WeeklyMonthlyRevenueResolveExceptionDTO resolveExceptionDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);
            // 校验是否存在
            WeeklyMonthlyRevenueImportLog importLog = importLogService.checkExist(id);
            // 校验归属
            importLogService.checkBelong(propertyId, importLog);
            // 登录人
            int loginUserId = sessionContext.getLoginUserId();

            if (WeeklyMonthlyRevenueResolveExceptionDTO.IGNORE == resolveExceptionDTO.getType()) {
                // 校验是否是失败、缺失记录
                checkImportResult(importLog.getImportResult());

                // 更新记录为忽略
                importLogService.ignore(importLog, loginUserId);
            } else if (WeeklyMonthlyRevenueResolveExceptionDTO.MANUAL_RESOLVE == resolveExceptionDTO.getType()) {
                // 校验是否是失败、缺失记录
                checkImportResult(importLog.getImportResult());

                MultipartFile file = resolveExceptionDTO.getFile();
                //校验文件不能为空
                if (Objects.isNull(file)) {
                    throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                            i18nMessageUtils.getErrorParamMapNeedTranslate(CommonFieldI18nConstant.FILE),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "The file cannot empty.");
                }
                String source = resolveExceptionDTO.getSource();
                //校验source不能为空
                if (StringUtils.isBlank(source)) {
                    throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                            i18nMessageUtils.getErrorParamMapNeedTranslate(CommonFieldI18nConstant.DATA_INTEGRATION_SOURCE),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "The source cannot empty.");
                }
                // 校验数据集成参数
                checkDataIntegrationParams(propertyDTO.getPmsType(), source, importLog.getReportType());
                // 日期范围
                List<Long> dateRange = Lists.newArrayList(importLog.getStartDate(), importLog.getEndDate());

                Collection<IWMRevenueManualImportReportHandleService> handleServices = applicationContext.getBeansOfType(IWMRevenueManualImportReportHandleService.class).values();
                for (IWMRevenueManualImportReportHandleService handleService : handleServices) {
                    if (handleService.supportsByReportType(importLog.getReportType())) {
                        // 校验文件
                        handleService.checkFile(propertyDTO, source, file);
                        // 处理数据
                        handleService.handleData(propertyDTO, file, dateRange, importLog.getCategory(),
                                importLog.getVersionDate(), loginUserId, source, importLog.getId());
                        break;
                    }
                }
            } else {
                // 校验是否是缺失状态的记录
                if (importLog.getImportResult() != ReportImportResultCons.IMPORT_RESULT_MISSED) {
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_ONLY_HANDLE_MISSED,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "Only report import records with missing status can be processed");
                }
                // 校验s3文件是否存在
                if (StringUtils.isBlank(importLog.getUrl())) {
                    throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                            i18nMessageUtils.getErrorParamMapNeedTranslate(CommonFieldI18nConstant.FILE),
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "The s3 file cannot empty.");
                }
                boolean fileExists = amazonS3Service.isFileExists(S3BucketFunctionCons.WEEKLY_MONTHLY_REVENUE_IMPORT_RECORD, importLog.getUrl());
                if (!fileExists) {
                    //文件已过期，无法自动解决
                    throw new BadRequestException(MonthlyRevenueErrorI18nConstant.FILE_EXPIRED_CANNOT_AUTO_SOLVE,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            "The file has expired and cannot be automatically resolved.");
                }

                // 校验文件和资产配置的PMS是否匹配
                checkFileAndPMSTypeIsMatch(importLog.getUrl(), propertyDTO.getPmsType());

                Collection<IWMRevenueAutoResolveReportMissService> autoResolveReportMissServices = applicationContext.getBeansOfType(IWMRevenueAutoResolveReportMissService.class).values();
                for (IWMRevenueAutoResolveReportMissService service : autoResolveReportMissServices) {
                    if (service.supportsByReportType(importLog.getReportType())) {
                        // 自动解决
                        service.autoResolve(propertyDTO, importLog, loginUserId, propertyDTO.getPmsType());
                        break;
                    }
                }
            }
            return Collections.emptyMap();
        }

        /**
         * 导入报告
         *
         * @param property  资产标识
         * @param reportDTO Import Report DTO
         * @return empty map
         * @author zc
         * @date 2024/5/20
         */
        @ApiId(1173)
        @PostMapping
        public Map<Object, Object> importReport(@RequestParam String property, @Valid WeeklyMonthlyRevenueImportReportDTO reportDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            // 校验数据集成参数
            checkDataIntegrationParams(propertyDTO.getPmsType(), reportDTO.getSource(), reportDTO.getReportType());

            // 校验报告类型和分类是否匹配
            checkReportTypeAndCategoryIsMatch(propertyDTO.getPmsType(),reportDTO);

            int loginUserId = sessionContext.getLoginUserId();

            String source = reportDTO.getSource();
            int reportType = reportDTO.getReportType();
            MultipartFile file = reportDTO.getFile();
            List<Long> dateRange = Lists.newArrayList(reportDTO.getStartDate(), reportDTO.getEndDate());

            Collection<IWMRevenueManualImportReportHandleService> handleServices = applicationContext.getBeansOfType(IWMRevenueManualImportReportHandleService.class).values();
            for (IWMRevenueManualImportReportHandleService handleService : handleServices) {
                if (handleService.supportsByReportType(reportType)) {
                    // 校验日期参数
                    handleService.checkDate(propertyDTO, source, reportDTO);
                    // 校验文件
                    handleService.checkFile(propertyDTO, source, file);
                    // 处理数据
                    handleService.handleData(propertyDTO, file, dateRange, reportDTO.getCategory(),
                            reportDTO.getVersionDate(), loginUserId, source, null);
                    break;
                }
            }
            return Collections.emptyMap();
        }

        /**
         * 校验报告类型和分类是否匹配
         *
         * @param pms       pms 类型
         * @param reportDTO 导入DTO
         * @author lichen
         * @date 2025/10/31
         */
        private void checkReportTypeAndCategoryIsMatch(String pms, WeeklyMonthlyRevenueImportReportDTO reportDTO) {
            if (reportDTO.getCategory() == MonthlyRevenueImportCategoryCons.FORECAST) {
                // opera/shiji： History and Forecast、Group Pickup、Reservation Forecast 报告支持导入 forecast category
                // pep：History and Forecast 报告支持导入 forecast category
                List<Integer> effectReportTypeList = listReportTypeForForecastCategory(pms);
                if (!effectReportTypeList.contains(reportDTO.getReportType())) {
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_REPORT_TYPE_NOT_SUPPORTED,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            MessageFormat.format("The currently selected report type is not supported, " +
                                    "reportType must be in {0}", effectReportTypeList));
                }
            } else {
                // opera：Group Pickup、Reservation Statistics、Cancellation Summary、Manager Flash 报告支持导入 history category
                // shiji：Group Pickup、Reservation forecast、Cancellation Summary、Manager Flash 报告支持导入 history category
                List<Integer> effectReportTypeList = listReportType(pms);
                if (!effectReportTypeList.contains(reportDTO.getReportType())) {
                    throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_REPORT_TYPE_NOT_SUPPORTED,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                            MessageFormat.format("The currently selected report type is not supported, " +
                                    "reportType must be in {0}", effectReportTypeList));
                }
            }
        }

        /**
         * 获取 forecast category 下支持的报告类型
         *
         * @param pms pms 类型
         * @return 报告类型
         * @author yyt
         * @date 2026/2/27
         **/
        private List<Integer> listReportTypeForForecastCategory(String pms) {
            if (pms.equals(SystemPlatformEnum.PEP.getDesc())) {
                return List.of(MonthlyRevenueReportCategoryEnum.HISTORY_AND_FORECAST.getCategory());
            } else {
                return List.of(MonthlyRevenueReportCategoryEnum.HISTORY_AND_FORECAST.getCategory(),
                        MonthlyRevenueReportCategoryEnum.GROUP_PICKUP.getCategory(),
                        MonthlyRevenueReportCategoryEnum.RESERVATION_FORECAST.getCategory());
            }
        }

        /**
         * 获取支持的报告类型
         *
         * @param pms pms 类型
         * @return 报告类型
         * @author yyt
         * @date 2026/2/27
         **/
        private List<Integer> listReportType(String pms) {
            if (pms.equals(SystemPlatformEnum.OPERA.getDesc())) {
                return List.of(MonthlyRevenueReportCategoryEnum.GROUP_PICKUP.getCategory(),
                        MonthlyRevenueReportCategoryEnum.RESERVATION_STATISTICS.getCategory(),
                        MonthlyRevenueReportCategoryEnum.CANCELLATION_SUMMARY.getCategory(),
                        MonthlyRevenueReportCategoryEnum.MANAGER_FLASH.getCategory());
            } else {
                return List.of(MonthlyRevenueReportCategoryEnum.GROUP_PICKUP.getCategory(),
                        MonthlyRevenueReportCategoryEnum.RESERVATION_FORECAST.getCategory(),
                        MonthlyRevenueReportCategoryEnum.CANCELLATION_SUMMARY.getCategory(),
                        MonthlyRevenueReportCategoryEnum.MANAGER_FLASH.getCategory());
            }
        }

        /**
         * 导入报告
         *
         * @param property  资产标识
         * @param reportDTO Import Report Budget DTO
         * @return empty map
         * @author zc
         * @date 2024/5/20
         */
        @ApiId(1174)
        @PostMapping("/budget")
        public Map<Object, Object> importBudgetReport(@RequestParam String property, @Valid WeeklyMonthlyRevenueImportBudgetReportDTO reportDTO) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            IWMRevenueBudgetParseService parser = getBudgetParser(SystemPlatformEnum.OTHER.getDesc());

            // 校验文件内容是否合法
            parser.checkFile(propertyDTO, reportDTO.getFile());
            // 处理数据
            parser.handleData(propertyDTO, reportDTO.getFile(), reportDTO.getTheYear(), sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 导出报告
         *
         * @param property  资产标识
         * @param yearMonth 年月(yyyyMM)
         * @param response  HttpServletResponse
         * @author zc
         * @date 2025/10/21
         */
        @ApiId(1618)
        @GetMapping
        public void export(@RequestParam String property, long yearMonth, HttpServletResponse response) {
            int propertyId = sessionContext.getPropertyId(property);
            PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyId);

            String templateFilePath = String.format("tmpl-xls/monthlyrevenue/monthly_revenue_tmpl_%s.xlsx", LocaleContextHolder.getLocale());
            try (InputStream is = new ClassPathResource(templateFilePath).getInputStream()) {
                HashMap<String, Object> data = monthlyRevenueRoomService.generateContext(propertyDTO, yearMonth, sessionContext.getLoginUserId());

                String fileName = String.format("MonthlyRevenue_%s_%s.xlsx", property, yearMonth);
                response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
                response.addHeader("Access-Control-Expose-Headers", "Content-Disposition");
                response.setHeader("Content-Disposition", String.join("", "attachment; filename=", fileName));

                XlsCommentAreaBuilder xlsCommentAreaBuilder = new XlsCommentAreaBuilder();
                xlsCommentAreaBuilder.addCommandMapping(StyleCommand.NAME, StyleCommand.class);

                JxlsPoiTemplateFillerBuilder.newInstance()
                        .withAreaBuilder(xlsCommentAreaBuilder)
                        .withTemplate(is)
                        .buildAndFill(data, response::getOutputStream);
            } catch (IOException exp) {
                throw new InternalServerErrorException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.SYSTEM_ERROR.getCode(),
                        "Export Monthly Report failed; exp:" + exp);
            }
        }

        /**
         * 校验是否是失败、缺失记录
         *
         * @param importResult 导入结果
         * @author zc
         * @date 2024/6/13
         */
        private void checkImportResult(int importResult) {
            if (importResult != ReportImportResultCons.IMPORT_RESULT_FAIL
                    && importResult != ReportImportResultCons.IMPORT_RESULT_MISSED) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_ONLY_HANDLE_EXCEPTION,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "Cannot operate status is not importResultFail or importResultMissed log");
            }
        }

        /**
         * 获取报告解析服务 for Budget
         *
         * @param source 来源标识
         * @return 报告解析服务
         * @author zc
         * @date 2024/8/22
         */
        private IWMRevenueBudgetParseService getBudgetParser(String source) {
            Collection<IWMRevenueBudgetParseService> parserList =
                    applicationContext.getBeansOfType(IWMRevenueBudgetParseService.class).values();
            IWMRevenueBudgetParseService parser = null;
            for (IWMRevenueBudgetParseService parseService : parserList) {
                if (parseService.supports(source)) {
                    parser = parseService;
                    break;
                }
            }
            if (Objects.isNull(parser)) {
                throw new BadRequestException(BaseErrorCodeEnum.SYSTEM_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.SYSTEM_ERROR.getCode(),
                        "Can not find Monthly Revenue Budget report parser");
            }
            return parser;
        }

        /**
         * 校验数据集成参数
         *
         * @param propertyPmsType 属性PMS类型
         * @param sourceSystem    来源系统
         * @param reportType      报告类型
         * @author zc
         * @date 2025/7/4
         */
        private void checkDataIntegrationParams(String propertyPmsType, String sourceSystem, int reportType) {
            List<String> effectSourceList = List.of(propertyPmsType, SystemPlatformEnum.OTHER.getDesc());
            if (!effectSourceList.contains(sourceSystem)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_SOURCE_SYSTEM_NOT_SUPPORTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The currently selected source system is not supported, " +
                                "sourceSystem must be in {0}", effectSourceList));
            }
            // 校验报告类型参数
            checkReportTypeParams(propertyPmsType, List.of(reportType));
        }

        /**
         * 校验报告类型参数
         *
         * @param propertyPmsType 属性PMS类型
         * @param reportTypeList  报告类型集合
         * @author zc
         * @date 2025/7/5
         */
        private void checkReportTypeParams(String propertyPmsType, List<Integer> reportTypeList) {
            if (CollectionUtils.isEmpty(reportTypeList)) {
                return;
            }
            List<Integer> effectReportTypeList;
            if (SystemPlatformEnum.OPERA.getDesc().equals(propertyPmsType)) {
                effectReportTypeList = MonthlyRevenueCons.OPERA_PMS_REPORT_TYPE_LIST;
            } else if (SystemPlatformEnum.SHI_JI.getDesc().equals(propertyPmsType)){
                effectReportTypeList = MonthlyRevenueCons.SHIJI_PMS_REPORT_TYPE_LIST;
            } else {
                effectReportTypeList = MonthlyRevenueCons.PEP_PMS_REPORT_TYPE_LIST;
            }
            if (!new HashSet<>(effectReportTypeList).containsAll(reportTypeList)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_INTEGRATION_REPORT_TYPE_NOT_SUPPORTED,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        MessageFormat.format("The currently selected report type is not supported, " +
                                "reportType must be in {0}", effectReportTypeList));
            }
        }

        /**
         * 校验文件和资产配置的PMS是否匹配
         *
         * @param url     文件 url
         * @param pmsType 资产配置的PMS 类型
         * @author lichen
         * @date 2025/11/5
         */
        private void checkFileAndPMSTypeIsMatch(String url, String pmsType) {
            //文件后缀
            String fileSuffix = url.substring(url.lastIndexOf(".") + 1);

            //是否匹配
            boolean isMatch = false;

            if (pmsType.equals(SystemPlatformEnum.OPERA.getDesc())) {
                if ("xml".equals(fileSuffix) || "XML".equals(fileSuffix)) {
                    isMatch = true;
                }
            } else if (Lists.newArrayList(SystemPlatformEnum.SHI_JI.getDesc(), SystemPlatformEnum.PEP.getDesc()).contains(pmsType)) {
                if ("csv".equals(fileSuffix) || "CSV".equals(fileSuffix)) {
                    isMatch = true;
                }
            }

            if (!isMatch) {
                //文件类型不正确，无法自动解决
                throw new BadRequestException(MonthlyRevenueErrorI18nConstant.FILE_TYPE_INCORRECT_CANNOT_AUTO_SOLVE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The file type is incorrect and cannot be automatically resolved");
            }
        }
    }