From 657e8c8375d6e905ee1bed5e1c0b019c493743cb Mon Sep 17 00:00:00 2001 From: fsyud Date: Tue, 14 Apr 2026 18:00:48 +0800 Subject: [PATCH] =?UTF-8?q?perf(=E5=AF=BC=E5=85=A5):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=8C=BA=E5=9F=9F=E9=85=8D=E7=BD=AE=E5=8A=A0=E8=BD=BD=E4=B8=8E?= =?UTF-8?q?=E5=A4=A9=E6=B0=94=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AreaConfigUtil 工具类,使用 ThreadLocal 缓存区域配置,支持并发导入任务 - 重构 HourlyOutageExcelProcessService,按区县批量查询气象数据,减少数据库访问次数 - 优化数据处理流程,引入行上下文和区县时间范围聚合,提升处理效率 - 在导入任务开始前初始化配置,任务结束后清理 ThreadLocal 防止内存泄漏 --- .../grid/listener/DataExcelListener.java | 22 + .../service/impl/AsyncImportServiceImpl.java | 3 + .../impl/HourlyOutageExcelProcessService.java | 427 +++++++++++++----- .../power/grid/utils/AreaConfigUtil.java | 192 ++++++++ 4 files changed, 521 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java diff --git a/src/main/java/com/southern/power/grid/listener/DataExcelListener.java b/src/main/java/com/southern/power/grid/listener/DataExcelListener.java index 86bfb0a..aa02177 100644 --- a/src/main/java/com/southern/power/grid/listener/DataExcelListener.java +++ b/src/main/java/com/southern/power/grid/listener/DataExcelListener.java @@ -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 { @Autowired private IDnerDailyPowerOutageEventSyncService dailyPowerOutageEventSycnService; + @Autowired + private AreaConfigUtil areaConfigUtil; + // 注入任务 @Setter private ImportTask task; @@ -101,6 +105,9 @@ public class DataExcelListener extends AnalysisEventListener { success = 0; fail = 0; task = null; + + // 清理 ThreadLocal 变量,防止内存泄漏 + areaConfigUtil.cleanup(); } // ==================== 核心:分批插入 + 手动事务 ==================== @@ -127,6 +134,21 @@ public class DataExcelListener extends AnalysisEventListener { .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 总条数(只读行数,不处理数据) */ diff --git a/src/main/java/com/southern/power/grid/service/impl/AsyncImportServiceImpl.java b/src/main/java/com/southern/power/grid/service/impl/AsyncImportServiceImpl.java index eac47d1..16d9d5f 100644 --- a/src/main/java/com/southern/power/grid/service/impl/AsyncImportServiceImpl.java +++ b/src/main/java/com/southern/power/grid/service/impl/AsyncImportServiceImpl.java @@ -41,6 +41,9 @@ public class AsyncImportServiceImpl { .set(ImportTask::getTotal, total) .eq(ImportTask::getTaskNo, task.getTaskNo())); + // 初始化区域配置(在导入任务开始前加载一次) + dataExcelListener.initAreaConfigs(); + // 注入任务 dataExcelListener.setTask(task); inputStream.reset(); // 流被读过一次了,重置一下 diff --git a/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java b/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java index 57f359d..4f5ccb5 100644 --- a/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java +++ b/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java @@ -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 nwAreaMap = new HashMap<>(); - - /** - * 气象区域配置 - * key = district_code ;value = WeatherSiteAreaConfiguration(包含 station_id/station_name) - */ - private final Map> 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 excelRows, Long eventId) { - // 1. 加载南网区划配置表 - loadNwAreaConfig(); - // 2. 加载气象区划配置表 - loadWeatherAreaConfig(); - + if (ObjectUtils.isEmpty(excelRows)) { + return; + } // 使用 Map 存储待插入实体,Key 为 districtCode + "|" + dataTime Map toInsertMap = new HashMap<>(); + // 区域编码缓存:相同省市区只查一次映射 + Map districtCodeCache = new HashMap<>(); + // 行与区县编码映射:避免二次重复计算 + List rowContexts = new ArrayList<>(excelRows.size()); + // 每个区县对应的气象查询时间范围 + Map districtTimeRangeMap = new HashMap<>(); - // 3 & 4. 循环处理 Excel 行 - for (DataExcelEntity row : excelRows) { -// if(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 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> weatherIndex = loadWeatherIndexByDistrict(districtTimeRangeMap); + + // 按区县天气索引组装分时事件,并执行同时次聚合 + assembleHourlyEvents(rowContexts, weatherIndex, toInsertMap, eventId); + if (!toInsertMap.isEmpty()) { // 将 Map 中的 values 转为 List 进行批量插入 List toInsert = new ArrayList<>(toInsertMap.values()); @@ -153,53 +94,249 @@ public class HourlyOutageExcelProcessService { } /** - * 加载南网区划配置 + * 标准化单行停电数据:初始化分类用户数、补齐结束时间、按规则更新停电状态。 * - **/ - private void loadNwAreaConfig() { - nwAreaMap.clear(); - List 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 list = weatherSiteAreaConfigurationMapper.selectAll(); - if (list == null) return; - for (WeatherSiteAreaConfiguration cfg : list) { - if (cfg.getDistrictCode() != null && !cfg.getDistrictCode().isEmpty()) { - List 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 excelRows, + Map districtCodeCache, + List rowContexts, + Map districtTimeRangeMap) { + // 遍历每一行:预处理 -> 校验 -> 映射区县 -> 产出上下文 + for (DataExcelEntity row : excelRows) { +// if(row.getLengthOutage() mergeTimeRange(v, row.getStartTime(), row.getEndTime())); + } + } + + /** + * 基于“区县-时间”气象索引构建分时停电事件,并按“区县+时次”做用户数聚合。 + * + * @param rowContexts 有效停电行上下文(含区县编码) + * @param weatherIndex 区县维度天气有序索引 + * @param toInsertMap 输出参数:待入库事件(key=区县编码|dataTime) + * @param eventId 事件批次ID + */ + private void assembleHourlyEvents(List rowContexts, + Map> weatherIndex, + Map toInsertMap, + Long eventId) { + // 遍历有效停电行,按停电时间窗命中对应整时天气数据 + for (RowContext context : rowContexts) { + DataExcelEntity row = context.getRow(); + String districtCode = context.getDistrictCode(); + NavigableMap 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 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> loadWeatherIndexByDistrict( + Map districtTimeRangeMap) { + // 预分配容量,减少 Map 扩容开销 + Map> weatherIndex = new HashMap<>(districtTimeRangeMap.size()); + if (ObjectUtils.isEmpty(districtTimeRangeMap)) { + return weatherIndex; + } + + // 初始化每个区县的有序时间索引容器 + List 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 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 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 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 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; + } + } } diff --git a/src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java b/src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java new file mode 100644 index 0000000..d2426bc --- /dev/null +++ b/src/main/java/com/southern/power/grid/utils/AreaConfigUtil.java @@ -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> nwAreaMapThreadLocal = ThreadLocal.withInitial(HashMap::new); + + /** + * 气象区域配置缓存(线程隔离) + * key = district_code + * value = WeatherSiteAreaConfiguration 列表(包含 station_id/station_name) + */ + private final ThreadLocal>> weatherAreaMapThreadLocal = + ThreadLocal.withInitial(HashMap::new); + + /** + * 加载所有区域配置 + * + * 【调用时机】 + * - 在导入任务开始前调用一次 + * - 每个导入任务调用一次,使用 ThreadLocal 隔离 + */ + public void loadAllConfigs() { + log.info("开始加载区域配置..."); + loadNwAreaConfig(); + loadWeatherAreaConfig(); + log.info("区域配置加载完成,南网配置: {} 条,气象配置: {} 条", + getNwAreaMap().size(), getWeatherAreaMap().size()); + } + + /** + * 加载南网区划配置 + */ + private void loadNwAreaConfig() { + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.select("district_code", "nw_province", "nw_city", "nw_district") + .isNotNull("district_code") + .ne("district_code", ""); + List list = nwSiteAreaConfigurationMapper.selectList(wrapper); + if (list == null || list.isEmpty()) { + log.warn("南网区域配置查询结果为空"); + nwAreaMapThreadLocal.set(new HashMap<>(0)); + return; + } + + Map 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 wrapper = new QueryWrapper<>(); + wrapper.select("district_code", "station_name", "station_id") + .isNotNull("district_code") + .ne("district_code", ""); + List list = weatherSiteAreaConfigurationMapper.selectList(wrapper); + if (list == null || list.isEmpty()) { + log.warn("气象区域配置查询结果为空"); + weatherAreaMapThreadLocal.set(new HashMap<>(0)); + return; + } + + Map> 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 getNwAreaMap() { + return nwAreaMapThreadLocal.get(); + } + + /** + * 获取气象区域配置 Map + * + * @return 气象区域配置 + */ + public Map> 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(); + } +}