Răsfoiți Sursa

[1015] 经会议讨论。先取消batch逻辑,注释任务

taosj 1 săptămână în urmă
părinte
comite
c190f22e35

+ 499 - 499
forecast-backend-server/src/main/java/com/ufo/project/forecast/fileupload/task/DataPowerForecastReportBatchTask.java

@@ -1,499 +1,499 @@
-package com.ufo.project.forecast.fileupload.task;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.time.Instant;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.ZoneId;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
-
-import com.alibaba.fastjson2.JSON;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import com.ufo.base.common.utils.StringUtils;
-import com.ufo.module.system.domain.SysDictData;
-import com.ufo.module.system.service.ISysDictTypeService;
-import com.ufo.project.config.domain.ConfigStation;
-import com.ufo.project.config.service.IConfigStationService;
-import com.ufo.project.forecast.fileupload.domain.DataPowerForecastReportBatch;
-import com.ufo.project.forecast.fileupload.service.IDataPowerForecastReportBatchService;
-import com.ufo.project.forecast.processor.ForecastProcessorFactory;
-import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastReport;
-import com.ufo.project.forecast.shortterm.power.mapper.DataShortTermPowerForecastReportMapper;
-import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastReport;
-import com.ufo.project.forecast.supershort.mapper.DataSuperShortTermForecastReportMapper;
-
-/**
- * 功率预测上报 - 批次生成定时任务
- *
- * 读取字典 power_forecast_report_batch_config 中的场站配置(JSON数组),
- * 在每个预计上报时间前 initInterval 分钟窗口内生成上报批次行, 并触发对应处理器初始化上报数据.
- *
- * 配置示例:
- * <pre>
- * [
- *   { "stationCode": "3301", "type": "short", "period": "daily", "times": ["08:00:00"], "initInterval": 60 }
- * ]
- * </pre>
- * initInterval 单位为分钟, 缺省 0.
- *
- * @author ufo
- * @date 2026-10-19
- */
-@Component
-public class DataPowerForecastReportBatchTask
-{
-    private static final Logger log = LoggerFactory.getLogger(DataPowerForecastReportBatchTask.class);
-
-    /** 上报批次配置字典类型 */
-    public static final String DICT_TYPE = "power_forecast_report_batch_config";
-
-    /** 扫描间隔(毫秒), 每轮结束后间隔5分钟 */
-    private static final long SCAN_INTERVAL_MILLIS = 300_000L;
-
-    /** 北京时间时区 */
-    private static final ZoneId ZONE_SH = ZoneId.of("Asia/Shanghai");
-
-    private static final DateTimeFormatter FMT_REPORT_DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
-
-    private static final DateTimeFormatter FMT_BATCH_NO = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
-
-    /** 数据行时间格式(北京时间) */
-    private static final DateTimeFormatter FMT_POINT_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE_SH);
-
-    /** 日期格式(yyyy-MM-dd) */
-    private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
-
-    @Autowired
-    private ISysDictTypeService dictTypeService;
-
-    @Autowired
-    private IConfigStationService configStationService;
-
-    @Autowired
-    private IDataPowerForecastReportBatchService reportBatchService;
-
-    @Autowired
-    private ForecastProcessorFactory factory;
-
-    @Autowired
-    private DataShortTermPowerForecastReportMapper shortTermReportMapper;
-
-    @Autowired
-    private DataSuperShortTermForecastReportMapper superShortTermReportMapper;
-
-    @Value("${project.forecast.reportfile.rootfolder}")
-    private String reportFileRootFolder;
-
-    /**
-     * 扫描字典配置, 为窗口内的预计上报时点生成批次行并触发数据初始化
-     */
-    @Scheduled(fixedDelay = SCAN_INTERVAL_MILLIS)
-    public void autoGenerateBatchNo()
-    {
-        List<Config> configs = loadConfigs();
-        if (configs.isEmpty())
-        {
-            return;
-        }
-        for (Config config : configs)
-        {
-            try
-            {
-                process(config);
-            }
-            catch (Exception e)
-            {
-                log.error("[PowerForecastReportBatch] 处理配置项失败, stationCode={}, type={}",
-                        config == null ? null : config.getStationCode(), config == null ? null : config.getType(), e);
-            }
-        }
-    }
-
-    /**
-     * 状态推进: 2已确认 -> 4文件上传中 (每分钟第23秒)
-     */
-    // @Scheduled(cron = "23 * * * * *")
-    public void startingFileGenerating()
-    {
-        DataPowerForecastReportBatch q = new DataPowerForecastReportBatch();
-        q.setStatus(DataPowerForecastReportBatch.STATUS_CONFIRMED);
-        List<DataPowerForecastReportBatch> batches = reportBatchService.selectDataPowerForecastReportBatchList(q);
-        if (batches == null || batches.isEmpty())
-        {
-            return;
-        }
-        for (DataPowerForecastReportBatch b : batches)
-        {
-            try
-            {
-                DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
-                u.setId(b.getId());
-                u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOADING);
-                reportBatchService.updateDataPowerForecastReportBatch(u);
-                log.info("[PowerForecastReportBatch] 状态 2->4, batchNo={}", b.getBatchNo());
-            }
-            catch (Exception e)
-            {
-                log.error("[PowerForecastReportBatch] 状态 2->4 失败, batchNo={}", b.getBatchNo(), e);
-            }
-        }
-    }
-
-    /**
-     * 生成上报文件: 4文件上传中 -> 5文件上传成功 / 9文件上传失败 (每分钟第45秒)
-     */
-    // @Scheduled(cron = "45 * * * * *")
-    public void generateReportFile()
-    {
-        DataPowerForecastReportBatch q = new DataPowerForecastReportBatch();
-        q.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOADING);
-        List<DataPowerForecastReportBatch> batches = reportBatchService.selectDataPowerForecastReportBatchList(q);
-        if (batches == null || batches.isEmpty())
-        {
-            return;
-        }
-        for (DataPowerForecastReportBatch b : batches)
-        {
-            try
-            {
-                File file = buildReportFile(b);
-                DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
-                u.setId(b.getId());
-                u.setFile(file.getAbsolutePath());
-                u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOAD_SUCCESS);
-                reportBatchService.updateDataPowerForecastReportBatch(u);
-                log.info("[PowerForecastReportBatch] 上报文件生成成功, batchNo={}, file={}",
-                        b.getBatchNo(), file.getAbsolutePath());
-            }
-            catch (Exception e)
-            {
-                log.error("[PowerForecastReportBatch] 上报文件生成失败, batchNo={}", b.getBatchNo(), e);
-                DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
-                u.setId(b.getId());
-                u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOAD_FAILED);
-                try
-                {
-                    reportBatchService.updateDataPowerForecastReportBatch(u);
-                }
-                catch (Exception ex)
-                {
-                    log.error("[PowerForecastReportBatch] 状态置9失败, batchNo={}", b.getBatchNo(), ex);
-                }
-            }
-        }
-    }
-
-    /**
-     * 生成 .dat 上报文件并返回文件对象(异常向上抛, 由调用方置状态9)
-     */
-    private File buildReportFile(DataPowerForecastReportBatch batch) throws IOException
-    {
-        List<ReportPoint> points = loadReportPoints(batch);
-        if (points.isEmpty())
-        {
-            throw new IllegalStateException("批次无上报数据, batchNo=" + batch.getBatchNo());
-        }
-        ConfigStation sq = new ConfigStation();
-        sq.setStationCode(batch.getStationCode());
-        List<ConfigStation> stations = configStationService.selectConfigStationList(sq);
-        if (stations == null || stations.isEmpty())
-        {
-            throw new IllegalStateException("未找到场站, stationCode=" + batch.getStationCode());
-        }
-        Long capacity = stations.get(0).getInstalledCapacity();
-        if (capacity == null)
-        {
-            throw new IllegalStateException("场站装机容量为空, stationCode=" + batch.getStationCode());
-        }
-        String marker = DataPowerForecastReportBatch.TYPE_ULTRA.equals(batch.getType()) ? "CDQYC" : "DQYC";
-        String fileName = "GF_" + batch.getStationName() + "_" + marker + "_" + batch.getReportDay()
-                + "_" + batch.getEstReportTime().replace(":", "") + ".dat";
-        int reportDay = batch.getReportDay();
-        String year = String.valueOf(reportDay / 10000);
-        String month = String.format(Locale.ROOT, "%02d", (reportDay / 100) % 100);
-        File dir = new File(reportFileRootFolder,
-                batch.getType() + File.separator + batch.getStationCode()
-                        + File.separator + year + File.separator + month);
-        if (!dir.exists() && !dir.mkdirs())
-        {
-            throw new IOException("创建目录失败: " + dir.getAbsolutePath());
-        }
-        File target = new File(dir, fileName);
-        StringBuilder sb = new StringBuilder();
-        sb.append("<! System=PowerForecast, Version=1.0, Code=UTF-8, Data=").append(batch.getReportDay()).append(" !>\n");
-        sb.append('<').append(marker).append("::").append(batch.getStationName()).append(">\n");
-        sb.append("Date=").append(LocalDate.parse(String.valueOf(batch.getReportDay()), FMT_REPORT_DAY).format(FMT_DATE))
-                .append(", Time=").append(batch.getEstReportTime().replace(":", "-")).append('\n');
-        sb.append("# 点号\t时间\t预测功率(MW)\t预计开机容量(MW)\t备注\n");
-        String capacityMw = String.format(Locale.ROOT, "%.2f", capacity / 1000.0);
-        for (int i = 0; i < points.size(); i++)
-        {
-            ReportPoint p = points.get(i);
-            sb.append(i + 1).append('\t')
-                    .append(FMT_POINT_TIME.format(Instant.ofEpochSecond(p.forecastTime))).append('\t')
-                    .append(String.format(Locale.ROOT, "%.2f", p.power / 1000.0)).append('\t')
-                    .append(capacityMw).append('\t')
-                    .append("正常").append('\n');
-        }
-        sb.append("</").append(marker).append("::").append(batch.getStationName()).append(">\n");
-        Files.write(target.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
-        return target;
-    }
-
-    /**
-     * 按批次号加载上报数据点, 按预测时刻升序(power 为 kW 原始值)
-     */
-    private List<ReportPoint> loadReportPoints(DataPowerForecastReportBatch batch)
-    {
-        List<ReportPoint> points = new ArrayList<>();
-        if (DataPowerForecastReportBatch.TYPE_ULTRA.equals(batch.getType()))
-        {
-            DataSuperShortTermForecastReport q = new DataSuperShortTermForecastReport();
-            q.setBatchNo(batch.getBatchNo());
-            for (DataSuperShortTermForecastReport r : superShortTermReportMapper.selectDataSuperShortTermForecastReportList(q))
-            {
-                if (r.getForecastTime() != null && StringUtils.isNotBlank(r.getPower()))
-                {
-                    points.add(new ReportPoint(r.getForecastTime(), Double.parseDouble(r.getPower())));
-                }
-            }
-        }
-        else
-        {
-            DataShortTermPowerForecastReport q = new DataShortTermPowerForecastReport();
-            q.setBatchNo(batch.getBatchNo());
-            for (DataShortTermPowerForecastReport r : shortTermReportMapper.selectDataShortTermPowerForecastReportList(q))
-            {
-                if (r.getForecastTime() != null && StringUtils.isNotBlank(r.getPower()))
-                {
-                    points.add(new ReportPoint(r.getForecastTime(), Double.parseDouble(r.getPower())));
-                }
-            }
-        }
-        points.sort(Comparator.comparingLong(p -> p.forecastTime));
-        return points;
-    }
-
-    /**
-     * 上报数据点: 预测时刻(UTC秒) + 功率(kW)
-     */
-    private static class ReportPoint
-    {
-        private final long forecastTime;
-
-        private final double power;
-
-        ReportPoint(long forecastTime, double power)
-        {
-            this.forecastTime = forecastTime;
-            this.power = power;
-        }
-    }
-
-    /**
-     * 读取字典配置, 解析为场站配置列表
-     */
-    private List<Config> loadConfigs()
-    {
-        try
-        {
-            List<SysDictData> dictList = dictTypeService.selectDictDataByType(DICT_TYPE);
-            if (dictList == null || dictList.isEmpty())
-            {
-                return Collections.emptyList();
-            }
-            String dictValue = dictList.get(0).getDictValue();
-            if (StringUtils.isBlank(dictValue))
-            {
-                return Collections.emptyList();
-            }
-            List<Config> configs = JSON.parseArray(dictValue, Config.class);
-            return configs == null ? Collections.<Config>emptyList() : configs;
-        }
-        catch (Exception e)
-        {
-            log.error("[PowerForecastReportBatch] 解析字典 {} 配置失败", DICT_TYPE, e);
-            return Collections.emptyList();
-        }
-    }
-
-    /**
-     * 处理单个场站配置: 遍历预计上报时点(升序), 在提前窗口内生成批次行并触发初始化
-     */
-    private void process(Config config)
-    {
-        if (config == null || StringUtils.isBlank(config.getStationCode()))
-        {
-            return;
-        }
-        if (!"daily".equals(config.getPeriod()))
-        {
-            log.warn("[PowerForecastReportBatch] period 非 daily, 跳过, stationCode={}", config.getStationCode());
-            return;
-        }
-        if (!DataPowerForecastReportBatch.TYPE_SHORT.equals(config.getType())
-                && !DataPowerForecastReportBatch.TYPE_ULTRA.equals(config.getType()))
-        {
-            log.warn("[PowerForecastReportBatch] type 未知, 跳过, stationCode={}, type={}",
-                    config.getStationCode(), config.getType());
-            return;
-        }
-        ConfigStation q = new ConfigStation();
-        q.setStationCode(config.getStationCode());
-        List<ConfigStation> stations = configStationService.selectConfigStationList(q);
-        if (stations == null || stations.isEmpty())
-        {
-            log.warn("[PowerForecastReportBatch] 未找到场站, 跳过, stationCode={}", config.getStationCode());
-            return;
-        }
-        String stationName = stations.get(0).getStationName();
-        List<String> times = config.getTimes();
-        if (times == null || times.isEmpty())
-        {
-            log.warn("[PowerForecastReportBatch] times 为空, 跳过, stationCode={}", config.getStationCode());
-            return;
-        }
-        times.sort(Comparator.naturalOrder());
-        for (String t : times)
-        {
-            LocalTime slotTime;
-            try
-            {
-                slotTime = LocalTime.parse(t);
-            }
-            catch (Exception e)
-            {
-                log.warn("[PowerForecastReportBatch] 时点格式非法, 跳过, stationCode={}, time={}",
-                        config.getStationCode(), t);
-                continue;
-            }
-            LocalDateTime now = LocalDateTime.now(ZONE_SH);
-            LocalDate today = LocalDate.now(ZONE_SH);
-            LocalDateTime slot = today.atTime(slotTime);
-            // 已过时点, 跳过后续更远的时点
-            if (now.isAfter(slot))
-            {
-                continue;
-            }
-            // 未到提前窗口, 后续时点更远, 直接结束
-            if (now.isBefore(slot.minusMinutes(config.getInitInterval() == null ? 0 : config.getInitInterval())))
-            {
-                break;
-            }
-            int reportDay = Integer.parseInt(today.format(FMT_REPORT_DAY));
-            boolean exists = reportBatchService.selectByTypeDayTime(config.getType(), reportDay, t)
-                    .stream().anyMatch(b -> config.getStationCode().equals(b.getStationCode()));
-            if (exists)
-            {
-                continue;
-            }
-            DataPowerForecastReportBatch b = new DataPowerForecastReportBatch();
-            b.setStationCode(config.getStationCode());
-            b.setStationName(stationName);
-            b.setType(config.getType());
-            b.setReportDay(reportDay);
-            b.setEstReportTime(t);
-            b.setStatus(DataPowerForecastReportBatch.STATUS_PENDING);
-            b.setGenerateSource("auto");
-            b.setBatchNo(prefix(config.getType()) + slot.format(FMT_BATCH_NO) + "_" + config.getStationCode());
-            reportBatchService.insertDataPowerForecastReportBatch(b);
-            log.info("[PowerForecastReportBatch] 新生成上报批次, batchNo={}, stationCode={}, time={}",
-                    b.getBatchNo(), config.getStationCode(), t);
-            factory.getInstant(config.getType()).initData(reportDay, t);
-        }
-    }
-
-    /**
-     * 批次号前缀: 超短期 ULT, 其余 ST
-     */
-    private static String prefix(String type)
-    {
-        return "ultra".equals(type) ? "ULT" : "ST";
-    }
-
-    /**
-     * 字典 power_forecast_report_batch_config 的单个场站配置
-     */
-    public static class Config
-    {
-        /** 场站编码 */
-        private String stationCode;
-
-        /** 类型(short短期,ultra超短期) */
-        private String type;
-
-        /** 周期(daily) */
-        private String period;
-
-        /** 预计上报时点列表(HH:mm:ss) */
-        private List<String> times;
-
-        /** 提前初始化窗口(分钟), 缺省 0 */
-        private Integer initInterval;
-
-        public String getStationCode()
-        {
-            return stationCode;
-        }
-
-        public void setStationCode(String stationCode)
-        {
-            this.stationCode = stationCode;
-        }
-
-        public String getType()
-        {
-            return type;
-        }
-
-        public void setType(String type)
-        {
-            this.type = type;
-        }
-
-        public String getPeriod()
-        {
-            return period;
-        }
-
-        public void setPeriod(String period)
-        {
-            this.period = period;
-        }
-
-        public List<String> getTimes()
-        {
-            return times;
-        }
-
-        public void setTimes(List<String> times)
-        {
-            this.times = times;
-        }
-
-        public Integer getInitInterval()
-        {
-            return initInterval;
-        }
-
-        public void setInitInterval(Integer initInterval)
-        {
-            this.initInterval = initInterval;
-        }
-    }
-}
+// package com.ufo.project.forecast.fileupload.task;
+
+// import java.io.File;
+// import java.io.IOException;
+// import java.nio.charset.StandardCharsets;
+// import java.nio.file.Files;
+// import java.time.Instant;
+// import java.time.LocalDate;
+// import java.time.LocalDateTime;
+// import java.time.LocalTime;
+// import java.time.ZoneId;
+// import java.time.format.DateTimeFormatter;
+// import java.util.ArrayList;
+// import java.util.Collections;
+// import java.util.Comparator;
+// import java.util.List;
+// import java.util.Locale;
+
+// import com.alibaba.fastjson2.JSON;
+// import org.slf4j.Logger;
+// import org.slf4j.LoggerFactory;
+// import org.springframework.beans.factory.annotation.Autowired;
+// import org.springframework.beans.factory.annotation.Value;
+// import org.springframework.scheduling.annotation.Scheduled;
+// import org.springframework.stereotype.Component;
+
+// import com.ufo.base.common.utils.StringUtils;
+// import com.ufo.module.system.domain.SysDictData;
+// import com.ufo.module.system.service.ISysDictTypeService;
+// import com.ufo.project.config.domain.ConfigStation;
+// import com.ufo.project.config.service.IConfigStationService;
+// import com.ufo.project.forecast.fileupload.domain.DataPowerForecastReportBatch;
+// import com.ufo.project.forecast.fileupload.service.IDataPowerForecastReportBatchService;
+// import com.ufo.project.forecast.processor.ForecastProcessorFactory;
+// import com.ufo.project.forecast.shortterm.power.domain.DataShortTermPowerForecastReport;
+// import com.ufo.project.forecast.shortterm.power.mapper.DataShortTermPowerForecastReportMapper;
+// import com.ufo.project.forecast.supershort.domain.DataSuperShortTermForecastReport;
+// import com.ufo.project.forecast.supershort.mapper.DataSuperShortTermForecastReportMapper;
+
+// /**
+//  * 功率预测上报 - 批次生成定时任务
+//  *
+//  * 读取字典 power_forecast_report_batch_config 中的场站配置(JSON数组),
+//  * 在每个预计上报时间前 initInterval 分钟窗口内生成上报批次行, 并触发对应处理器初始化上报数据.
+//  *
+//  * 配置示例:
+//  * <pre>
+//  * [
+//  *   { "stationCode": "3301", "type": "short", "period": "daily", "times": ["08:00:00"], "initInterval": 60 }
+//  * ]
+//  * </pre>
+//  * initInterval 单位为分钟, 缺省 0.
+//  *
+//  * @author ufo
+//  * @date 2026-10-19
+//  */
+// @Component
+// public class DataPowerForecastReportBatchTask
+// {
+//     private static final Logger log = LoggerFactory.getLogger(DataPowerForecastReportBatchTask.class);
+
+//     /** 上报批次配置字典类型 */
+//     public static final String DICT_TYPE = "power_forecast_report_batch_config";
+
+//     /** 扫描间隔(毫秒), 每轮结束后间隔5分钟 */
+//     private static final long SCAN_INTERVAL_MILLIS = 300_000L;
+
+//     /** 北京时间时区 */
+//     private static final ZoneId ZONE_SH = ZoneId.of("Asia/Shanghai");
+
+//     private static final DateTimeFormatter FMT_REPORT_DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+//     private static final DateTimeFormatter FMT_BATCH_NO = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
+
+//     /** 数据行时间格式(北京时间) */
+//     private static final DateTimeFormatter FMT_POINT_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZONE_SH);
+
+//     /** 日期格式(yyyy-MM-dd) */
+//     private static final DateTimeFormatter FMT_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+
+//     @Autowired
+//     private ISysDictTypeService dictTypeService;
+
+//     @Autowired
+//     private IConfigStationService configStationService;
+
+//     @Autowired
+//     private IDataPowerForecastReportBatchService reportBatchService;
+
+//     @Autowired
+//     private ForecastProcessorFactory factory;
+
+//     @Autowired
+//     private DataShortTermPowerForecastReportMapper shortTermReportMapper;
+
+//     @Autowired
+//     private DataSuperShortTermForecastReportMapper superShortTermReportMapper;
+
+//     @Value("${project.forecast.reportfile.rootfolder}")
+//     private String reportFileRootFolder;
+
+//     /**
+//      * 扫描字典配置, 为窗口内的预计上报时点生成批次行并触发数据初始化
+//      */
+//     @Scheduled(fixedDelay = SCAN_INTERVAL_MILLIS)
+//     public void autoGenerateBatchNo()
+//     {
+//         List<Config> configs = loadConfigs();
+//         if (configs.isEmpty())
+//         {
+//             return;
+//         }
+//         for (Config config : configs)
+//         {
+//             try
+//             {
+//                 process(config);
+//             }
+//             catch (Exception e)
+//             {
+//                 log.error("[PowerForecastReportBatch] 处理配置项失败, stationCode={}, type={}",
+//                         config == null ? null : config.getStationCode(), config == null ? null : config.getType(), e);
+//             }
+//         }
+//     }
+
+//     /**
+//      * 状态推进: 2已确认 -> 4文件上传中 (每分钟第23秒)
+//      */
+//     // @Scheduled(cron = "23 * * * * *")
+//     public void startingFileGenerating()
+//     {
+//         DataPowerForecastReportBatch q = new DataPowerForecastReportBatch();
+//         q.setStatus(DataPowerForecastReportBatch.STATUS_CONFIRMED);
+//         List<DataPowerForecastReportBatch> batches = reportBatchService.selectDataPowerForecastReportBatchList(q);
+//         if (batches == null || batches.isEmpty())
+//         {
+//             return;
+//         }
+//         for (DataPowerForecastReportBatch b : batches)
+//         {
+//             try
+//             {
+//                 DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
+//                 u.setId(b.getId());
+//                 u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOADING);
+//                 reportBatchService.updateDataPowerForecastReportBatch(u);
+//                 log.info("[PowerForecastReportBatch] 状态 2->4, batchNo={}", b.getBatchNo());
+//             }
+//             catch (Exception e)
+//             {
+//                 log.error("[PowerForecastReportBatch] 状态 2->4 失败, batchNo={}", b.getBatchNo(), e);
+//             }
+//         }
+//     }
+
+//     /**
+//      * 生成上报文件: 4文件上传中 -> 5文件上传成功 / 9文件上传失败 (每分钟第45秒)
+//      */
+//     // @Scheduled(cron = "45 * * * * *")
+//     public void generateReportFile()
+//     {
+//         DataPowerForecastReportBatch q = new DataPowerForecastReportBatch();
+//         q.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOADING);
+//         List<DataPowerForecastReportBatch> batches = reportBatchService.selectDataPowerForecastReportBatchList(q);
+//         if (batches == null || batches.isEmpty())
+//         {
+//             return;
+//         }
+//         for (DataPowerForecastReportBatch b : batches)
+//         {
+//             try
+//             {
+//                 File file = buildReportFile(b);
+//                 DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
+//                 u.setId(b.getId());
+//                 u.setFile(file.getAbsolutePath());
+//                 u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOAD_SUCCESS);
+//                 reportBatchService.updateDataPowerForecastReportBatch(u);
+//                 log.info("[PowerForecastReportBatch] 上报文件生成成功, batchNo={}, file={}",
+//                         b.getBatchNo(), file.getAbsolutePath());
+//             }
+//             catch (Exception e)
+//             {
+//                 log.error("[PowerForecastReportBatch] 上报文件生成失败, batchNo={}", b.getBatchNo(), e);
+//                 DataPowerForecastReportBatch u = new DataPowerForecastReportBatch();
+//                 u.setId(b.getId());
+//                 u.setStatus(DataPowerForecastReportBatch.STATUS_FILE_UPLOAD_FAILED);
+//                 try
+//                 {
+//                     reportBatchService.updateDataPowerForecastReportBatch(u);
+//                 }
+//                 catch (Exception ex)
+//                 {
+//                     log.error("[PowerForecastReportBatch] 状态置9失败, batchNo={}", b.getBatchNo(), ex);
+//                 }
+//             }
+//         }
+//     }
+
+//     /**
+//      * 生成 .dat 上报文件并返回文件对象(异常向上抛, 由调用方置状态9)
+//      */
+//     private File buildReportFile(DataPowerForecastReportBatch batch) throws IOException
+//     {
+//         List<ReportPoint> points = loadReportPoints(batch);
+//         if (points.isEmpty())
+//         {
+//             throw new IllegalStateException("批次无上报数据, batchNo=" + batch.getBatchNo());
+//         }
+//         ConfigStation sq = new ConfigStation();
+//         sq.setStationCode(batch.getStationCode());
+//         List<ConfigStation> stations = configStationService.selectConfigStationList(sq);
+//         if (stations == null || stations.isEmpty())
+//         {
+//             throw new IllegalStateException("未找到场站, stationCode=" + batch.getStationCode());
+//         }
+//         Long capacity = stations.get(0).getInstalledCapacity();
+//         if (capacity == null)
+//         {
+//             throw new IllegalStateException("场站装机容量为空, stationCode=" + batch.getStationCode());
+//         }
+//         String marker = DataPowerForecastReportBatch.TYPE_ULTRA.equals(batch.getType()) ? "CDQYC" : "DQYC";
+//         String fileName = "GF_" + batch.getStationName() + "_" + marker + "_" + batch.getReportDay()
+//                 + "_" + batch.getEstReportTime().replace(":", "") + ".dat";
+//         int reportDay = batch.getReportDay();
+//         String year = String.valueOf(reportDay / 10000);
+//         String month = String.format(Locale.ROOT, "%02d", (reportDay / 100) % 100);
+//         File dir = new File(reportFileRootFolder,
+//                 batch.getType() + File.separator + batch.getStationCode()
+//                         + File.separator + year + File.separator + month);
+//         if (!dir.exists() && !dir.mkdirs())
+//         {
+//             throw new IOException("创建目录失败: " + dir.getAbsolutePath());
+//         }
+//         File target = new File(dir, fileName);
+//         StringBuilder sb = new StringBuilder();
+//         sb.append("<! System=PowerForecast, Version=1.0, Code=UTF-8, Data=").append(batch.getReportDay()).append(" !>\n");
+//         sb.append('<').append(marker).append("::").append(batch.getStationName()).append(">\n");
+//         sb.append("Date=").append(LocalDate.parse(String.valueOf(batch.getReportDay()), FMT_REPORT_DAY).format(FMT_DATE))
+//                 .append(", Time=").append(batch.getEstReportTime().replace(":", "-")).append('\n');
+//         sb.append("# 点号\t时间\t预测功率(MW)\t预计开机容量(MW)\t备注\n");
+//         String capacityMw = String.format(Locale.ROOT, "%.2f", capacity / 1000.0);
+//         for (int i = 0; i < points.size(); i++)
+//         {
+//             ReportPoint p = points.get(i);
+//             sb.append(i + 1).append('\t')
+//                     .append(FMT_POINT_TIME.format(Instant.ofEpochSecond(p.forecastTime))).append('\t')
+//                     .append(String.format(Locale.ROOT, "%.2f", p.power / 1000.0)).append('\t')
+//                     .append(capacityMw).append('\t')
+//                     .append("正常").append('\n');
+//         }
+//         sb.append("</").append(marker).append("::").append(batch.getStationName()).append(">\n");
+//         Files.write(target.toPath(), sb.toString().getBytes(StandardCharsets.UTF_8));
+//         return target;
+//     }
+
+//     /**
+//      * 按批次号加载上报数据点, 按预测时刻升序(power 为 kW 原始值)
+//      */
+//     private List<ReportPoint> loadReportPoints(DataPowerForecastReportBatch batch)
+//     {
+//         List<ReportPoint> points = new ArrayList<>();
+//         if (DataPowerForecastReportBatch.TYPE_ULTRA.equals(batch.getType()))
+//         {
+//             DataSuperShortTermForecastReport q = new DataSuperShortTermForecastReport();
+//             q.setBatchNo(batch.getBatchNo());
+//             for (DataSuperShortTermForecastReport r : superShortTermReportMapper.selectDataSuperShortTermForecastReportList(q))
+//             {
+//                 if (r.getForecastTime() != null && StringUtils.isNotBlank(r.getPower()))
+//                 {
+//                     points.add(new ReportPoint(r.getForecastTime(), Double.parseDouble(r.getPower())));
+//                 }
+//             }
+//         }
+//         else
+//         {
+//             DataShortTermPowerForecastReport q = new DataShortTermPowerForecastReport();
+//             q.setBatchNo(batch.getBatchNo());
+//             for (DataShortTermPowerForecastReport r : shortTermReportMapper.selectDataShortTermPowerForecastReportList(q))
+//             {
+//                 if (r.getForecastTime() != null && StringUtils.isNotBlank(r.getPower()))
+//                 {
+//                     points.add(new ReportPoint(r.getForecastTime(), Double.parseDouble(r.getPower())));
+//                 }
+//             }
+//         }
+//         points.sort(Comparator.comparingLong(p -> p.forecastTime));
+//         return points;
+//     }
+
+//     /**
+//      * 上报数据点: 预测时刻(UTC秒) + 功率(kW)
+//      */
+//     private static class ReportPoint
+//     {
+//         private final long forecastTime;
+
+//         private final double power;
+
+//         ReportPoint(long forecastTime, double power)
+//         {
+//             this.forecastTime = forecastTime;
+//             this.power = power;
+//         }
+//     }
+
+//     /**
+//      * 读取字典配置, 解析为场站配置列表
+//      */
+//     private List<Config> loadConfigs()
+//     {
+//         try
+//         {
+//             List<SysDictData> dictList = dictTypeService.selectDictDataByType(DICT_TYPE);
+//             if (dictList == null || dictList.isEmpty())
+//             {
+//                 return Collections.emptyList();
+//             }
+//             String dictValue = dictList.get(0).getDictValue();
+//             if (StringUtils.isBlank(dictValue))
+//             {
+//                 return Collections.emptyList();
+//             }
+//             List<Config> configs = JSON.parseArray(dictValue, Config.class);
+//             return configs == null ? Collections.<Config>emptyList() : configs;
+//         }
+//         catch (Exception e)
+//         {
+//             log.error("[PowerForecastReportBatch] 解析字典 {} 配置失败", DICT_TYPE, e);
+//             return Collections.emptyList();
+//         }
+//     }
+
+//     /**
+//      * 处理单个场站配置: 遍历预计上报时点(升序), 在提前窗口内生成批次行并触发初始化
+//      */
+//     private void process(Config config)
+//     {
+//         if (config == null || StringUtils.isBlank(config.getStationCode()))
+//         {
+//             return;
+//         }
+//         if (!"daily".equals(config.getPeriod()))
+//         {
+//             log.warn("[PowerForecastReportBatch] period 非 daily, 跳过, stationCode={}", config.getStationCode());
+//             return;
+//         }
+//         if (!DataPowerForecastReportBatch.TYPE_SHORT.equals(config.getType())
+//                 && !DataPowerForecastReportBatch.TYPE_ULTRA.equals(config.getType()))
+//         {
+//             log.warn("[PowerForecastReportBatch] type 未知, 跳过, stationCode={}, type={}",
+//                     config.getStationCode(), config.getType());
+//             return;
+//         }
+//         ConfigStation q = new ConfigStation();
+//         q.setStationCode(config.getStationCode());
+//         List<ConfigStation> stations = configStationService.selectConfigStationList(q);
+//         if (stations == null || stations.isEmpty())
+//         {
+//             log.warn("[PowerForecastReportBatch] 未找到场站, 跳过, stationCode={}", config.getStationCode());
+//             return;
+//         }
+//         String stationName = stations.get(0).getStationName();
+//         List<String> times = config.getTimes();
+//         if (times == null || times.isEmpty())
+//         {
+//             log.warn("[PowerForecastReportBatch] times 为空, 跳过, stationCode={}", config.getStationCode());
+//             return;
+//         }
+//         times.sort(Comparator.naturalOrder());
+//         for (String t : times)
+//         {
+//             LocalTime slotTime;
+//             try
+//             {
+//                 slotTime = LocalTime.parse(t);
+//             }
+//             catch (Exception e)
+//             {
+//                 log.warn("[PowerForecastReportBatch] 时点格式非法, 跳过, stationCode={}, time={}",
+//                         config.getStationCode(), t);
+//                 continue;
+//             }
+//             LocalDateTime now = LocalDateTime.now(ZONE_SH);
+//             LocalDate today = LocalDate.now(ZONE_SH);
+//             LocalDateTime slot = today.atTime(slotTime);
+//             // 已过时点, 跳过后续更远的时点
+//             if (now.isAfter(slot))
+//             {
+//                 continue;
+//             }
+//             // 未到提前窗口, 后续时点更远, 直接结束
+//             if (now.isBefore(slot.minusMinutes(config.getInitInterval() == null ? 0 : config.getInitInterval())))
+//             {
+//                 break;
+//             }
+//             int reportDay = Integer.parseInt(today.format(FMT_REPORT_DAY));
+//             boolean exists = reportBatchService.selectByTypeDayTime(config.getType(), reportDay, t)
+//                     .stream().anyMatch(b -> config.getStationCode().equals(b.getStationCode()));
+//             if (exists)
+//             {
+//                 continue;
+//             }
+//             DataPowerForecastReportBatch b = new DataPowerForecastReportBatch();
+//             b.setStationCode(config.getStationCode());
+//             b.setStationName(stationName);
+//             b.setType(config.getType());
+//             b.setReportDay(reportDay);
+//             b.setEstReportTime(t);
+//             b.setStatus(DataPowerForecastReportBatch.STATUS_PENDING);
+//             b.setGenerateSource("auto");
+//             b.setBatchNo(prefix(config.getType()) + slot.format(FMT_BATCH_NO) + "_" + config.getStationCode());
+//             reportBatchService.insertDataPowerForecastReportBatch(b);
+//             log.info("[PowerForecastReportBatch] 新生成上报批次, batchNo={}, stationCode={}, time={}",
+//                     b.getBatchNo(), config.getStationCode(), t);
+//             factory.getInstant(config.getType()).initData(reportDay, t);
+//         }
+//     }
+
+//     /**
+//      * 批次号前缀: 超短期 ULT, 其余 ST
+//      */
+//     private static String prefix(String type)
+//     {
+//         return "ultra".equals(type) ? "ULT" : "ST";
+//     }
+
+//     /**
+//      * 字典 power_forecast_report_batch_config 的单个场站配置
+//      */
+//     public static class Config
+//     {
+//         /** 场站编码 */
+//         private String stationCode;
+
+//         /** 类型(short短期,ultra超短期) */
+//         private String type;
+
+//         /** 周期(daily) */
+//         private String period;
+
+//         /** 预计上报时点列表(HH:mm:ss) */
+//         private List<String> times;
+
+//         /** 提前初始化窗口(分钟), 缺省 0 */
+//         private Integer initInterval;
+
+//         public String getStationCode()
+//         {
+//             return stationCode;
+//         }
+
+//         public void setStationCode(String stationCode)
+//         {
+//             this.stationCode = stationCode;
+//         }
+
+//         public String getType()
+//         {
+//             return type;
+//         }
+
+//         public void setType(String type)
+//         {
+//             this.type = type;
+//         }
+
+//         public String getPeriod()
+//         {
+//             return period;
+//         }
+
+//         public void setPeriod(String period)
+//         {
+//             this.period = period;
+//         }
+
+//         public List<String> getTimes()
+//         {
+//             return times;
+//         }
+
+//         public void setTimes(List<String> times)
+//         {
+//             this.times = times;
+//         }
+
+//         public Integer getInitInterval()
+//         {
+//             return initInterval;
+//         }
+
+//         public void setInitInterval(Integer initInterval)
+//         {
+//             this.initInterval = initInterval;
+//         }
+//     }
+// }