소스 검색

[2610021] 增加采集数据mock: MockDataTask 新增每5分钟任务 generateCollectedData, 向 data_weather_local/data_electricity_local 各插1行(tm取最近5分钟整北京时间, config_station_id=2, 装机150000kW, 理论=晴空钟形/实发=理论×云量衰减×可用率, 两行共享物理状态, 按北京日期可复现), 并修复 DataWeatherLocalMapper.xml insert 重复 board_temperature 列及列值顺序错位、update 重复 SET

taosj 1 주 전
부모
커밋
b2d80cc9e6

+ 252 - 0
forecast-backend-server/src/main/java/com/ufo/project/data/task/MockDataTask.java

@@ -10,15 +10,21 @@ import java.time.LocalDate;
 import java.time.ZoneOffset;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
+import java.util.Date;
 import java.util.Locale;
 import java.util.Random;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 
 import com.ufo.base.framework.config.ProjectConfig;
+import com.ufo.project.model.domain.DataElectricityLocal;
+import com.ufo.project.model.mapper.DataElectricityLocalMapper;
+import com.ufo.project.weather.domain.DataWeatherLocal;
+import com.ufo.project.weather.mapper.DataWeatherLocalMapper;
 
 /**
  * 功率与气象mock数据生成任务
@@ -40,6 +46,10 @@ import com.ufo.base.framework.config.ProjectConfig;
  * 文件名时间为UTC, date_time列为GMT+8(即文件名时间+8h);
  * t2m/rh2/sp/云量/风/ghi/dni/dhi尽量符合物理规律, 按北纬38.9度定标, 同样按起报时间可复现.
  *
+ * 另含采集数据mock任务(generateCollectedData): 每5分钟向 data_weather_local 与
+ * data_electricity_local 各插1行, tm取当前最近5分钟整北京时间, config_station_id固定为2,
+ * 两行共享同一时刻物理状态(云量/钟形/季节), 数据尽量接近真实, 按北京日期可复现.
+ *
  * @author ufo
  * @date 2026-10-21
  */
@@ -48,6 +58,12 @@ public class MockDataTask
 {
     private static final Logger log = LoggerFactory.getLogger(MockDataTask.class);
 
+    @Autowired
+    private DataWeatherLocalMapper dataWeatherLocalMapper;
+
+    @Autowired
+    private DataElectricityLocalMapper dataElectricityLocalMapper;
+
     /** UTC时区 */
     private static final ZoneOffset UTC = ZoneOffset.UTC;
 
@@ -84,6 +100,15 @@ public class MockDataTask
     /** 出力上限 */
     private static final double POWER_CAP = 2600.0;
 
+    /** 采集数据mock固定场站ID(config_station_id) */
+    private static final long COLLECT_STATION_ID = 2L;
+
+    /** 采集数据mock装机容量(kW) */
+    private static final double COLLECT_CAPACITY_KW = 150000.0;
+
+    /** 采集数据mock理论出力定标的晴空峰值辐照度(W/m², 夏季正午晴空) */
+    private static final double CLEAR_PEAK_REF = 850.0;
+
     /** 短期气象数据点数(自起报时间(含)起960个间隔, 961点) */
     private static final int WEATHER_POINTS = 961;
 
@@ -210,6 +235,233 @@ public class MockDataTask
         }
     }
 
+    /**
+     * 采集数据: 每5分钟执行一次, 向 data_weather_local 与 data_electricity_local 各插1行,
+     * tm为当前时间就近取整到最近5分钟整北京时间(如00:05:34→00:05:00, 00:09:12→00:10:00),
+     * config_station_id固定为2; 两行共享同一时刻的物理状态(云量/钟形/季节), 功率与气象描述同一天气;
+     * 理论出力为按150000kW装机容量定标的晴空钟形(夏峰~150000/冬峰~112500, 夜间0),
+     * 实发功率=理论×慢快云量衰减×可用率(恒≤理论), 随机参数按北京日期派生, 当日数据稳定可复现
+     */
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void generateCollectedData()
+    {
+        ZonedDateTime tm = roundToFiveMinute(ZonedDateTime.now(SHANGHAI));
+        try
+        {
+            DayPhysics phys = dayPhysics(tm.toLocalDate());
+            double tH = tm.toInstant().getEpochSecond() / 3600.0;
+            double cosz = coszAt(phys, tm);
+            double cloud = tccAt(phys, tH) / 100.0;
+            double peak = 750 + 100 * phys.season;
+            double ghi = 0.90 * peak * (1 - 0.95 * cloud) * cosz + 0.10 * peak * cosz * (1 + cloud);
+            double ghiClear = peak * cosz * (0.9 + 0.1 * cosz);
+
+            double hour = tm.getHour() + tm.getMinute() / 60.0;
+            double t2m = 17 + 7 * phys.season + 5 * Math.cos(2 * Math.PI * (hour - 14.5) / 24);
+            double rh2 = clamp(85 - 2.2 * t2m + 12 * cloud, 35, 98);
+            double spHpa = 1013 + 3 * Math.sin(2 * Math.PI * tH / phys.tP + phys.pP);
+            double spd10 = Math.max(0.3, phys.windBase + Math.sin(2 * Math.PI * tH / 24 + phys.pW));
+            double dir10 = wrapDir(phys.dirBase
+                    + 40 * Math.sin(2 * Math.PI * tH / phys.tD + phys.pD)
+                    + 12 * Math.sin(2 * Math.PI * tH / phys.tD2 + phys.pD2));
+
+            double theoretical = clamp(COLLECT_CAPACITY_KW * ghiClear / CLEAR_PEAK_REF, 0, COLLECT_CAPACITY_KW);
+            double fastFluct = 1 - phys.fastAmp * (0.5 + 0.5 * Math.sin(2 * Math.PI * tH / phys.fastT + phys.fastP));
+            double actual = clamp(theoretical * (1 - 0.90 * cloud) * fastFluct * phys.availability / 100.0, 0, theoretical);
+
+            Date tmDate = tm.toInstant().toDate();
+
+            DataWeatherLocal weather = new DataWeatherLocal();
+            weather.setTm(tmDate);
+            weather.setConfigStationId(COLLECT_STATION_ID);
+            weather.setIrradiance(round(ghi, 2));
+            weather.setInclinedIrradiance(round(ghi * (1 + 0.12 * cosz), 2));
+            weather.setTemperature(round(t2m, 2));
+            weather.setBoardTemperature(round(t2m + 0.031 * ghi, 2));
+            weather.setPressure(round(spHpa, 2));
+            weather.setHumidity(round(rh2, 1));
+            weather.setWindSpeed(round(spd10, 2));
+            weather.setWindDirection(round(dir10, 1));
+            weather.setDailyIrradiance(round(dailyIrradianceMj(phys, tm), 3));
+            dataWeatherLocalMapper.insertDataWeatherLocal(weather);
+
+            DataElectricityLocal power = new DataElectricityLocal();
+            power.setTm(tmDate);
+            power.setConfigStationId(COLLECT_STATION_ID);
+            power.setActualPower(round(actual, 1));
+            power.setTheoreticalPower(round(theoretical, 1));
+            power.setCurtailmentPower(0.0);
+            power.setAvailability(round(phys.availability, 2));
+            power.setDeviceStatus(0);
+            power.setGridStatus(1);
+            dataElectricityLocalMapper.insertDataElectricityLocal(power);
+
+            log.info("[MockDataTask] 生成采集数据mock, tm={}", tm.format(TIME_SH_FMT));
+        }
+        catch (Exception e)
+        {
+            log.error("[MockDataTask] 生成采集数据mock失败, tm={}", tm, e);
+        }
+    }
+
+    /**
+     * 当前北京时间就近取整到最近5分钟整点(如00:05:34→00:05:00, 00:09:12→00:10:00),
+     * 跨午夜时自动进位到次日00:00
+     */
+    private static ZonedDateTime roundToFiveMinute(ZonedDateTime now)
+    {
+        long secs = now.getHour() * 3600L + now.getMinute() * 60L + now.getSecond();
+        long rounded = ((secs + 150) / 300) * 300;
+        LocalDate day = now.toLocalDate();
+        if (rounded >= 86400L)
+        {
+            day = day.plusDays(1);
+            rounded -= 86400L;
+        }
+        return day.atStartOfDay(SHANGHAI).plusSeconds(rounded);
+    }
+
+    /**
+     * 采集数据mock: 由北京日期派生当日物理参数
+     * (种子为当日北京午夜的epoch分钟, 当日结果稳定可复现, 跨天换新云图/风场)
+     */
+    private static DayPhysics dayPhysics(LocalDate day)
+    {
+        DayPhysics p = new DayPhysics();
+        p.season = Math.cos(2 * Math.PI * (day.getDayOfYear() - 195.0) / 365.0);
+        double halfDay = 5.9 + 1.4 * p.season;
+        p.sunrise = 12 - halfDay;
+        p.sunset = 12 + halfDay;
+
+        Random random = new Random(day.atStartOfDay(SHANGHAI).toInstant().toEpochMilli() / 60000L);
+        p.baseCc = 20 + 40 * random.nextDouble();
+        p.ampCc = 10 + 20 * random.nextDouble();
+        p.tCc = 48 + 24 * random.nextDouble();
+        p.pCc = 2 * Math.PI * random.nextDouble();
+        p.tP = 96 + 48 * random.nextDouble();
+        p.pP = 2 * Math.PI * random.nextDouble();
+        p.windBase = 3 + 2 * random.nextDouble();
+        p.pW = 2 * Math.PI * random.nextDouble();
+        p.dirBase = 360 * random.nextDouble();
+        p.tD = 48 + 48 * random.nextDouble();
+        p.pD = 2 * Math.PI * random.nextDouble();
+        p.tD2 = 6 + 8 * random.nextDouble();
+        p.pD2 = 2 * Math.PI * random.nextDouble();
+        p.fastAmp = 0.02 + 0.08 * random.nextDouble();
+        p.fastT = 1 + 1 * random.nextDouble();
+        p.fastP = 2 * Math.PI * random.nextDouble();
+        p.availability = 99 + random.nextDouble();
+        return p;
+    }
+
+    /**
+     * 太阳天顶角余弦近似: 夜间为0, 正午(墙钟12点)为1, 白天呈正弦钟形
+     */
+    private static double coszAt(DayPhysics p, ZonedDateTime t)
+    {
+        double hour = t.getHour() + t.getMinute() / 60.0;
+        if (hour <= p.sunrise || hour >= p.sunset)
+        {
+            return 0;
+        }
+        return Math.sin(Math.PI * (hour - p.sunrise) / (p.sunset - p.sunrise));
+    }
+
+    /**
+     * 指定epoch时刻的总云量(0~100), 慢漂移
+     */
+    private static double tccAt(DayPhysics p, double tH)
+    {
+        return clamp(p.baseCc + p.ampCc * Math.sin(2 * Math.PI * tH / p.tCc + p.pCc), 0, 100);
+    }
+
+    /**
+     * 指定时刻的水平面总辐照度GHI(W/m²):
+     * 晴空峰值随季节, 云量主要削弱直分量, 散射随云量增大, 夜间为0
+     */
+    private static double ghiAt(DayPhysics p, ZonedDateTime t)
+    {
+        double cosz = coszAt(p, t);
+        if (cosz <= 0)
+        {
+            return 0;
+        }
+        double peak = 750 + 100 * p.season;
+        double cloud = tccAt(p, t.toInstant().getEpochSecond() / 3600.0) / 100.0;
+        return 0.90 * peak * (1 - 0.95 * cloud) * cosz + 0.10 * peak * cosz * (1 + cloud);
+    }
+
+    /**
+     * 当日当地00:00至tm的水平面总辐照度累计(MJ/m²),
+     * 按5分钟步长用当日物理参数积分
+     */
+    private static double dailyIrradianceMj(DayPhysics p, ZonedDateTime tm)
+    {
+        ZonedDateTime day0 = tm.toLocalDate().atStartOfDay(SHANGHAI);
+        double mj = 0;
+        for (long s = 0; s < tm.getHour() * 3600L + tm.getMinute() * 60L; s += 300)
+        {
+            mj += ghiAt(p, day0.plusSeconds(s)) * 300.0 / 1e6;
+        }
+        return mj;
+    }
+
+    /**
+     * 按指定小数位四舍五入
+     */
+    private static double round(double v, int scale)
+    {
+        double f = Math.pow(10, scale);
+        return Math.round(v * f) / f;
+    }
+
+    /**
+     * 采集数据mock: 每北京日期一组物理参数(钟形/云量/气压/风/快云/可用率)
+     */
+    private static final class DayPhysics
+    {
+        /** 季节系数(-1冬至~1夏至) */
+        double season;
+        /** 日出时刻(北京墙钟小时) */
+        double sunrise;
+        /** 日落时刻(北京墙钟小时) */
+        double sunset;
+        /** 慢云量基准(%) */
+        double baseCc;
+        /** 慢云量幅值(%) */
+        double ampCc;
+        /** 慢云量周期(小时) */
+        double tCc;
+        /** 慢云量相位(rad) */
+        double pCc;
+        /** 气压漂移周期(小时) */
+        double tP;
+        /** 气压漂移相位(rad) */
+        double pP;
+        /** 风速基准(m/s) */
+        double windBase;
+        /** 风速日变化相位(rad) */
+        double pW;
+        /** 风向基准(度) */
+        double dirBase;
+        /** 风向慢摆周期(小时) */
+        double tD;
+        /** 风向慢摆相位(rad) */
+        double pD;
+        /** 风向快摆周期(小时) */
+        double tD2;
+        /** 风向快摆相位(rad) */
+        double pD2;
+        /** 快云量幅值(功率衰减系数) */
+        double fastAmp;
+        /** 快云量周期(小时) */
+        double fastT;
+        /** 快云量相位(rad) */
+        double fastP;
+        /** 可用率(%) */
+        double availability;
+    }
+
     /**
      * 生成单个mock预报文件
      *

+ 1 - 4
forecast-backend-server/src/main/resources/mybatis/weather/DataWeatherLocalMapper.xml

@@ -89,21 +89,19 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="humidity != null">humidity,</if>
             <if test="windSpeed != null">wind_speed,</if>
             <if test="windDirection != null">wind_direction,</if>
-            <if test="boardTemperature != null">board_temperature,</if>
             <if test="dailyIrradiance != null">daily_irradiance,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="tm != null">#{tm},</if>
             <if test="configStationId != null">#{configStationId},</if>
-            <if test="inclinedIrradiance != null">#{inclinedIrradiance},</if> 
             <if test="irradiance != null">#{irradiance},</if>
+            <if test="inclinedIrradiance != null">#{inclinedIrradiance},</if>
             <if test="boardTemperature != null">#{boardTemperature},</if>
             <if test="temperature != null">#{temperature},</if>
             <if test="pressure != null">#{pressure},</if>
             <if test="humidity != null">#{humidity},</if>
             <if test="windSpeed != null">#{windSpeed},</if>
             <if test="windDirection != null">#{windDirection},</if>
-            <if test="boardTemperature != null">#{boardTemperature},</if>
             <if test="dailyIrradiance != null">#{dailyIrradiance},</if>
          </trim>
     </insert>
@@ -120,7 +118,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="humidity != null">humidity = #{humidity},</if>
             <if test="windSpeed != null">wind_speed = #{windSpeed},</if>
             <if test="windDirection != null">wind_direction = #{windDirection},</if>
-            <if test="boardTemperature != null">board_temperature = #{boardTemperature},</if>
             <if test="dailyIrradiance != null">daily_irradiance = #{dailyIrradiance},</if>
         </trim>
         where tm = #{tm}