# BudgetDatabaseController.java

    package cloud.wconnect.rateshop.controller.budgetdatabase;

    import cloud.wconnect.commonlib.annotation.ApiId;
    import cloud.wconnect.commonlib.component.SessionContext;
    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.pojo.dto.ListPagedDTO;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.I18nMessageUtils;
    import cloud.wconnect.rateshop.cons.budgetdatabase.*;
    import cloud.wconnect.rateshop.entity.budgetdatabase.BudgetDatabaseConfigFbTab;
    import cloud.wconnect.rateshop.entity.budgetdatabase.BudgetDatabaseConfigOtherTab;
    import cloud.wconnect.rateshop.entity.budgetdatabase.BudgetDatabaseConfigRoomTab;
    import cloud.wconnect.rateshop.pojo.dto.budgetdatabase.*;
    import cloud.wconnect.rateshop.pojo.query.budgetdatabase.StatusLogQuery;
    import cloud.wconnect.rateshop.service.budgetdatabase.*;
    import com.google.common.collect.ImmutableMap;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.validation.Valid;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.validation.annotation.Validated;
    import org.springframework.web.bind.annotation.*;

    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.stream.Collectors;

    /**
     * <p>
     *  前端控制器
     * </p>
     *
     * @author yyt
     * @since 2025-08-13
     */
    @Validated
    @RestController
    @RequestMapping("/budget-database")
    public class BudgetDatabaseController {

        @Autowired
        private SessionContext sessionContext;
        @Autowired
        private IBudgetDatabaseTotalService budgetDatabaseTotalService;
        @Autowired
        private IBudgetDatabaseRoomRgiService budgetDatabaseRoomRgiService;
        @Autowired
        private IBudgetDatabaseStatusLogService budgetDatabaseStatusLogService;
        @Autowired
        private IBudgetDatabaseService budgetDatabaseService;
        @Autowired
        private IBudgetDatabaseExportService budgetDatabaseExportService;
        @Autowired
        private IBudgetDatabaseConfigRoomTabService budgetDatabaseConfigRoomTabService;
        @Autowired
        private IBudgetDatabaseConfigRoomTabItemService budgetDatabaseConfigRoomTabItemService;
        @Autowired
        private IBudgetDatabaseConfigFbTabService budgetDatabaseConfigFbTabService;
        @Autowired
        private IBudgetDatabaseConfigOtherTabService budgetDatabaseConfigOtherTabService;
        @Autowired
        private IBudgetDatabaseOtherRevenueService budgetDatabaseOtherRevenueService;
        @Autowired
        private IBudgetDatabaseRoomService budgetDatabaseRoomService;
        @Autowired
        private IBudgetDatabaseRoomOtherService budgetDatabaseRoomOtherService;
        @Autowired
        private IBudgetDatabaseFbRevenueService budgetDatabaseFbRevenueService;
        @Autowired
        private IBudgetDatabaseFbCoversService budgetDatabaseFbCoversService;
        @Autowired
        private IBudgetDatabaseConfigFbTabItemService budgetDatabaseConfigFbTabItemService;
        @Autowired
        private IBudgetDatabaseConfigOtherTabItemService budgetDatabaseConfigOtherTabItemService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private I18nMessageUtils i18nMessageUtils;

        /**
         * 1542 - 获取 Total
         *
         * @param year     年
         * @param property 资产标识
         * @return Total
         * @author yyt
         * @date 2025/8/13
         **/
        @ApiId(1542)
        @RequestMapping("/{year:\\d+}/total")
        public TotalDTO listTotal(@PathVariable int year, String property) {
            return budgetDatabaseTotalService.listTotal(sessionContext.getPropertyId(property), year);
        }

        /**
         * 1645 - 获取 Room 分类下的 Total
         *
         * @param tabId    tab id
         * @param property 资产标识
         * @return Total
         * @author yyt
         * @date 2025/11/13
         **/
        @ApiId(1645)
        @GetMapping("/{tabId:\\d+}/room-total")
        public RoomTotalDTO listRoomTotal(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);
            return budgetDatabaseTotalService.listRoomTotal(tab);
        }

        /**
         * 1646 - 编辑 Room 分类下的 Total
         *
         * @param tabId             tab id
         * @param property          资产标识
         * @param updateRevenueDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/11/14
         **/
        @ApiId(1646)
        @PostMapping("/{tabId:\\d+}/room-total")
        public Map<String, Object> editRoomTotal(@PathVariable int tabId, String property, @RequestBody List<@Valid UpdateOneYearRevenueDTO> updateRevenueDTOS) {
            if (CollectionUtils.isEmpty(updateRevenueDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateRevenueDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateRevenueDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);

            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkExist(tabId);
            budgetDatabaseConfigRoomTabService.checkBelong(tab, propertyId);

            Integer year = tab.getBudgetYear();

            // 检查当前分类下的指标存在
            budgetDatabaseConfigRoomTabItemService.checkExist(propertyId, tabId, RoomClassificationEnum.OTHER.getCode(),
                    updateRevenueDTOS.stream().map(UpdateOneYearRevenueDTO::getItemId).collect(Collectors.toList()));

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseRoomOtherService.checkCanUpdateBudgetForStatus(propertyId, year, updateRevenueDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseRoomOtherService.checkCanUpdateForReopen(propertyId, year, updateRevenueDTOS);

            // 自动参与计算的，不能修改预算数据
            budgetDatabaseRoomOtherService.checkCanUpdateForParticipateCalculate(propertyId, year, updateRevenueDTOS);

            budgetDatabaseRoomOtherService.editRoomTotal(propertyId, year, tabId, updateRevenueDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1543 - 获取 Room
         *
         * @param tabId    年
         * @param property 资产标识
         * @return Segment Statistics
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1543)
        @RequestMapping("/{tabId:\\d+}/room")
        public List<RoomOperationInfoDTO> listRoom(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);

            return budgetDatabaseRoomService.listRoom(tab, true);
        }

        /**
         * 1544 - 获取 Room 中 Month Statistics
         *
         * @param tabId    tab id
         * @param property 资产标识
         * @return Month Statistics
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1544)
        @RequestMapping("/{tabId:\\d+}/month-statistics")
        public MonthStatisticsDTO listMonthStatistics(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);

            return budgetDatabaseRoomService.listMonthStatistics(tab, true);
        }

        /**
         * 1545 - 获取某个月的 Market Segmentation
         *
         * @param tabId    tab id
         * @param month    月
         * @param property 资产标识
         * @return Market Segmentation
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1545)
        @RequestMapping("/{tabId:\\d+}/market-segmentation/{month:\\d+}")
        public MarketSegmentationDTO listMarketSegmentation(@PathVariable int tabId, @PathVariable int month, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);

            return budgetDatabaseRoomService.listMarketSegmentation(propertyId, tab, month);
        }

        /**
         * 1546 - 获取 F&B 下的 Revenue
         *
         * @param tabId    tab id
         * @param property 资产标识
         * @return F&B
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1546)
        @RequestMapping("/{tabId:\\d+}/fb-revenue")
        public FbRevenueDTO listFbRevenue(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigFbTab tab = budgetDatabaseConfigFbTabService.checkTab(tabId, propertyId);
            return budgetDatabaseFbRevenueService.listFbRevenue(tab, true);
        }

        /**
         * 1643 - 获取 F&B 下的 Covers
         *
         * @param tabId    tab id
         * @param property 资产标识
         * @return F&B
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1643)
        @RequestMapping("/{tabId:\\d+}/fb-covers")
        public FbCoversDTO listFbCovers(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigFbTab tab = budgetDatabaseConfigFbTabService.checkTab(tabId, propertyId);
            return budgetDatabaseFbCoversService.listFbCoversDTO(tab);
        }

        /**
         * 1547 - 获取 Other
         *
         * @param tabId     tab id
         * @param property 资产标识
         * @return Other
         * @author yyt
         * @date 2025/8/14
         **/
        @ApiId(1547)
        @RequestMapping("/{tabId:\\d+}/other")
        public OtherDTO listOther(@PathVariable int tabId, String property) {
            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigOtherTab tab = budgetDatabaseConfigOtherTabService.checkTab(tabId, propertyId);
            return budgetDatabaseOtherRevenueService.listOther(tab, true);
        }

        /**
         * 1548 - 编辑 Month rgi
         *
         * @param tabId               tab id
         * @param property           资产标识
         * @param updateMonthRgiDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1548)
        @PostMapping("/{tabId:\\d+}/rgi")
        public Map<String, Object> updateMonthRgi(@PathVariable int tabId, String property, @RequestBody List<@Valid UpdateMonthRgiDTO> updateMonthRgiDTOS) {
            if(CollectionUtils.isEmpty(updateMonthRgiDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateMonthRgiDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateMonthRgiDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseRoomRgiService.checkCanUpdateBudgetForStatus(tab, updateMonthRgiDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseRoomRgiService.checkCanUpdateForReopen(tab, updateMonthRgiDTOS);

            budgetDatabaseRoomRgiService.updateMonthRgi(propertyId, tab, updateMonthRgiDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1549 - 编辑某个月的 Market Segmentation
         *
         * @param tabId                        tab id
         * @param month                        月
         * @param property                     资产标识
         * @param updateMarketSegmentationDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1549)
        @PostMapping("/{tabId:\\d+}/market-segmentation/{month:\\d+}")
        public Map<String, Object> updateMarketSegmentation(@PathVariable int tabId, @PathVariable int month, String property,
                                                            @RequestBody List<@Valid UpdateMarketSegmentationDTO> updateMarketSegmentationDTOS) {
            if (CollectionUtils.isEmpty(updateMarketSegmentationDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateMarketSegmentationDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateMarketSegmentationDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigRoomTab tab = budgetDatabaseConfigRoomTabService.checkTab(tabId, propertyId);

            // 检查当前分类下的指标存在
            budgetDatabaseConfigRoomTabItemService.checkExist(propertyId, tabId, RoomClassificationEnum.ROOM.getCode(),
                    updateMarketSegmentationDTOS.stream().map(UpdateMarketSegmentationDTO::getRoomId).collect(Collectors.toList()));

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseRoomService.checkCanUpdateBudgetForStatus(tab, month, updateMarketSegmentationDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseRoomService.checkCanUpdateForReopen(tab, month, updateMarketSegmentationDTOS);

            // 自动参与计算的，不能修改预算数据
            budgetDatabaseRoomService.checkCanUpdateForParticipateCalculate(tab, month, updateMarketSegmentationDTOS);

            budgetDatabaseRoomService.updateMarketSegmentation(tab, month, updateMarketSegmentationDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1550 - 编辑 F&B 下的 Revenue
         *
         * @param tabId              tab id
         * @param property          资产标识
         * @param updateRevenueDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1550)
        @PostMapping("/{tabId:\\d+}/fb-revenue")
        public Map<String, Object> updateFbRevenue(@PathVariable int tabId, String property, @RequestBody List<@Valid UpdateOneYearRevenueDTO> updateRevenueDTOS) {
            if (CollectionUtils.isEmpty(updateRevenueDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateRevenueDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateRevenueDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigFbTab tab = budgetDatabaseConfigFbTabService.checkTab(tabId, propertyId);

            // 检查当前分类下的指标存在
            budgetDatabaseConfigFbTabItemService.checkExist(propertyId, tabId, FbClassificationEnum.REVENUE.getCode(),
                    updateRevenueDTOS.stream().map(UpdateOneYearRevenueDTO::getItemId).collect(Collectors.toList()));

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseFbRevenueService.checkCanUpdateBudgetForStatus(tab, updateRevenueDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseFbRevenueService.checkCanUpdateForReopen(tab, updateRevenueDTOS);

            // 自动参与计算的，不能修改预算数据
            budgetDatabaseFbRevenueService.checkCanUpdateForParticipateCalculate(tab, updateRevenueDTOS);

            budgetDatabaseFbRevenueService.updateFbRevenue(tab, updateRevenueDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1644 - 编辑 F&B 下的 Covers
         *
         * @param tabId              tab id
         * @param property          资产标识
         * @param updateRevenueDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1644)
        @PostMapping("/{tabId:\\d+}/fb-covers")
        public Map<String, Object> updateFbCovers(@PathVariable int tabId, String property, @RequestBody List<@Valid UpdateOneYearRoomsDTO> updateRevenueDTOS) {
            if (CollectionUtils.isEmpty(updateRevenueDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateRevenueDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateRevenueDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigFbTab tab = budgetDatabaseConfigFbTabService.checkTab(tabId, propertyId);

            // 检查当前分类下的指标存在
            budgetDatabaseConfigFbTabItemService.checkExist(propertyId, tabId, FbClassificationEnum.COVERS.getCode(),
                    updateRevenueDTOS.stream().map(UpdateOneYearRoomsDTO::getItemId).collect(Collectors.toList()));

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseFbCoversService.checkCanUpdateBudgetForStatus(tab, updateRevenueDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseFbCoversService.checkCanUpdateForReopen(tab, updateRevenueDTOS);

            // 自动参与计算的，不能修改预算数据
            budgetDatabaseFbCoversService.checkCanUpdateForParticipateCalculate(tab, updateRevenueDTOS);

            budgetDatabaseFbCoversService.updateFbCovers(tab, updateRevenueDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1551 - 编辑 Other
         *
         * @param tabId              tab id
         * @param property          资产标识
         * @param updateRevenueDTOS 修改信息
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1551)
        @PostMapping("/{tabId:\\d+}/other")
        public Map<String, Object> updateOther(@PathVariable int tabId, String property, @RequestBody List<@Valid UpdateOneYearRevenueDTO> updateRevenueDTOS) {
            if (CollectionUtils.isEmpty(updateRevenueDTOS)) {
                throw new BadRequestException(CommonErrorI18nConstant.CANNOT_EMPTY,
                        i18nMessageUtils.getErrorParamMapNotNeedTranslate("updateRevenueDTOS"),
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "the updateRevenueDTOS is empty");
            }

            int propertyId = sessionContext.getPropertyId(property);
            BudgetDatabaseConfigOtherTab tab = budgetDatabaseConfigOtherTabService.checkTab(tabId, propertyId);

            // 检查当前分类下的指标存在
            budgetDatabaseConfigOtherTabItemService.checkExist(propertyId, tabId, OtherClassificationEnum.REVENUE.getCode(),
                    updateRevenueDTOS.stream().map(UpdateOneYearRevenueDTO::getItemId).collect(Collectors.toList()));

            // Draft 状态下可以修改 Budget 数据，Completed 状态下 Budget 数据不可修改
            budgetDatabaseOtherRevenueService.checkCanUpdateBudgetForStatus(tab, updateRevenueDTOS);

            // Reopen 后的数据不可以修改历史日期的数据（检查是否有 reopen 记录，若有，则不能修改历史月的 预测数据）
            budgetDatabaseOtherRevenueService.checkCanUpdateForReopen(tab, updateRevenueDTOS);

            // 自动参与计算的，不能修改预算数据
            budgetDatabaseOtherRevenueService.checkCanUpdateForParticipateCalculate(tab, updateRevenueDTOS);

            budgetDatabaseOtherRevenueService.updateOther(tab, updateRevenueDTOS, sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1552 - 获取状态修改记录
         *
         * @param year     年
         * @param property 资产标识
         * @param query    查询条件
         * @return 状态修改记录
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1552)
        @GetMapping("/{year:\\d+}/status-log")
        public ListPagedDTO<StatusLogDTO> listStatusLog(@PathVariable int year, String property, StatusLogQuery query) {
            return budgetDatabaseStatusLogService.listStatusLog(sessionContext.getPropertyId(property), year, query);
        }

        /**
         * 1553 - complete / reopen
         *
         * @param year            年
         * @param property        资产标识
         * @param updateStatusDTO 修改状态
         * @return {}
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1553)
        @PutMapping("/{year:\\d+}/status")
        public Map<String, Object> updateStatus(@PathVariable int year, String property, @Valid @RequestBody UpdateBudgetDatabaseStatusDTO updateStatusDTO) {
            int propertyId = sessionContext.getPropertyId(property);

            // 历史年份的数据不可以 Reopen
            if (Objects.equals(updateStatusDTO.getNewStatus(), BudgetDatabaseStatusTypeEnum.REOPEN.getCode())) {
                String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
                if (year < DateTimeUtils.dateNow(timezone).getYear()) {
                    throw new BadRequestException(BudgetDatabaseErrorCons.HISTORY_YEAR_NOT_SUPPORTED_REOPEN,
                            BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "can not reopen history year");
                }
            }

            budgetDatabaseService.updateStatus(propertyId, year, updateStatusDTO.getNewStatus(), sessionContext.getLoginUserId());

            return Collections.emptyMap();
        }

        /**
         * 1586 - 查询可 copy to 的资产
         *
         * @param year 年
         * @return 资产信息
         * @author yyt
         * @date 2025/9/6
         **/
        @ApiId(1586)
        @GetMapping("/{year:\\d+}/copy/properties")
        public List<PropertyCopyToDTO> listCopyToProperty(@PathVariable int year) {
            return budgetDatabaseService.listCopyToProperty(year, sessionContext.getPropertyIds());
        }

        /**
         * 1563 - Copy To
         *
         * @param year      年
         * @param property  资产标识
         * @param copyToDTO copy to
         * @return hasCompleteProperty 是否有已完成的资产
         * @author yyt
         * @date 2025/8/15
         **/
        @ApiId(1563)
        @PostMapping("/{year:\\d+}/copy")
        public Map<String, Object> copyTO(@PathVariable int year, String property, @Valid @RequestBody CopyToDTO copyToDTO) {
            int propertyId = sessionContext.getPropertyId(property);

            // 没有数据权限
            if (copyToDTO.getPropertyIds().stream().anyMatch(item -> !sessionContext.getPropertyIds().contains(item))) {
                throw new ForbiddenException(BaseErrorCodeEnum.DATA_FORBIDDEN.getI18nCode(),
                        BaseErrorCodeEnum.DATA_FORBIDDEN.getCode(), "No data permission");
            }

            // 不能选历史年
            String timezone = propertyI18nRedisService.get(propertyId).getTimezone();
            if (Integer.parseInt(copyToDTO.getTargetYear()) < DateTimeUtils.dateNow(timezone).getYear()) {
                throw new BadRequestException(BudgetDatabaseErrorCons.HISTORY_YEAR_NOT_SUPPORTED_CHOOSE,
                        BaseErrorCodeEnum.DATA_VALIDATION_ERROR.getCode(), "can not choose history year");
            }

            // 已完成的的资产，不支持操作
            if (budgetDatabaseService.checkComplete(copyToDTO.getPropertyIds(), Integer.parseInt(copyToDTO.getTargetYear()))) {
                return ImmutableMap.of("hasCompleteProperty", true);
            }

            budgetDatabaseService.copyTo(propertyId, year, copyToDTO);

            return ImmutableMap.of("hasCompleteProperty", false);
        }

        /**
         * 1592 - 导出
         *
         * @param year     年
         * @param property 资产标识
         * @param response 响应信息
         * @author yyt
         * @date 2025/9/11
         **/
        @ApiId(1592)
        @GetMapping("/{year:\\d+}/export")
        public void export(@PathVariable int year, String property, HttpServletResponse response) {
            PropertyDTO propertyDTO = propertyI18nRedisService.get(sessionContext.getPropertyId(property));
            budgetDatabaseExportService.export(propertyDTO, year, response);
        }

    }