perf(导入): 重构区域配置加载与天气数据查询逻辑

- 新增 AreaConfigUtil 工具类,使用 ThreadLocal 缓存区域配置,支持并发导入任务
- 重构 HourlyOutageExcelProcessService,按区县批量查询气象数据,减少数据库访问次数
- 优化数据处理流程,引入行上下文和区县时间范围聚合,提升处理效率
- 在导入任务开始前初始化配置,任务结束后清理 ThreadLocal 防止内存泄漏
This commit is contained in:
fsyud 2026-04-14 18:00:48 +08:00
parent 2f65f85143
commit 657e8c8375
4 changed files with 521 additions and 123 deletions

View File

@ -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 总条数只读行数不处理数据
*/

View File

@ -41,6 +41,9 @@ public class AsyncImportServiceImpl {
.set(ImportTask::getTotal, total)
.eq(ImportTask::getTaskNo, task.getTaskNo()));
// 初始化区域配置在导入任务开始前加载一次
dataExcelListener.initAreaConfigs();
// 注入任务
dataExcelListener.setTask(task);
inputStream.reset(); // 流被读过一次了重置一下

View File

@ -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,105 +32,48 @@ 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 (needSetRestored) {
row.setOutageState("已复电");
}
}
// 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);
}
}
}
if (rowContexts.isEmpty()) {
return;
}
// 3.2 按区县分批 IN 查询气象数据并建立区县+时次索引
Map<String, NavigableMap<LocalDateTime, RegionalWeatherData>> weatherIndex = loadWeatherIndexByDistrict(districtTimeRangeMap);
// 按区县天气索引组装分时事件并执行同时次聚合
assembleHourlyEvents(rowContexts, weatherIndex, toInsertMap, eventId);
if (!toInsertMap.isEmpty()) {
// Map 中的 values 转为 List 进行批量插入
List<DnerHourlyPowerOutageEvent> toInsert = new ArrayList<>(toInsertMap.values());
@ -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;
}
// 初始化每个区县的有序时间索引容器
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 String findDistrictCode(String nwProvince, String nwCity, String nwDistrict) {
String key = buildNwKey(nwProvince, nwCity, nwDistrict);
return nwAreaMap.get(key);
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;
}
}
}

View 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();
}
}