perf(导入): 重构区域配置加载与天气数据查询逻辑
- 新增 AreaConfigUtil 工具类,使用 ThreadLocal 缓存区域配置,支持并发导入任务 - 重构 HourlyOutageExcelProcessService,按区县批量查询气象数据,减少数据库访问次数 - 优化数据处理流程,引入行上下文和区县时间范围聚合,提升处理效率 - 在导入任务开始前初始化配置,任务结束后清理 ThreadLocal 防止内存泄漏
This commit is contained in:
parent
2f65f85143
commit
657e8c8375
@ -10,6 +10,7 @@ import com.southern.power.grid.entity.ImportTask;
|
||||
import com.southern.power.grid.enums.ImportTaskStatusEnum;
|
||||
import com.southern.power.grid.service.IDnerDailyPowerOutageEventSyncService;
|
||||
import com.southern.power.grid.service.impl.HourlyOutageExcelProcessService;
|
||||
import com.southern.power.grid.utils.AreaConfigUtil;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@ -48,6 +49,9 @@ public class DataExcelListener extends AnalysisEventListener<DataExcelEntity> {
|
||||
@Autowired
|
||||
private IDnerDailyPowerOutageEventSyncService dailyPowerOutageEventSycnService;
|
||||
|
||||
@Autowired
|
||||
private AreaConfigUtil areaConfigUtil;
|
||||
|
||||
// 注入任务
|
||||
@Setter
|
||||
private ImportTask task;
|
||||
@ -101,6 +105,9 @@ public class DataExcelListener extends AnalysisEventListener<DataExcelEntity> {
|
||||
success = 0;
|
||||
fail = 0;
|
||||
task = null;
|
||||
|
||||
// 清理 ThreadLocal 变量,防止内存泄漏
|
||||
areaConfigUtil.cleanup();
|
||||
}
|
||||
|
||||
// ==================== 核心:分批插入 + 手动事务 ====================
|
||||
@ -127,6 +134,21 @@ public class DataExcelListener extends AnalysisEventListener<DataExcelEntity> {
|
||||
.eq(ImportTask::getTaskNo, task.getTaskNo()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化区域配置
|
||||
* 在每次导入任务开始前调用,加载南网区域配置和气象区域配置
|
||||
*/
|
||||
public void initAreaConfigs() {
|
||||
areaConfigUtil.loadAllConfigs();
|
||||
// 验证配置是否成功加载
|
||||
if (areaConfigUtil.getNwAreaMapSize() == 0) {
|
||||
throw new IllegalStateException("南网区域配置加载失败,数据为空");
|
||||
}
|
||||
if (areaConfigUtil.getWeatherAreaMapSize() == 0) {
|
||||
throw new IllegalStateException("气象区域配置加载失败,数据为空");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Excel 总条数(只读行数,不处理数据)
|
||||
*/
|
||||
|
||||
@ -41,6 +41,9 @@ public class AsyncImportServiceImpl {
|
||||
.set(ImportTask::getTotal, total)
|
||||
.eq(ImportTask::getTaskNo, task.getTaskNo()));
|
||||
|
||||
// 初始化区域配置(在导入任务开始前加载一次)
|
||||
dataExcelListener.initAreaConfigs();
|
||||
|
||||
// 注入任务
|
||||
dataExcelListener.setTask(task);
|
||||
inputStream.reset(); // 流被读过一次了,重置一下
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package com.southern.power.grid.service.impl;
|
||||
|
||||
import com.southern.power.grid.dao.DnerHourlyPowerOutageEventMapper;
|
||||
import com.southern.power.grid.dao.NwSiteAreaConfigurationMapper;
|
||||
import com.southern.power.grid.dao.RegionalWeatherDataMapper;
|
||||
import com.southern.power.grid.dao.WeatherSiteAreaConfigurationMapper;
|
||||
import com.southern.power.grid.entity.*;
|
||||
import com.southern.power.grid.entity.DataExcelEntity;
|
||||
import com.southern.power.grid.entity.DnerHourlyPowerOutageEvent;
|
||||
import com.southern.power.grid.entity.RegionalWeatherData;
|
||||
import com.southern.power.grid.utils.AreaConfigUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@ -18,15 +19,12 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class HourlyOutageExcelProcessService {
|
||||
@Resource
|
||||
private NwSiteAreaConfigurationMapper nwSiteAreaConfigurationMapper;
|
||||
|
||||
@Resource
|
||||
private WeatherSiteAreaConfigurationMapper weatherSiteAreaConfigurationMapper;
|
||||
|
||||
@Resource
|
||||
private RegionalWeatherDataMapper regionalWeatherDataMapper;
|
||||
@ -34,104 +32,47 @@ public class HourlyOutageExcelProcessService {
|
||||
@Resource
|
||||
private DnerHourlyPowerOutageEventMapper dnerHourlyPowerOutageEventMapper;
|
||||
|
||||
@Resource
|
||||
private AreaConfigUtil areaConfigUtil;
|
||||
|
||||
private static final Float lengthOutage = 60F;
|
||||
private static final int WEATHER_QUERY_BATCH_SIZE = 200;
|
||||
|
||||
// 定义时间格式器(建议定义为静态常量,避免重复创建)
|
||||
/**
|
||||
* 时间格式器(静态常量,避免重复创建)
|
||||
*/
|
||||
private static final DateTimeFormatter DATA_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
/**
|
||||
* 南网区域配置
|
||||
* key = nw_province + "|" + nw_city + "|" + nw_district ;value = district_code
|
||||
*/
|
||||
private final Map<String, String> nwAreaMap = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 气象区域配置
|
||||
* key = district_code ;value = WeatherSiteAreaConfiguration(包含 station_id/station_name)
|
||||
*/
|
||||
private final Map<String, List<WeatherSiteAreaConfiguration>> weatherAreaMap = new HashMap<>();
|
||||
|
||||
private static final DateTimeFormatter DB_DATETIME_STR =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final String AREA_KEY_SEPARATOR = "|";
|
||||
|
||||
/**
|
||||
* 对外主入口:处理一批 Excel 解析后的数据,按逻辑补齐气象数据并批量入库
|
||||
*/
|
||||
@Transactional
|
||||
public void process(List<DataExcelEntity> excelRows, Long eventId) {
|
||||
// 1. 加载南网区划配置表
|
||||
loadNwAreaConfig();
|
||||
// 2. 加载气象区划配置表
|
||||
loadWeatherAreaConfig();
|
||||
|
||||
if (ObjectUtils.isEmpty(excelRows)) {
|
||||
return;
|
||||
}
|
||||
// 使用 Map 存储待插入实体,Key 为 districtCode + "|" + dataTime
|
||||
Map<String, DnerHourlyPowerOutageEvent> toInsertMap = new HashMap<>();
|
||||
// 区域编码缓存:相同省市区只查一次映射
|
||||
Map<String, String> districtCodeCache = new HashMap<>();
|
||||
// 行与区县编码映射:避免二次重复计算
|
||||
List<RowContext> rowContexts = new ArrayList<>(excelRows.size());
|
||||
// 每个区县对应的气象查询时间范围
|
||||
Map<String, TimeRange> districtTimeRangeMap = new HashMap<>();
|
||||
|
||||
// 3 & 4. 循环处理 Excel 行
|
||||
for (DataExcelEntity row : excelRows) {
|
||||
// if(row.getLengthOutage()<lengthOutage) {
|
||||
// log.info("lengthOutage is less than 60! {}, {}, {}", row.getProvince(), row.getCity(), row.getDistrict());
|
||||
// continue;
|
||||
// }
|
||||
row.setFaultUserCount(0);
|
||||
row.setScheduledUserCount(0);
|
||||
// 收集并过滤有效行,同时构建区县维度时间范围
|
||||
collectValidRowsAndRanges(excelRows, districtCodeCache, rowContexts, districtTimeRangeMap);
|
||||
|
||||
// 只有当前不是【已复电】状态,才需要判断
|
||||
if (!"已复电".equals(row.getOutageState())) {
|
||||
boolean needSetRestored = false;
|
||||
|
||||
// 场景1:停电时长为空 → 用当前时间 - 开始时间 判断是否超7天
|
||||
if (ObjectUtils.isEmpty(row.getLengthOutage())) {
|
||||
needSetRestored = LocalDateTime.now().isAfter(row.getStartTime().plusDays(7));
|
||||
}
|
||||
// 场景2:停电时长不为空 → 直接判断时长是否≥7天
|
||||
else {
|
||||
needSetRestored = row.getLengthOutage() >= 7L * 24 * 60;
|
||||
if (rowContexts.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 满足条件则设置为已复电
|
||||
if (needSetRestored) {
|
||||
row.setOutageState("已复电");
|
||||
}
|
||||
}
|
||||
// 3.2 按区县分批 IN 查询气象数据,并建立区县+时次索引
|
||||
Map<String, NavigableMap<LocalDateTime, RegionalWeatherData>> weatherIndex = loadWeatherIndexByDistrict(districtTimeRangeMap);
|
||||
|
||||
|
||||
// 3.1 根据南网省/市/区县 → district_code
|
||||
String districtCode = findDistrictCode(row.getProvince(), row.getCity(), row.getDistrict());
|
||||
if (districtCode == null) {
|
||||
// 找不到映射,可记录日志或统计
|
||||
log.info("区域编码没有找到! {}, {}, {}", row.getProvince(), row.getCity(), row.getDistrict());
|
||||
continue;
|
||||
}
|
||||
// 3.2 获取区域时段内气象数据(需要包含开始时间的整时数据)
|
||||
List<RegionalWeatherData> regionalWeatherDataList =
|
||||
regionalWeatherDataMapper.selectByOrgCodeAndDataTime(districtCode,
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00").format(row.getStartTime()));
|
||||
|
||||
// 3.4 组装分时停电事件实体
|
||||
for (RegionalWeatherData regionalWeatherData : regionalWeatherDataList) {
|
||||
LocalDateTime dataDateTime;
|
||||
try {
|
||||
dataDateTime = LocalDateTime.parse(regionalWeatherData.getDataTime(), DATA_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
log.warn("解析 dataTime 失败: {}", regionalWeatherData.getDataTime());
|
||||
continue;
|
||||
}
|
||||
if (row.getStartTime().isBefore(dataDateTime) && dataDateTime.isBefore(row.getEndTime())) {
|
||||
String key = districtCode + "|" + regionalWeatherData.getDataTime();
|
||||
// 检查 Map 中是否已存在相同组合键
|
||||
DnerHourlyPowerOutageEvent existing = toInsertMap.get(key);
|
||||
if (existing == null) {
|
||||
// 不存在,创建新实体并放入 Map
|
||||
DnerHourlyPowerOutageEvent entity = buildHourlyEvent(row, districtCode, regionalWeatherData);
|
||||
entity.setEventId(eventId);
|
||||
toInsertMap.put(key, entity);
|
||||
} else {
|
||||
// 已存在,执行累加逻辑
|
||||
accumulateEvent(existing, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 按区县天气索引组装分时事件,并执行同时次聚合
|
||||
assembleHourlyEvents(rowContexts, weatherIndex, toInsertMap, eventId);
|
||||
|
||||
if (!toInsertMap.isEmpty()) {
|
||||
// 将 Map 中的 values 转为 List 进行批量插入
|
||||
@ -153,53 +94,249 @@ public class HourlyOutageExcelProcessService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载南网区划配置
|
||||
* 标准化单行停电数据:初始化分类用户数、补齐结束时间、按规则更新停电状态。
|
||||
*
|
||||
**/
|
||||
private void loadNwAreaConfig() {
|
||||
nwAreaMap.clear();
|
||||
List<NwSiteAreaConfiguration> list = nwSiteAreaConfigurationMapper.selectAll();
|
||||
if (list == null) return;
|
||||
* @param row 单行停电数据
|
||||
*/
|
||||
private void normalizeRowBeforeProcess(DataExcelEntity row) {
|
||||
// 先清空分类用户数,后续会根据停电类型重新赋值并用于聚合
|
||||
row.setFaultUserCount(0);
|
||||
row.setScheduledUserCount(0);
|
||||
// 如果结束时间缺失但有起始时间与停电时长,则按“分钟 -> 秒”回填结束时间
|
||||
if (row.getEndTime() == null && row.getStartTime() != null && row.getLengthOutage() != null) {
|
||||
long outageSeconds = Math.round(row.getLengthOutage() * 60);
|
||||
row.setEndTime(row.getStartTime().plusSeconds(outageSeconds));
|
||||
}
|
||||
|
||||
for (NwSiteAreaConfiguration cfg : list) {
|
||||
String key = buildNwKey(cfg.getNwProvince(), cfg.getNwCity(), cfg.getNwDistrict());
|
||||
if (cfg.getDistrictCode() != null && !cfg.getDistrictCode().isEmpty()) {
|
||||
nwAreaMap.put(key, cfg.getDistrictCode());
|
||||
// 只有当前不是【已复电】状态,才需要判断
|
||||
if (!"已复电".equals(row.getOutageState())) {
|
||||
boolean needSetRestored = false;
|
||||
|
||||
// 场景1:停电时长为空 → 用当前时间 - 开始时间 判断是否超7天
|
||||
if (ObjectUtils.isEmpty(row.getLengthOutage())) {
|
||||
if (row.getStartTime() != null) {
|
||||
needSetRestored = LocalDateTime.now().isAfter(row.getStartTime().plusDays(7));
|
||||
}
|
||||
}
|
||||
// 场景2:停电时长不为空 → 直接判断时长是否≥7天
|
||||
else {
|
||||
needSetRestored = row.getLengthOutage() >= 7L * 24 * 60;
|
||||
}
|
||||
|
||||
// 满足条件则设置为已复电
|
||||
if (needSetRestored) {
|
||||
row.setOutageState("已复电");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载气象区划配置表
|
||||
* 遍历原始 Excel 行,完成预处理、合法性校验、区域编码映射,并产出后续处理所需上下文。
|
||||
*
|
||||
**/
|
||||
private void loadWeatherAreaConfig() {
|
||||
weatherAreaMap.clear();
|
||||
List<WeatherSiteAreaConfiguration> list = weatherSiteAreaConfigurationMapper.selectAll();
|
||||
if (list == null) return;
|
||||
for (WeatherSiteAreaConfiguration cfg : list) {
|
||||
if (cfg.getDistrictCode() != null && !cfg.getDistrictCode().isEmpty()) {
|
||||
List<WeatherSiteAreaConfiguration> weatherSiteAreaConfigurations = weatherAreaMap.get(cfg.getDistrictCode());
|
||||
if (weatherSiteAreaConfigurations == null) {
|
||||
weatherSiteAreaConfigurations = new ArrayList<>();
|
||||
weatherSiteAreaConfigurations.add(cfg);
|
||||
weatherAreaMap.put(cfg.getDistrictCode(), weatherSiteAreaConfigurations);
|
||||
* @param excelRows 原始 Excel 行
|
||||
* @param districtCodeCache 区域编码缓存(省市区 -> 区县编码)
|
||||
* @param rowContexts 输出参数:有效行与区县编码映射
|
||||
* @param districtTimeRangeMap 输出参数:每个区县对应的最小开始时间与最大结束时间
|
||||
*/
|
||||
private void collectValidRowsAndRanges(List<DataExcelEntity> excelRows,
|
||||
Map<String, String> districtCodeCache,
|
||||
List<RowContext> rowContexts,
|
||||
Map<String, TimeRange> districtTimeRangeMap) {
|
||||
// 遍历每一行:预处理 -> 校验 -> 映射区县 -> 产出上下文
|
||||
for (DataExcelEntity row : excelRows) {
|
||||
// if(row.getLengthOutage()<lengthOutage) {
|
||||
// log.info("lengthOutage is less than 60! {}, {}, {}", row.getProvince(), row.getCity(), row.getDistrict());
|
||||
// continue;
|
||||
// }
|
||||
// 预处理当前行:初始化用户数、补齐结束时间、必要时修正停电状态
|
||||
normalizeRowBeforeProcess(row);
|
||||
|
||||
if (row.getStartTime() == null || row.getEndTime() == null || !row.getStartTime().isBefore(row.getEndTime())) {
|
||||
log.warn("停电时间非法,跳过该行: {}, {}, {}, start={}, end={}",
|
||||
row.getProvince(), row.getCity(), row.getDistrict(), row.getStartTime(), row.getEndTime());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 根据南网省/市/区县 → district_code(带缓存)
|
||||
String districtCode = findDistrictCode(row.getProvince(), row.getCity(), row.getDistrict(), districtCodeCache);
|
||||
if (districtCode == null) {
|
||||
// 找不到映射,可记录日志或统计
|
||||
log.info("区域编码没有找到! {}, {}, {}", row.getProvince(), row.getCity(), row.getDistrict());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 保存有效行与区县映射,供后续按天气索引生成分时事件
|
||||
rowContexts.add(new RowContext(row, districtCode));
|
||||
// 维护区县级最小开始时间与最大结束时间,用于后续批量查询天气范围
|
||||
districtTimeRangeMap.compute(districtCode, (k, v) -> mergeTimeRange(v, row.getStartTime(), row.getEndTime()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于“区县-时间”气象索引构建分时停电事件,并按“区县+时次”做用户数聚合。
|
||||
*
|
||||
* @param rowContexts 有效停电行上下文(含区县编码)
|
||||
* @param weatherIndex 区县维度天气有序索引
|
||||
* @param toInsertMap 输出参数:待入库事件(key=区县编码|dataTime)
|
||||
* @param eventId 事件批次ID
|
||||
*/
|
||||
private void assembleHourlyEvents(List<RowContext> rowContexts,
|
||||
Map<String, NavigableMap<LocalDateTime, RegionalWeatherData>> weatherIndex,
|
||||
Map<String, DnerHourlyPowerOutageEvent> toInsertMap,
|
||||
Long eventId) {
|
||||
// 遍历有效停电行,按停电时间窗命中对应整时天气数据
|
||||
for (RowContext context : rowContexts) {
|
||||
DataExcelEntity row = context.getRow();
|
||||
String districtCode = context.getDistrictCode();
|
||||
NavigableMap<LocalDateTime, RegionalWeatherData> districtWeatherMap = weatherIndex.get(districtCode);
|
||||
// 当前区县没有天气索引时无法组装分时事件
|
||||
if (ObjectUtils.isEmpty(districtWeatherMap)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 命中起止时间范围内的所有整时数据(含 start 所在整时,含 end 所在整时)
|
||||
LocalDateTime startHour = row.getStartTime().withMinute(0).withSecond(0).withNano(0);
|
||||
LocalDateTime endHour = row.getEndTime().withMinute(0).withSecond(0).withNano(0);
|
||||
if (endHour.isBefore(startHour)) {
|
||||
continue;
|
||||
}
|
||||
NavigableMap<LocalDateTime, RegionalWeatherData> hitMap =
|
||||
districtWeatherMap.subMap(startHour, true, endHour, true);
|
||||
|
||||
for (RegionalWeatherData regionalWeatherData : hitMap.values()) {
|
||||
// 同区县同 dataTime 视为同一分时事件,进行合并累加
|
||||
String key = districtCode + AREA_KEY_SEPARATOR + regionalWeatherData.getDataTime();
|
||||
// 检查 Map 中是否已存在相同组合键
|
||||
DnerHourlyPowerOutageEvent existing = toInsertMap.get(key);
|
||||
if (existing == null) {
|
||||
// 不存在,创建新实体并放入 Map
|
||||
DnerHourlyPowerOutageEvent entity = buildHourlyEvent(row, districtCode, regionalWeatherData);
|
||||
entity.setEventId(eventId);
|
||||
toInsertMap.put(key, entity);
|
||||
} else {
|
||||
weatherSiteAreaConfigurations.add(cfg);
|
||||
// 已存在,执行累加逻辑
|
||||
accumulateEvent(existing, row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String buildNwKey(String province, String city, String district) {
|
||||
return (province == null ? "" : province.trim()) + "|"
|
||||
+ (city == null ? "" : city.trim()) + "|"
|
||||
+ (district == null ? "" : district.trim());
|
||||
/**
|
||||
* 按区县分批查询气象数据,并构建“区县 -> 时间有序气象索引”。
|
||||
* 查询阶段使用 IN + 时间范围批量拉取,落地阶段再按各区县自身时间窗二次过滤,
|
||||
* 兼顾减少 SQL 次数与避免无效数据进入内存索引。
|
||||
*
|
||||
* @param districtTimeRangeMap key=区县编码,value=该区县在当前批次停电数据中的最小开始时间与最大结束时间
|
||||
* @return key=区县编码,value=按 dataTime 排序的气象数据(用于后续 subMap 区间命中)
|
||||
*/
|
||||
private Map<String, NavigableMap<LocalDateTime, RegionalWeatherData>> loadWeatherIndexByDistrict(
|
||||
Map<String, TimeRange> districtTimeRangeMap) {
|
||||
// 预分配容量,减少 Map 扩容开销
|
||||
Map<String, NavigableMap<LocalDateTime, RegionalWeatherData>> weatherIndex = new HashMap<>(districtTimeRangeMap.size());
|
||||
if (ObjectUtils.isEmpty(districtTimeRangeMap)) {
|
||||
return weatherIndex;
|
||||
}
|
||||
|
||||
private String findDistrictCode(String nwProvince, String nwCity, String nwDistrict) {
|
||||
String key = buildNwKey(nwProvince, nwCity, nwDistrict);
|
||||
return nwAreaMap.get(key);
|
||||
// 初始化每个区县的有序时间索引容器
|
||||
List<String> districtCodes = new ArrayList<>(districtTimeRangeMap.keySet());
|
||||
for (String districtCode : districtCodes) {
|
||||
weatherIndex.put(districtCode, new TreeMap<>());
|
||||
}
|
||||
|
||||
// 按固定批大小分批执行 IN 查询,避免单次 SQL 过长
|
||||
for (int i = 0; i < districtCodes.size(); i += WEATHER_QUERY_BATCH_SIZE) {
|
||||
int end = Math.min(i + WEATHER_QUERY_BATCH_SIZE, districtCodes.size());
|
||||
List<String> batchCodes = districtCodes.subList(i, end);
|
||||
|
||||
// 计算该批区县的总体查询时间窗(最小开始时间~最大结束时间)
|
||||
LocalDateTime batchMinStart = null;
|
||||
LocalDateTime batchMaxEnd = null;
|
||||
for (String districtCode : batchCodes) {
|
||||
TimeRange timeRange = districtTimeRangeMap.get(districtCode);
|
||||
if (timeRange == null) {
|
||||
continue;
|
||||
}
|
||||
if (batchMinStart == null || timeRange.getMinStart().isBefore(batchMinStart)) {
|
||||
batchMinStart = timeRange.getMinStart();
|
||||
}
|
||||
if (batchMaxEnd == null || timeRange.getMaxEnd().isAfter(batchMaxEnd)) {
|
||||
batchMaxEnd = timeRange.getMaxEnd();
|
||||
}
|
||||
}
|
||||
if (batchMinStart == null || batchMaxEnd == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 批量拉取该批区县在时间窗内的气象数据
|
||||
List<RegionalWeatherData> weatherDataList = regionalWeatherDataMapper.selectByOrgCodesAndTimeRange(
|
||||
new ArrayList<>(batchCodes),
|
||||
formatToHourStart(batchMinStart),
|
||||
formatToHourStart(batchMaxEnd));
|
||||
|
||||
for (RegionalWeatherData weatherData : weatherDataList) {
|
||||
String orgCode = weatherData.getOrgCode();
|
||||
TimeRange districtRange = districtTimeRangeMap.get(orgCode);
|
||||
if (districtRange == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LocalDateTime dataDateTime;
|
||||
try {
|
||||
dataDateTime = LocalDateTime.parse(weatherData.getDataTime(), DATA_TIME_FORMATTER);
|
||||
} catch (DateTimeParseException e) {
|
||||
log.warn("解析 dataTime 失败: {}", weatherData.getDataTime());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 二次过滤:仅保留落在该区县自身时间窗内的数据
|
||||
if (dataDateTime.isBefore(districtRange.getMinStart()) || dataDateTime.isAfter(districtRange.getMaxEnd())) {
|
||||
continue;
|
||||
}
|
||||
// 写入区县级有序索引,供后续 subMap 快速区间命中
|
||||
NavigableMap<LocalDateTime, RegionalWeatherData> districtWeatherMap = weatherIndex.get(orgCode);
|
||||
if (districtWeatherMap != null) {
|
||||
districtWeatherMap.put(dataDateTime, weatherData);
|
||||
}
|
||||
}
|
||||
}
|
||||
return weatherIndex;
|
||||
}
|
||||
|
||||
private TimeRange mergeTimeRange(TimeRange current, LocalDateTime startTime, LocalDateTime endTime) {
|
||||
if (current == null) {
|
||||
return new TimeRange(startTime, endTime);
|
||||
}
|
||||
if (startTime.isBefore(current.getMinStart())) {
|
||||
current.setMinStart(startTime);
|
||||
}
|
||||
if (endTime.isAfter(current.getMaxEnd())) {
|
||||
current.setMaxEnd(endTime);
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
private String findDistrictCode(String nwProvince, String nwCity, String nwDistrict, Map<String, String> districtCodeCache) {
|
||||
String key = buildAreaKey(nwProvince, nwCity, nwDistrict);
|
||||
if (districtCodeCache.containsKey(key)) {
|
||||
return districtCodeCache.get(key);
|
||||
}
|
||||
String districtCode = areaConfigUtil.getNwAreaMap().get(key);
|
||||
districtCodeCache.put(key, districtCode);
|
||||
return districtCode;
|
||||
}
|
||||
|
||||
private String buildAreaKey(String nwProvince, String nwCity, String nwDistrict) {
|
||||
return safeTrim(nwProvince) + AREA_KEY_SEPARATOR
|
||||
+ safeTrim(nwCity) + AREA_KEY_SEPARATOR
|
||||
+ safeTrim(nwDistrict);
|
||||
}
|
||||
|
||||
private String safeTrim(String value) {
|
||||
return value == null ? "" : value.trim();
|
||||
}
|
||||
|
||||
private String formatToHourStart(LocalDateTime dateTime) {
|
||||
return dateTime.withMinute(0).withSecond(0).withNano(0).format(DATA_TIME_FORMATTER);
|
||||
}
|
||||
|
||||
private DnerHourlyPowerOutageEvent buildHourlyEvent(DataExcelEntity row,
|
||||
@ -255,4 +392,48 @@ public class HourlyOutageExcelProcessService {
|
||||
// create_by/update_by 可根据当前登录用户等进行填充
|
||||
return event;
|
||||
}
|
||||
|
||||
private static class RowContext {
|
||||
private final DataExcelEntity row;
|
||||
private final String districtCode;
|
||||
|
||||
private RowContext(DataExcelEntity row, String districtCode) {
|
||||
this.row = row;
|
||||
this.districtCode = districtCode;
|
||||
}
|
||||
|
||||
public DataExcelEntity getRow() {
|
||||
return row;
|
||||
}
|
||||
|
||||
public String getDistrictCode() {
|
||||
return districtCode;
|
||||
}
|
||||
}
|
||||
|
||||
private static class TimeRange {
|
||||
private LocalDateTime minStart;
|
||||
private LocalDateTime maxEnd;
|
||||
|
||||
private TimeRange(LocalDateTime minStart, LocalDateTime maxEnd) {
|
||||
this.minStart = minStart;
|
||||
this.maxEnd = maxEnd;
|
||||
}
|
||||
|
||||
public LocalDateTime getMinStart() {
|
||||
return minStart;
|
||||
}
|
||||
|
||||
public void setMinStart(LocalDateTime minStart) {
|
||||
this.minStart = minStart;
|
||||
}
|
||||
|
||||
public LocalDateTime getMaxEnd() {
|
||||
return maxEnd;
|
||||
}
|
||||
|
||||
public void setMaxEnd(LocalDateTime maxEnd) {
|
||||
this.maxEnd = maxEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
192
src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java
Normal file
192
src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java
Normal file
@ -0,0 +1,192 @@
|
||||
package com.southern.power.grid.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.southern.power.grid.dao.NwSiteAreaConfigurationMapper;
|
||||
import com.southern.power.grid.dao.WeatherSiteAreaConfigurationMapper;
|
||||
import com.southern.power.grid.entity.NwSiteAreaConfiguration;
|
||||
import com.southern.power.grid.entity.WeatherSiteAreaConfiguration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 区域配置加载工具类
|
||||
*
|
||||
* 【功能说明】
|
||||
* - 管理南网区域配置和气象区域配置的加载与获取
|
||||
* - 使用 ThreadLocal 确保每个导入任务有独立的配置缓存,避免并发冲突
|
||||
* - 在导入任务开始前调用 loadAllConfigs() 加载配置
|
||||
*
|
||||
* 【使用场景】
|
||||
* - Excel 导入任务开始前调用 loadAllConfigs() 加载配置
|
||||
* - 业务处理中通过 getNwAreaMap() 和 getWeatherAreaMap() 获取配置
|
||||
*
|
||||
* 【线程安全】
|
||||
* - 使用 ThreadLocal 确保每个线程(导入任务)有独立的配置缓存
|
||||
* - 支持多个导入任务并发执行,不会互相干扰
|
||||
*
|
||||
**/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AreaConfigUtil {
|
||||
|
||||
@Resource
|
||||
private NwSiteAreaConfigurationMapper nwSiteAreaConfigurationMapper;
|
||||
|
||||
@Resource
|
||||
private WeatherSiteAreaConfigurationMapper weatherSiteAreaConfigurationMapper;
|
||||
|
||||
/**
|
||||
* 南网区域配置缓存(线程隔离)
|
||||
* key = nw_province + "|" + nw_city + "|" + nw_district
|
||||
* value = district_code
|
||||
*/
|
||||
private final ThreadLocal<Map<String, String>> nwAreaMapThreadLocal = ThreadLocal.withInitial(HashMap::new);
|
||||
|
||||
/**
|
||||
* 气象区域配置缓存(线程隔离)
|
||||
* key = district_code
|
||||
* value = WeatherSiteAreaConfiguration 列表(包含 station_id/station_name)
|
||||
*/
|
||||
private final ThreadLocal<Map<String, List<WeatherSiteAreaConfiguration>>> weatherAreaMapThreadLocal =
|
||||
ThreadLocal.withInitial(HashMap::new);
|
||||
|
||||
/**
|
||||
* 加载所有区域配置
|
||||
*
|
||||
* 【调用时机】
|
||||
* - 在导入任务开始前调用一次
|
||||
* - 每个导入任务调用一次,使用 ThreadLocal 隔离
|
||||
*/
|
||||
public void loadAllConfigs() {
|
||||
log.info("开始加载区域配置...");
|
||||
loadNwAreaConfig();
|
||||
loadWeatherAreaConfig();
|
||||
log.info("区域配置加载完成,南网配置: {} 条,气象配置: {} 条",
|
||||
getNwAreaMap().size(), getWeatherAreaMap().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载南网区划配置
|
||||
*/
|
||||
private void loadNwAreaConfig() {
|
||||
QueryWrapper<NwSiteAreaConfiguration> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("district_code", "nw_province", "nw_city", "nw_district")
|
||||
.isNotNull("district_code")
|
||||
.ne("district_code", "");
|
||||
List<NwSiteAreaConfiguration> list = nwSiteAreaConfigurationMapper.selectList(wrapper);
|
||||
if (list == null || list.isEmpty()) {
|
||||
log.warn("南网区域配置查询结果为空");
|
||||
nwAreaMapThreadLocal.set(new HashMap<>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, String> nwAreaMap = new HashMap<>((int)(list.size() / 0.75) + 1);
|
||||
for (NwSiteAreaConfiguration cfg : list) {
|
||||
String key = buildNwKey(cfg.getNwProvince(), cfg.getNwCity(), cfg.getNwDistrict());
|
||||
nwAreaMap.put(key, cfg.getDistrictCode());
|
||||
}
|
||||
nwAreaMapThreadLocal.set(nwAreaMap);
|
||||
log.debug("南网区划配置加载完成,共 {} 条", nwAreaMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载气象区划配置
|
||||
*/
|
||||
private void loadWeatherAreaConfig() {
|
||||
QueryWrapper<WeatherSiteAreaConfiguration> wrapper = new QueryWrapper<>();
|
||||
wrapper.select("district_code", "station_name", "station_id")
|
||||
.isNotNull("district_code")
|
||||
.ne("district_code", "");
|
||||
List<WeatherSiteAreaConfiguration> list = weatherSiteAreaConfigurationMapper.selectList(wrapper);
|
||||
if (list == null || list.isEmpty()) {
|
||||
log.warn("气象区域配置查询结果为空");
|
||||
weatherAreaMapThreadLocal.set(new HashMap<>(0));
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, List<WeatherSiteAreaConfiguration>> weatherAreaMap = new HashMap<>((int)(list.size() / 0.75) + 1);
|
||||
for (WeatherSiteAreaConfiguration cfg : list) {
|
||||
weatherAreaMap.computeIfAbsent(cfg.getDistrictCode(), k -> new ArrayList<>()).add(cfg);
|
||||
}
|
||||
weatherAreaMapThreadLocal.set(weatherAreaMap);
|
||||
log.debug("气象区划配置加载完成,共 {} 条", weatherAreaMap.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建南网配置 key
|
||||
*
|
||||
* @param province 省份
|
||||
* @param city 城市
|
||||
* @param district 区县
|
||||
* @return key 格式: province|city|district
|
||||
*/
|
||||
private String buildNwKey(String province, String city, String district) {
|
||||
return new StringBuilder(64)
|
||||
.append(province == null ? "" : province.trim())
|
||||
.append('|')
|
||||
.append(city == null ? "" : city.trim())
|
||||
.append('|')
|
||||
.append(district == null ? "" : district.trim())
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取南网区域配置 Map
|
||||
*
|
||||
* @return 南网区域配置
|
||||
*/
|
||||
public Map<String, String> getNwAreaMap() {
|
||||
return nwAreaMapThreadLocal.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取气象区域配置 Map
|
||||
*
|
||||
* @return 气象区域配置
|
||||
*/
|
||||
public Map<String, List<WeatherSiteAreaConfiguration>> getWeatherAreaMap() {
|
||||
return weatherAreaMapThreadLocal.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空配置(用于测试或需要重新加载的场景)
|
||||
*/
|
||||
public void clearConfigs() {
|
||||
log.info("清空区域配置缓存...");
|
||||
getNwAreaMap().clear();
|
||||
getWeatherAreaMap().clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理线程本地变量,防止内存泄漏
|
||||
* 应在导入任务完成后调用
|
||||
*/
|
||||
public void cleanup() {
|
||||
nwAreaMapThreadLocal.remove();
|
||||
weatherAreaMapThreadLocal.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取南网配置条数(用于监控)
|
||||
*
|
||||
* @return 南网配置条数
|
||||
*/
|
||||
public int getNwAreaMapSize() {
|
||||
return getNwAreaMap().size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取气象配置条数(用于监控)
|
||||
*
|
||||
* @return 气象配置条数
|
||||
*/
|
||||
public int getWeatherAreaMapSize() {
|
||||
return getWeatherAreaMap().size();
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user