# GeneratePLReportTask.java

    package cloud.wconnect.finance.businesstask.pl;

    import cloud.wconnect.commonlib.cons.BaseConstant;
    import cloud.wconnect.commonlib.cons.FunctionIdConstant;
    import cloud.wconnect.commonlib.exception.BadRequestException;
    import cloud.wconnect.commonlib.pojo.dto.PropertyDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.PropertyM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.dto.m3Config.TenantM3ConfigDTO;
    import cloud.wconnect.commonlib.pojo.query.OpenFunctionPropertyQuery;
    import cloud.wconnect.commonlib.redis.PropertyI18nRedisService;
    import cloud.wconnect.commonlib.service.M3ConfigInternalService;
    import cloud.wconnect.commonlib.service.MailInternalService;
    import cloud.wconnect.commonlib.service.PropertyInternalService;
    import cloud.wconnect.commonlib.util.DateTimeUtils;
    import cloud.wconnect.commonlib.util.YearMonthUtils;
    import cloud.wconnect.finance.pojo.dto.pl.m3api.CustomerCloseAccountDTO;
    import cloud.wconnect.finance.service.pl.*;
    import com.google.common.collect.Lists;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.collections.CollectionUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;

    import java.time.Year;
    import java.time.YearMonth;
    import java.time.ZoneId;
    import java.util.List;
    import java.util.concurrent.TimeUnit;

    /**
     * 生成P&L报告的定时任务
     *
     * @author lichen
     * @date 2025/9/26
     */
    @Slf4j
    @Component
    public class GeneratePLReportTask {

        @Autowired
        private RedisTemplate<String, Object> redisTemplate;
        @Value("${default.timezone}")
        private String defaultTimezone;
        @Autowired
        private MailInternalService mailInternalService;
        @Autowired
        private PropertyInternalService propertyInternalService;
        @Autowired
        private M3ConfigInternalService m3ConfigInternalService;
        @Autowired
        private IM3PlReportService m3PlReportService;
        @Autowired
        private PropertyI18nRedisService propertyI18nRedisService;
        @Autowired
        private M3ApiService m3ApiService;
        @Autowired
        private IM3PlReportLogService m3PlReportLogService;
        @Autowired
        private M3DataService m3DataService;
        //并发前缀
        private static final String PREFIX_KEY = "wconnect:task:pl:generate-pl-report:";

        /**
         * 每日凌晨6点开始生成P&L报告
         *
         * @return void
         * @author lichen
         * @date 2025/9/26
         */
        @Scheduled(cron = "${schedule.cron.pl.generate-pl-report}", zone = "${default.timezone}")
        public void generatePLReport() {
            try {
                String currentDateStr = DateTimeUtils.dateNowStr(defaultTimezone, BaseConstant.DATE_FORMAT_YYMMDDHH);
                if (!preventConcurrency(PREFIX_KEY + currentDateStr)) {
                    return;
                }
                //查询开通P&L功能的有效资产
                List<PropertyDTO> openFunctionPropertyDTOList = propertyInternalService.getOpenFunctionProperty(OpenFunctionPropertyQuery.builder()
                                .openFunctionIds(Lists.newArrayList(FunctionIdConstant.PL)).build())
                        .stream().filter(PropertyDTO::getStatus).toList();
                if (CollectionUtils.isEmpty(openFunctionPropertyDTOList)) {
                    log.info("GeneratePLReportTask, no open function property");
                    return;
                }
                List<Integer> propertyIds = openFunctionPropertyDTOList.stream().map(PropertyDTO::getId).toList();

                //获取资产的M3配置
                List<TenantM3ConfigDTO> tenantM3ConfigDTOS = m3ConfigInternalService.listPropertyM3Config(propertyIds);
                if (CollectionUtils.isEmpty(tenantM3ConfigDTOS)) {
                    log.info("GeneratePLReportTask, no m3 config");
                    return;
                }

                //获取当前年yyyy
                int currentYear = Year.now().getValue();

                for (TenantM3ConfigDTO tenantM3ConfigDTO : tenantM3ConfigDTOS) {
                    List<PropertyM3ConfigDTO> propertyM3ConfigDTOS = tenantM3ConfigDTO.getPropertyM3ConfigDTOS();
                    if (CollectionUtils.isEmpty(propertyM3ConfigDTOS)) {
                        log.info("GeneratePLReportTask, no property m3 config, tenantId:" + tenantM3ConfigDTO.getTenantId());
                        continue;
                    }

                    //客户的关账信息
                    CustomerCloseAccountDTO customerCloseAccountDTO = m3ApiService.getCustomerCloseAccount(tenantM3ConfigDTO, Integer.toString(currentYear));

                    for (PropertyM3ConfigDTO propertyM3ConfigDTO : propertyM3ConfigDTOS) {
                        //生成资产的P&L报告
                        doGeneratePLReport(tenantM3ConfigDTO, propertyM3ConfigDTO, customerCloseAccountDTO);
                    }
                }
            } catch (Exception e) {
                String alertTitle = "[Warning] - P&L Exceptions about generate PL Report";
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle,
                        "<p>There is something wrong when generate PL Report.</p>"
                                + "<p>Exception:" + e + "</p>"
                );
            }
        }

        /**
         * 生成P&L报告
         *
         * @param tenantM3ConfigDTO    租户M3配置
         * @param propertyM3ConfigDTO  资产M3配置
         * @param customerCloseAccount 客户的关账信息
         * @return void
         * @author lichen
         * @date 2025/9/26
         */
        private void doGeneratePLReport(TenantM3ConfigDTO tenantM3ConfigDTO, PropertyM3ConfigDTO propertyM3ConfigDTO, CustomerCloseAccountDTO customerCloseAccount) {
            try {
                PropertyDTO propertyDTO = propertyI18nRedisService.get(propertyM3ConfigDTO.getPropertyId());
                ZoneId zoneId = ZoneId.of(propertyDTO.getTimezone());

                //获取上个月的时间，格式yyyyMM
                YearMonth lastMonth = YearMonth.now(zoneId).minusMonths(1);
                Integer lastMonthInteger = Integer.valueOf(YearMonthUtils.DATE_TIME_FORMATTER.format(lastMonth));

                //判断客户是否关账
                boolean closed = m3DataService.isClosed(customerCloseAccount, propertyM3ConfigDTO, lastMonthInteger);
                if(!closed){
                    return;
                }
                //校验是否可以生成报告
                m3PlReportService.checkCanGeneratePLReport(propertyDTO.getId(), lastMonthInteger, 0);

                //插入处理日志
                Integer logId = m3PlReportLogService.insertProcessingLog(propertyDTO.getTenantId(), propertyDTO.getId(), lastMonthInteger, 0);

                //调用M3接口获取基础数据
                m3DataService.handleM3BasicData(tenantM3ConfigDTO, propertyM3ConfigDTO, lastMonthInteger, 0, logId);

                //生成P&L报告
                m3PlReportService.generatePLReport(propertyM3ConfigDTO.getPropertyId(), lastMonthInteger, 0, logId);
            } catch (BadRequestException exception) {
                log.warn("GeneratePLReportTask.doGeneratePLReport check error, propertyId:" + propertyM3ConfigDTO.getPropertyId(), exception);
            } catch (Exception e) {
                String alertTitle = "[Warning] - P&L Exceptions about generate PL Report of propertyId:" + propertyM3ConfigDTO.getPropertyId();
                //发送预警邮件
                mailInternalService.sendAlertEmail(alertTitle,
                        "<p>There is something wrong when generate PL Report of propertyId: " + propertyM3ConfigDTO.getPropertyId() + ".</p>"
                                + "<p>Exception:" + e + "</p>"
                );
            }
        }

        /**
         * 阻止并发操作
         *
         * @param key 缓存key
         * @return java.lang.Boolean
         * @author lichen
         * @date 2023/10/16
         */
        private Boolean preventConcurrency(String key) {
            return redisTemplate.opsForValue().setIfAbsent(key, 0, 1, TimeUnit.DAYS);
        }
    }