# AcApiServiceImpl.java

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

    import cloud.wconnect.commonlib.component.BeanMapper;
    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.BaseErrorCodeEnum;
    import cloud.wconnect.commonlib.cons.CommonErrorI18nConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.exception.ForbiddenException;
    import cloud.wconnect.commonlib.exception.ListenerException;
    import cloud.wconnect.commonlib.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.internationalization.InternationalizationNumberDTO;
    import cloud.wconnect.commonlib.service.UserInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.commonlib.util.InternationalizationUtils;
    import cloud.wconnect.finance.cons.api.ApiErrorI18nConstant;
    import cloud.wconnect.finance.cons.api.ApiStatusEnum;
    import cloud.wconnect.finance.entity.api.AcApi;
    import cloud.wconnect.finance.entity.api.AcApiComment;
    import cloud.wconnect.finance.entity.api.AcOta;
    import cloud.wconnect.finance.mapper.api.AcApiMapper;
    import cloud.wconnect.finance.pojo.bo.api.api.AcApiBO;
    import cloud.wconnect.finance.pojo.bo.api.api.AcApiListBO;
    import cloud.wconnect.finance.pojo.bo.api.ota.AcOtaBO;
    import cloud.wconnect.finance.pojo.bo.api.ota.AcOtaListBO;
    import cloud.wconnect.finance.pojo.dto.api.*;
    import cloud.wconnect.finance.pojo.query.api.ApiBatchNoQuery;
    import cloud.wconnect.finance.pojo.query.api.ApiCommissionQuery;
    import cloud.wconnect.finance.pojo.vo.api.AcExportVO;
    import cloud.wconnect.finance.pojo.vo.api.ApiExportVO;
    import cloud.wconnect.finance.service.api.IAcApiCommentService;
    import cloud.wconnect.finance.service.api.IAcApiService;
    import cloud.wconnect.finance.service.api.IAcOtaService;
    import cloud.wconnect.finance.service.api.api.IApiParseService;
    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.core.conditions.update.UpdateWrapper;
    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.Sets;
    import jakarta.annotation.Resource;
    import org.apache.commons.collections.CollectionUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.i18n.LocaleContextHolder;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;

    import java.io.InputStream;
    import java.text.MessageFormat;
    import java.util.*;
    import java.util.function.Function;
    import java.util.stream.Collectors;

    /**
     * <p>
     * api 报告数据 服务实现类
     * </p>
     *
     * @author zc
     * @since 2023-08-04
     */
    @Service
    @DS("finance")
    public class AcApiServiceImpl extends ServiceImpl<AcApiMapper, AcApi> implements IAcApiService {
        @Resource
        private IAcApiCommentService acApiCommentService;
        @Resource
        private IAcOtaService acOtaService;
        @Resource
        private UserInternalService userInternalService;
        @Resource
        private BeanMapper beanMapper;
        @Resource
        private ApplicationContext applicationContext;
        @Resource
        private I18nMessageUtils i18nMessageUtils;

        /**
         * 获取 Batch no
         *
         * @param propertyId      资产 id
         * @param apiBatchNoQuery 查询条件
         * @return Batch no 集合
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public ListPagedDTO<String> listBatchNo(int propertyId, ApiBatchNoQuery apiBatchNoQuery) {
            LambdaQueryWrapper<AcApi> queryWrapper = new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .ne(AcApi::getStatus, BaseConstant.ZERO)
                    .select(AcApi::getBatchNo)
                    .groupBy(AcApi::getBatchNo);
            long total;
            List<AcApi> batchNoList;
            if (Objects.nonNull(apiBatchNoQuery.getPage()) && Objects.nonNull(apiBatchNoQuery.getSize())) {
                queryWrapper.like(StringUtils.isNotBlank(apiBatchNoQuery.getBatchNo()),
                        AcApi::getBatchNo, apiBatchNoQuery.getBatchNo());
                Page<AcApi> page = page(new Page<>(apiBatchNoQuery.getPage(), apiBatchNoQuery.getSize()), queryWrapper);

                total = page.getTotal();
                batchNoList = page.getRecords();
            } else {
                queryWrapper.eq(AcApi::getBatchNo, apiBatchNoQuery.getBatchNo());

                batchNoList = list(queryWrapper);
                total = batchNoList.size();
            }

            if (CollectionUtils.isEmpty(batchNoList)) {
                return new ListPagedDTO<>();
            }

            return new ListPagedDTO<>(total, batchNoList.stream().map(AcApi::getBatchNo).collect(Collectors.toList()));
        }

        /**
         * 校验 Batch no 是否有效
         *
         * @param propertyId 资产 id
         * @param batchNo    Batch no
         * @return true: 有效；false: 无效
         * @author zc
         * @date 2023/8/7
         */
        @Override
        public boolean checkInUseForBatchNo(int propertyId, String batchNo) {
            long count = count(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .eq(AcApi::getBatchNo, batchNo)
                    .ne(AcApi::getStatus, BaseConstant.ZERO));
            return count > BaseConstant.LONG_ZERO;
        }

        /**
         * 校验 Batch no 是否存在
         *
         * @param propertyId 资产 id
         * @param batchNo    Batch no
         * @author zc
         * @date 2023/8/7
         */
        @Override
        public void checkBatchNoExist(int propertyId, String batchNo) {
            long count = count(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .eq(AcApi::getBatchNo, batchNo));
            if (count == BaseConstant.LONG_ZERO) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The batch no is not found; batchNo:" + batchNo + "; propertyId:" + propertyId);
            }
        }

        /**
         * 回滚数据状态
         *
         * @param acApi API 数据
         * @author zc
         * @date 2023/8/7
         */
        @Override
        public void undo(AcApi acApi) {
            updateById(new AcApi()
                    .setId(acApi.getId())
                    .setStatus(acApi.getOldStatus())
                    .setStatusUpdatedBy(acApi.getOldStatusUpdatedBy())
                    .setStatusUpdatedTime(acApi.getOldStatusUpdatedTime())
                    .setOldStatus(BaseConstant.ZERO)
                    .setOldStatusUpdatedBy(BaseConstant.ZERO)
                    .setOldStatusUpdatedTime(BaseConstant.LONG_ZERO));
        }

        /**
         * 回滚数据状态 for Matched
         *
         * @param acApi API 数据
         * @author zc
         * @date 2023/8/7
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void undoMatched(AcApi acApi) {
            // 清除 API 与 OTA 的绑定关系
            acOtaService.update(new UpdateWrapper<AcOta>().lambda()
                    .set(AcOta::getApiId, BaseConstant.LONG_ZERO)
                    .eq(AcOta::getApiId, acApi.getId()));

            // 回滚 API 状态
            updateById(new AcApi()
                    .setId(acApi.getId())
                    .setStatus(ApiStatusEnum.STATUS_MISSING_IN_PMS.getCode())
                    .setStatusUpdatedBy(BaseConstant.ZERO)
                    .setStatusUpdatedTime(acApi.getCreatedTime())
                    .setOldStatus(BaseConstant.ZERO)
                    .setOldStatusUpdatedBy(BaseConstant.ZERO)
                    .setOldStatusUpdatedTime(BaseConstant.LONG_ZERO));
        }

        /**
         * 获取 API 数据
         *
         * @param propertyDTO 资产
         * @param batchNo     Batch no
         * @param status      状态集合
         * @return AcExportDTO 集合
         * @author zc
         * @date 2023/8/7
         */
        @Override
        public List<AcExportVO> getApiExportList(PropertyDTO propertyDTO, String batchNo, List<Integer> status) {
            List<AcApi> acApiList = list(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyDTO.getId())
                    .eq(AcApi::getBatchNo, batchNo)
                    .in(AcApi::getStatus, status)
                    .orderByDesc(AcApi::getId));
            if (CollectionUtils.isEmpty(acApiList)) {
                return Lists.newArrayList(AcExportVO.builder()
                        .invoiceNo("Empty")
                        .build());
            }
            List<Long> apiIds = acApiList.stream().map(AcApi::getId).collect(Collectors.toList());

            Map<Long, AcOta> apiId2Ota = acOtaService.listByApiIds(apiIds).stream()
                    .collect(Collectors.toMap(AcOta::getApiId, Function.identity()));

            String longDateFormat = propertyDTO.getInternationalization().getDateTime().getLongDate();
            String timezone = propertyDTO.getTimezone();
            String language = propertyDTO.getLanguage();

            InternationalizationNumberDTO numberDTO = propertyDTO.getInternationalization().getNumber();
            String currency = numberDTO.getCurrency();
            String calculationRule = numberDTO.getCalculationRule();

            Map<String, List<ApiExportVO>> invoiceNo2ApiList = acApiList.stream().map(api -> {
                ApiExportVO apiExportVO = new ApiExportVO();
                AcOta acOta = apiId2Ota.get(api.getId());
                if (Objects.nonNull(acOta)) {
                    apiExportVO.setOtaBatchNo(acOta.getBatchNo());
                    apiExportVO.setOtaCrsNo(acOta.getCrsNo());
                    apiExportVO.setOtaArrivalDate(InternationalizationUtils.formatDateTime(acOta.getArrivalDate(),
                            longDateFormat, StringUtils.EMPTY, timezone, language));
                    apiExportVO.setOtaDepartureDate(InternationalizationUtils.formatDateTime(acOta.getDepartureDate(),
                            longDateFormat, StringUtils.EMPTY, timezone, language));
                    apiExportVO.setOtaLastName(acOta.getLastName());
                    apiExportVO.setOtaFirstName(acOta.getFirstName());
                    apiExportVO.setOtaRoomRevenue(InternationalizationUtils.formatAmountViewValue(acOta.getRoomRevenue(),
                            calculationRule, currency));
                    apiExportVO.setOtaNights(InternationalizationUtils.formatViewValue(acOta.getNights(),
                            0, calculationRule, StringUtils.EMPTY));
                }
                apiExportVO.setInvoiceNo(api.getInvoiceNo());
                apiExportVO.setApiStatus(i18nMessageUtils.message(ApiStatusEnum.getI18nCode(api.getStatus()),
                        LocaleContextHolder.getLocale()));
                apiExportVO.setApiBatchNo(api.getBatchNo());
                apiExportVO.setApiAirline(api.getAirline());
                apiExportVO.setApiConfNo(api.getConfNo());
                apiExportVO.setApiCheckInDate(InternationalizationUtils.formatDateTime(api.getCheckIn(),
                        longDateFormat, StringUtils.EMPTY, timezone, language));
                apiExportVO.setApiLastName(api.getLastName());
                apiExportVO.setApiFirstName(api.getFirstName());
                apiExportVO.setApiNights(InternationalizationUtils.formatViewValue(api.getNights(),
                        0, calculationRule, StringUtils.EMPTY));
                apiExportVO.setApiTotalRoom(InternationalizationUtils.formatViewValue(api.getTotalRoom(),
                        0, calculationRule, StringUtils.EMPTY));
                return apiExportVO;
            }).collect(Collectors.groupingBy(ApiExportVO::getInvoiceNo));

            List<AcExportVO> acExportVOList = Lists.newArrayList();
            invoiceNo2ApiList.forEach((invoiceNo, apiList) -> {
                AcExportVO acExportVO = new AcExportVO();
                acExportVO.setInvoiceNo(invoiceNo);
                acExportVO.setApiList(apiList);
                acExportVOList.add(acExportVO);
            });
            return acExportVOList;
        }

        /**
         * 获取 Api commission 列表数据
         *
         * @param tenantId           租户 id
         * @param propertyId         资产 id
         * @param apiCommissionQuery 查询条件
         * @return AcApiDTO 集合
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public ListPagedDTO<AcApiDTO> list(int tenantId, int propertyId, ApiCommissionQuery apiCommissionQuery) {
            // 生成 Api LambdaQueryWrapper
            LambdaQueryWrapper<AcApi> queryWrapper = generateApiQuery(propertyId, apiCommissionQuery, Boolean.TRUE);
            Page<AcApi> page = page(new Page<>(apiCommissionQuery.getPage(), apiCommissionQuery.getSize()), queryWrapper);
            List<AcApi> acApiList = page.getRecords();
            if (CollectionUtils.isEmpty(acApiList)) {
                return new ListPagedDTO<>();
            }

            // 组装 note 数量
            List<Long> apiIds = acApiList.stream().map(AcApi::getId).collect(Collectors.toList());
            Map<Long, Long> acApiId2NoteCount = acApiCommentService.listByApiIds(propertyId, apiIds).stream()
                    .collect(Collectors.groupingBy(AcApiComment::getApiId, Collectors.counting()));

            // 获取操作人名称
            Set<Integer> userIds = acApiList.stream().map(AcApi::getStatusUpdatedBy).collect(Collectors.toSet());
            Map<Integer, String> userId2Name = userInternalService.getNameMapByIds(tenantId, userIds);

            List<AcApiDTO> apiList = beanMapper.mapList(acApiList, AcApiDTO.class, (item, dto) -> {
                dto.setNoteCount(acApiId2NoteCount.getOrDefault(item.getId(), BaseConstant.LONG_ZERO));
                dto.setCanUndo(!item.getStatusUpdatedBy().equals(BaseConstant.ZERO));
                dto.setStatusUpdatedName(userId2Name.get(item.getStatusUpdatedBy()));
            });

            return new ListPagedDTO<>(page.getTotal(), apiList);
        }

        /**
         * 根据 Batch No 获取 Api commission 数据
         *
         * @param propertyId 资产 id
         * @param batchNo    Batch No
         * @param status     数据状态
         * @return AcApi 集合
         * @author zc
         * @date 2023/8/9
         */
        @Override
        public List<AcApi> listByBatchNo(int propertyId, String batchNo, int status) {
            return list(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .eq(AcApi::getBatchNo, batchNo)
                    .eq(AcApi::getStatus, status));
        }

        /**
         * 获取 Api 数据各状态统计
         *
         * @param propertyId         资产 id
         * @param apiCommissionQuery 查询条件
         * @return AcApiStatusTotalDTO
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public AcApiStatusTotalDTO getApiStatusTotal(int propertyId, ApiCommissionQuery apiCommissionQuery) {
            // 生成 Api LambdaQueryWrapper
            LambdaQueryWrapper<AcApi> queryWrapper = generateApiQuery(propertyId, apiCommissionQuery, Boolean.FALSE);
            Map<Integer, Long> status2Total = list(queryWrapper).stream()
                    .collect(Collectors.groupingBy(AcApi::getStatus, Collectors.counting()));

            return AcApiStatusTotalDTO.builder()
                    .matchedCount(status2Total.getOrDefault(ApiStatusEnum.STATUS_MATCHED.getCode(), BaseConstant.LONG_ZERO))
                    .missingInPms(status2Total.getOrDefault(ApiStatusEnum.STATUS_MISSING_IN_PMS.getCode(), BaseConstant.LONG_ZERO))
                    .noRecordInPmsCount(status2Total.getOrDefault(ApiStatusEnum.STATUS_NO_RECORD_IN_PMS.getCode(), BaseConstant.LONG_ZERO))
                    .payCloseCount(status2Total.getOrDefault(ApiStatusEnum.STATUS_PAY_CLOSE.getCode(), BaseConstant.LONG_ZERO))
                    .build();
        }

        /**
         * 校验数据是否存在
         *
         * @param id api id
         * @return AcApi
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public AcApi checkExist(long id) {
            AcApi acApi = getById(id);
            if (Objects.isNull(acApi)) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The acApi is not found by id; id: " + id);
            }
            return acApi;
        }

        /**
         * 校验数据归属
         *
         * @param propertyId 资产 id
         * @param acApi      Api
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public void checkBelong(int propertyId, AcApi acApi) {
            if (!acApi.getPropertyId().equals(propertyId)) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        MessageFormat.format("The acApi does not belong to this property; propertyId:{0}; acApiId:{1}",
                                propertyId, acApi.getId()));
            }
        }

        /**
         * 获取数据详情
         *
         * @param propertyId 资产 id
         * @param acApi      Api
         * @return ApiDataDTO
         * @author zc
         * @date 2023/8/4
         */
        @Override
        public ApiDataDTO getDetail(int propertyId, AcApi acApi) {
            AcOta acOta = acOtaService.getByApiId(acApi.getId());
            return ApiDataDTO.builder()
                    .api(beanMapper.map(acApi, AcApiDTO.class))
                    .ota(Objects.isNull(acOta) ? null : beanMapper.map(acOta, AcOtaDTO.class))
                    .build();
        }

        /**
         * 生成 Api LambdaQueryWrapper
         *
         * @param propertyId         资产 id
         * @param apiCommissionQuery 查询条件
         * @param queryList          true：getList; false: getStatusTotal
         * @return LambdaQueryWrapper<AcApi>
         * @author zc
         * @date 2023/8/4
         */
        private LambdaQueryWrapper<AcApi> generateApiQuery(int propertyId, ApiCommissionQuery apiCommissionQuery, boolean queryList) {
            LambdaQueryWrapper<AcApi> queryWrapper = new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .like(StringUtils.isNotBlank(apiCommissionQuery.getBatchNo()),
                            AcApi::getBatchNo, apiCommissionQuery.getBatchNo())
                    .like(StringUtils.isNotBlank(apiCommissionQuery.getConfNo()),
                            AcApi::getConfNo, apiCommissionQuery.getConfNo())
                    .like(StringUtils.isNotBlank(apiCommissionQuery.getInvoiceNo()),
                            AcApi::getInvoiceNo, apiCommissionQuery.getInvoiceNo())
                    .ge(Objects.nonNull(apiCommissionQuery.getFromDate()),
                            AcApi::getCheckIn, apiCommissionQuery.getFromDate())
                    .le(Objects.nonNull(apiCommissionQuery.getToDate()),
                            AcApi::getCheckIn, apiCommissionQuery.getToDate())
                    .and(StringUtils.isNotBlank(apiCommissionQuery.getName()),
                            query -> query
                                    .apply("LOWER(first_name) LIKE CONCAT('%', LOWER({0}), '%')",
                                            apiCommissionQuery.getName())
                                    .or()
                                    .apply("LOWER(last_name) LIKE CONCAT('%', LOWER({0}), '%')",
                                            apiCommissionQuery.getName()))
                    // 过滤掉 status = 0 的数据，因为这些数据未与 OTA 报告匹配过，不应该被用户看到
                    .ne(AcApi::getStatus, BaseConstant.ZERO);
            if (queryList) {
                queryWrapper.in(CollectionUtils.isNotEmpty(apiCommissionQuery.getStatus()),
                        AcApi::getStatus, apiCommissionQuery.getStatus());
            }
            queryWrapper.orderByDesc(AcApi::getId);
            return queryWrapper;
        }

        /**
         * 更新 API 数据状态
         *
         * @param status    状态 (2.No Record in PMS; 3.Matched; 4.Pay & Close)
         * @param userId    用户 id
         * @param originApi 原 api 数据
         * @author zc
         * @date 2023/8/5
         */
        @Override
        public void updateStatus(int status, int userId, AcApi originApi) {
            updateById(new AcApi()
                    .setId(originApi.getId())
                    .setStatus(status)
                    .setStatusUpdatedBy(userId)
                    .setStatusUpdatedTime(DateTimeUtils.nowTimestamp())
                    .setOldStatus(originApi.getStatus())
                    .setOldStatusUpdatedBy(originApi.getStatusUpdatedBy())
                    .setOldStatusUpdatedTime(originApi.getStatusUpdatedTime()));
        }

        /**
         * 更新 API 数据状态：3.Matched
         *
         * @param updateStatusDTO 更新内容
         * @param userId          用户 id
         * @param originApi       原 api 数据
         * @author zc
         * @date 2023/8/5
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateStatusMatched(ApiUpdateStatusDTO updateStatusDTO, int userId, AcApi originApi) {
            // 校验 OTA 数据是否存在
            AcOta acOta = acOtaService.checkExist(updateStatusDTO.getOtaId());

            // 校验 OTA 是否被占用
            if (!acOta.getApiId().equals(BaseConstant.LONG_ZERO)) {
                throw new BadRequestException(ApiErrorI18nConstant.OTA_IS_OCCUPIED_BY_API,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The OTA data is occupied by API; apiId:" + originApi.getId());
            }

            // 校验 OTA 与 API 是否是同一批次编号
            if (!acOta.getBatchNo().equals(originApi.getBatchNo())) {
                throw new BadRequestException(ApiErrorI18nConstant.OTA_NOT_MATCH_API_BATCH_NO,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The OTA does not match API Batch no; apiId:" + originApi.getId());
            }

            // OTA 绑定 API
            acOtaService.updateById(new AcOta()
                    .setId(acOta.getId())
                    .setApiId(originApi.getId()));

            // 更新 API 数据状态
            updateStatus(updateStatusDTO.getStatus(), userId, originApi);
        }

        /**
         * 根据 API id 更新状态为 Pay & Close
         *
         * @param propertyId 资产 id
         * @param userId     用户 id
         * @param ids        api id
         * @author zc
         * @date 2023/8/5
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void updateStatusPayCloseBatchByIds(int propertyId, int userId, List<Integer> ids) {
            // 校验入参 ids 是否有效
            List<AcApi> acApis = listByIds(Sets.newHashSet(ids));
            if (acApis.size() != ids.size()) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_EXIST,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("apiIds"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The number of query conditions does not match the results," +
                                " may be ids contain duplicate data or contains data that does not exist");
            }
            // 校验是否存在其他资产 id 的 API 数据
            if (acApis.stream().anyMatch(item -> !item.getPropertyId().equals(propertyId))) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(),
                        "No access operated others property data; ids: " + ids);
            }
            // 校验是否存在非 Matched 状态的 API 数据
            if (acApis.stream().anyMatch(item -> !item.getStatus().equals(ApiStatusEnum.STATUS_MATCHED.getCode()))) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_INVALID,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("api"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The ids cannot contain data that is not in the Matched status");
            }

            update(new UpdateWrapper<AcApi>().lambda()
                    .setSql("old_status = status, old_status_updated_by = status_updated_by, old_status_updated_time = status_updated_time")
                    .set(AcApi::getStatus, ApiStatusEnum.STATUS_PAY_CLOSE.getCode())
                    .set(AcApi::getStatusUpdatedBy, userId)
                    .set(AcApi::getStatusUpdatedTime, DateTimeUtils.nowTimestamp())
                    .in(AcApi::getId, ids));
        }

        /**
         * 根据查询条件更新状态为 Pay & Close
         *
         * @param propertyId 资产 id
         * @param userId     用户 id
         * @param paramDTO   查询条件
         * @author zc
         * @date 2023/8/5
         */
        @Override
        public void updateStatusPayCloseBatchByParam(int propertyId, int userId, ApiUpdateStatusBatchParamDTO paramDTO) {
            update(new UpdateWrapper<AcApi>().lambda()
                    .setSql("old_status = status, old_status_updated_by = status_updated_by, old_status_updated_time = status_updated_time")
                    .set(AcApi::getStatus, ApiStatusEnum.STATUS_PAY_CLOSE.getCode())
                    .set(AcApi::getStatusUpdatedBy, userId)
                    .set(AcApi::getStatusUpdatedTime, DateTimeUtils.nowTimestamp())
                    .eq(AcApi::getPropertyId, propertyId)
                    .like(StringUtils.isNotBlank(paramDTO.getInvoiceNo()),
                            AcApi::getInvoiceNo, paramDTO.getInvoiceNo())
                    .eq(AcApi::getBatchNo, paramDTO.getBatchNo())
                    .eq(AcApi::getStatus, ApiStatusEnum.STATUS_MATCHED.getCode()));
        }

        /**
         * 解析报告
         *
         * @param propertyDTO 资产
         * @param source      数据源平台
         * @param in          文件
         * @return ApiListBO 集合
         * @author zc
         * @date 2023/8/8
         */
        @Override
        public List<AcApiListBO> parseReport(PropertyDTO propertyDTO, String source, InputStream in) {
            Collection<IApiParseService> parserList = applicationContext.getBeansOfType(IApiParseService.class).values();
            for (IApiParseService parser : parserList) {
                if (parser.supports(source)) {
                    return parser.parse(propertyDTO, in);
                }
            }
            throw new ListenerException("Not found report parser");
        }

        /**
         * 处理导入的 API 报告
         *
         * @param property 资产
         * @param apiBO    API 数据
         * @author zc
         * @date 2023/8/8
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public ConflictApiDTO handleImportApiReport(PropertyDTO property, AcApiBO apiBO) {
            // 校验 Batch no 是否存在
            boolean isEffective = checkInUseForBatchNo(property.getId(), apiBO.getBatchNo());
            if (isEffective) {
                throw new BadRequestException(BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getI18nCode(),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The batch no is invalid");
            }

            // 清除资产下 API.status = 0 的数据
            remove(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, property.getId())
                    .eq(AcApi::getStatus, BaseConstant.ZERO));

            List<AcApi> acApiList = formatApiList(property, apiBO.getBatchNo(), apiBO.getList());

            ConflictApiDTO conflictApiDTO = new ConflictApiDTO();

            // 获取起冲突的 Invoice No 数据
            Set<String> invoiceNos = acApiList.stream().map(AcApi::getInvoiceNo).collect(Collectors.toSet());
            conflictApiDTO.setInvoiceNos(listConflictInvoiceNos(property.getId(), invoiceNos));
            conflictApiDTO.setApiList(beanMapper.mapList(listConflictApis(property.getId(), acApiList,
                    null), AcApiDTO.class));

            // 保存 API 数据
            saveBatch(acApiList);

            return conflictApiDTO;
        }

        /**
         * 获取起冲突的 Invoice No 数据
         *
         * @param propertyId 资产 id
         * @param invoiceNos Invoice No
         * @return Invoice No 集合
         * @author zc
         * @date 2023/8/9
         */
        private List<String> listConflictInvoiceNos(int propertyId, Set<String> invoiceNos) {
            return list(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .in(AcApi::getInvoiceNo, invoiceNos)).stream().map(AcApi::getInvoiceNo).distinct()
                    .collect(Collectors.toList());
        }

        /**
         * 获取起冲突的 API 数据
         *
         * @param propertyId 资产 id
         * @param acApiList  API
         * @param status     API 状态
         * @return AcApi 集合
         * @author zc
         * @date 2023/8/9
         */
        private List<AcApi> listConflictApis(int propertyId, List<AcApi> acApiList, Integer status) {
            Map<Boolean, List<AcApi>> confNoIsBlank2ApiList = acApiList.stream()
                    .collect(Collectors.partitioningBy(item -> StringUtils.isBlank(item.getConfNo())));

            List<AcApi> acApis = Lists.newArrayList();

            List<AcApi> selectCheckInAndNameApi = confNoIsBlank2ApiList.get(Boolean.FALSE);
            if (CollectionUtils.isNotEmpty(selectCheckInAndNameApi)) {
                acApis.addAll(baseMapper.listConflictApiByCheckInAndName(propertyId, selectCheckInAndNameApi, status));
            }

            List<AcApi> selectConfNos = confNoIsBlank2ApiList.get(Boolean.TRUE);
            if (CollectionUtils.isNotEmpty(selectConfNos)) {
                List<String> confNos = selectConfNos.stream().map(AcApi::getConfNo).collect(Collectors.toList());
                acApis.addAll(list(new QueryWrapper<AcApi>().lambda()
                        .eq(AcApi::getPropertyId, propertyId)
                        .in(AcApi::getConfNo, confNos)
                        .eq(Objects.nonNull(status), AcApi::getStatus, status)
                        .ne(AcApi::getStatus, BaseConstant.ZERO)));
            }
            return acApis;
        }

        /**
         * 格式化 API 数据
         *
         * @param property 资产
         * @param batchNo  Batch No
         * @param apiList  API 数据
         * @return AcApi 集合
         * @author zc
         * @date 2023/8/9
         */
        private List<AcApi> formatApiList(PropertyDTO property, String batchNo,
                                          List<AcApiListBO> apiList) {
            int propertyId = property.getId();
            String timezone = property.getTimezone();
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            return beanMapper.mapList(apiList, AcApi.class, (bo, item) -> {
                item.setPropertyId(propertyId);
                item.setBatchNo(batchNo);
                item.setCheckIn(DateTimeUtils.dateStrToEpochSecond(bo.getCheckInStr(),
                        BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone));
                item.setStatus(BaseConstant.ZERO);
                item.setStatusUpdatedBy(BaseConstant.ZERO);
                item.setStatusUpdatedTime(BaseConstant.LONG_ZERO);
                item.setOldStatus(BaseConstant.ZERO);
                item.setOldStatusUpdatedBy(BaseConstant.ZERO);
                item.setOldStatusUpdatedTime(BaseConstant.LONG_ZERO);
                item.setCreatedTime(nowTimestamp);
            });
        }

        /**
         * 处理导入的 OTA 报告
         *
         * @param property 资产
         * @param otaBO    OTA 数据
         * @author zc
         * @date 2023/8/9
         */
        @Override
        @Transactional(rollbackFor = Exception.class)
        public void handleImportOtaReport(PropertyDTO property, AcOtaBO otaBO) {
            // 获取 API 数据
            List<AcApi> apiList = listByBatchNo(property.getId(), otaBO.getBatchNo(), BaseConstant.ZERO);
            if (apiList.isEmpty()) {
                throw new BadRequestException(CommonErrorI18nConstant.DATA_NOT_FOUND,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(),
                        "The apiList is not found by batch no; batchNo:" + otaBO.getBatchNo());
            }

            /*
             * API 与 OTA 匹配，匹配规则：
             *  1、先用 ConfNo 匹配；
             *  2、再用 CheckIn + LastName 匹配；
             * 根据上述规则，如果能匹配上，设置 matched，匹配不上设置 missing in PMS。
             * 在匹配时，如果发现起冲突的数据中 API 状态是 Pay & Close，则继续沿用这个状态，并设置 API 与 OTA 绑定关系，
             * 设置绑定关系时，如果当前 OTA 报告中没有与之匹配的 数据，则 copy 一份起冲突的 API 绑定的 OTA 数据，与当前 API 绑定，
             * copy 时 BatchNo 还是之前的，不能改变
             */
            List<AcOta> acOtaList = formatOtaList(property, otaBO.getBatchNo(), otaBO.getList());
            Map<String, AcOta> crsNo2Ota = acOtaList.stream().collect(Collectors.toMap(AcOta::getCrsNo, Function.identity(), (v1, v2) -> v2));
            Map<String, AcOta> arrivalDateLastName2Ota = acOtaList.stream()
                    .collect(Collectors.toMap(AcOta::getArrivalDateAndLastName, Function.identity(), (v1, v2) -> v2));

            // 获取 API 中 Pay & Close 状态的冲突数据
            List<AcApi> payCloseApis = listConflictApis(property.getId(), apiList, ApiStatusEnum.STATUS_PAY_CLOSE.getCode());
            Map<Boolean, List<AcApi>> confNoIsBlank2Apis = payCloseApis.stream()
                    .collect(Collectors.partitioningBy(item -> StringUtils.isBlank(item.getConfNo())));

            Map<String, AcApi> checkInAndName2PayCloseApi = confNoIsBlank2Apis.getOrDefault(Boolean.TRUE, Collections.emptyList()).stream()
                    .collect(Collectors.toMap(AcApi::getCheckInAndName, Function.identity(), (v1, v2) -> v2));

            Map<String, AcApi> confNo2PayCloseApi = confNoIsBlank2Apis.getOrDefault(Boolean.FALSE, Collections.emptyList()).stream()
                    .collect(Collectors.toMap(AcApi::getConfNo, Function.identity(), (v1, v2) -> v2));

            // 获取冲突 API 对应的 OTA 数据
            List<Long> payCloseApiIds = payCloseApis.stream().map(AcApi::getId).collect(Collectors.toList());
            Map<Long, AcOta> payCloseApiId2Ota = acOtaService.listByApiIds(payCloseApiIds).stream()
                    .collect(Collectors.toMap(AcOta::getApiId, Function.identity()));

            long nowTimestamp = DateTimeUtils.nowTimestamp();

            List<AcApi> needUpdateApiList = Lists.newArrayList();
            // 开始匹配
            for (AcApi api : apiList) {
                int newStatus = ApiStatusEnum.STATUS_MATCHED.getCode();

                // ConfNo 匹配
                AcOta acOta = crsNo2Ota.get(api.getConfNo());
                if (StringUtils.isBlank(api.getConfNo()) || Objects.isNull(acOta)) {
                    // CheckIn + LastName 匹配
                    acOta = arrivalDateLastName2Ota.get(api.getCheckInAndLastName());
                    if (Objects.isNull(acOta)) {
                        // 若没有匹配上，则设置状态为 missing in PMS
                        newStatus = ApiStatusEnum.STATUS_MISSING_IN_PMS.getCode();
                    } else {
                        // 若匹配上，则设置绑定关系
                        acOta.setApiId(api.getId());
                    }
                } else {
                    // 若匹配上，则设置绑定关系
                    acOta.setApiId(api.getId());
                }

                // 匹配后，检查冲突数据中是否有当前 API，如果有，则沿用 Pay & Close 状态
                AcApi payCloseApi = StringUtils.isBlank(api.getConfNo()) ?
                        checkInAndName2PayCloseApi.get(api.getCheckInAndName()) : confNo2PayCloseApi.get(api.getConfNo());
                if (Objects.nonNull(payCloseApi)) {
                    newStatus = ApiStatusEnum.STATUS_PAY_CLOSE.getCode();

                    // 当前报告中没有与之匹配的 OTA 时，copy 冲突的 API 对应的 OTA
                    if (Objects.isNull(acOta)) {
                        acOta = payCloseApiId2Ota.get(payCloseApi.getId());
                        // copy 一个 OTA 与当前 API 绑定上
                        acOta.setId(null);
                        acOta.setApiId(api.getId());
                        acOta.setCreatedTime(nowTimestamp);

                        acOtaList.add(acOta);
                    }
                }

                AcApi updateApi = new AcApi();
                updateApi.setId(api.getId());
                updateApi.setStatus(newStatus);
                updateApi.setStatusUpdatedTime(nowTimestamp);
                needUpdateApiList.add(updateApi);
            }
            // 更新 API
            updateBatchById(needUpdateApiList);

            // 保存 OTA
            acOtaService.saveBatch(acOtaList);
        }

        /**
         * 格式化 OTA 数据
         *
         * @param property 资产
         * @param batchNo  Batch No
         * @param otaList  OTA 数据
         * @return AcApi 集合
         * @author zc
         * @date 2023/8/9
         */
        private List<AcOta> formatOtaList(PropertyDTO property, String batchNo,
                                          List<AcOtaListBO> otaList) {
            int propertyId = property.getId();
            String timezone = property.getTimezone();
            long nowTimestamp = DateTimeUtils.nowTimestamp();
            return beanMapper.mapList(otaList, AcOta.class, (bo, item) -> {
                item.setPropertyId(propertyId);
                item.setBatchNo(batchNo);
                item.setArrivalDate(DateTimeUtils.dateStrToEpochSecond(bo.getArrivalDateStr(),
                        BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone));
                item.setDepartureDate(DateTimeUtils.dateStrToEpochSecond(bo.getDepartureDateStr(),
                        BaseConstant.DATE_FORMAT_YYYY_MM_DD, timezone));
                item.setApiId(BaseConstant.LONG_ZERO);
                item.setCreatedTime(nowTimestamp);
            });
        }

        /**
         * 删除指定 Batch no 的数据
         *
         * @param propertyId 资产 id
         * @param batchNo    Batch no
         * @author zc
         * @date 2025/7/30
         */
        @Override
        public void deleteByBatchNo(int propertyId, String batchNo) {
            remove(new QueryWrapper<AcApi>().lambda()
                    .eq(AcApi::getPropertyId, propertyId)
                    .eq(AcApi::getBatchNo, batchNo));
        }
    }