From e358a01ee7af177df8e515c657fd7b5f3c6a44fa Mon Sep 17 00:00:00 2001 From: sansenhoshi Date: Wed, 1 Jul 2026 11:52:27 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E5=90=8D=E7=A7=B0=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=EF=BC=8C=E6=80=A7=E8=83=BD=E4=BC=98=E5=8C=96=EF=BC=8C?= =?UTF-8?q?=E7=BB=93=E6=9E=84=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.zh-CN.md | 4 +- pom.xml | 2 +- .../common/aspect/DataFilterAspect.java | 8 +- .../handler/FieldMetaObjectHandler.java | 25 +++- .../interceptor/DataFilterInterceptor.java | 19 ++- .../modules/security/oauth2/Oauth2Filter.java | 4 +- .../modules/security/user/SecurityUser.java | 13 +- .../modules/sys/alert/SseAlertService.java | 5 +- .../service/impl/SysDictTypeServiceImpl.java | 16 ++- .../dailydata/WeatherDataImportManager.java | 57 +++++--- .../dailydata/WeatherDataListener.java | 5 + .../WeatherDailyDataController.java | 27 ++-- .../dailydata/dao/WeatherDailyDataDao.java | 13 +- .../weather/dailydata/dto/ImportProgress.java | 16 ++- .../impl/WeatherDailyDataServiceImpl.java | 18 ++- .../controller/FileDownloadController.java | 75 ++++++++--- .../WeatherFileScanRecordServiceImpl.java | 28 +++- .../service/WeatherStationService.java | 6 + .../impl/WeatherStationServiceImpl.java | 89 ++++++++++++- .../dailyweather/WeatherDailyDataDao.xml | 7 +- .../com/weather/common/redis/RedisKeys.java | 14 ++ weather-data-ui/index.html | 2 +- .../src/components/alert-marquee/index.vue | 8 ++ .../src/components/wang-editor/index.vue | 22 ++-- .../src/composables/useAlertMarquee.ts | 15 ++- weather-data-ui/src/hooks/useView.ts | 41 +----- .../src/layout/header/base-header.vue | 31 ++--- weather-data-ui/src/layout/index.vue | 30 +++-- weather-data-ui/src/types/interface.ts | 8 -- weather-data-ui/src/utils/cache.ts | 3 +- weather-data-ui/src/utils/chartBuilder.ts | 2 - weather-data-ui/src/utils/http.ts | 17 +-- .../views/dailyweather/weatherdailydata.vue | 49 ++++--- weather-data-ui/src/views/login.vue | 3 +- .../src/views/weather/realtime-monitoring.vue | 124 ++++++++++++++++-- 35 files changed, 567 insertions(+), 239 deletions(-) delete mode 100644 weather-data-ui/src/utils/chartBuilder.ts diff --git a/README.zh-CN.md b/README.zh-CN.md index b0d3579..f5af2e0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,6 +1,6 @@ -# 气象数据管理系统 +# 气象数据平台 -基于 Spring Boot 3.5 与 Vue 3 的全栈气象数据管理与分析平台。 +基于 Spring Boot 3.5 与 Vue 3 的全栈气象数据平台。 > 开发者文档:[CLAUDE.md](CLAUDE.md) | 模块文档:[system-admin](system-admin/CLAUDE.md) | [system-common](system-common/CLAUDE.md) | [system-dynamic-datasource](system-dynamic-datasource/CLAUDE.md) | [weather-data-ui](weather-data-ui/CLAUDE.md) > diff --git a/pom.xml b/pom.xml index afed1f4..c0bf7d3 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ pom weather-data - 气象数据管理系统 + 气象数据平台 org.springframework.boot diff --git a/system-admin/src/main/java/com/weather/common/aspect/DataFilterAspect.java b/system-admin/src/main/java/com/weather/common/aspect/DataFilterAspect.java index 334f5f2..cc62476 100644 --- a/system-admin/src/main/java/com/weather/common/aspect/DataFilterAspect.java +++ b/system-admin/src/main/java/com/weather/common/aspect/DataFilterAspect.java @@ -3,7 +3,7 @@ package com.weather.common.aspect; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.StrUtil; -import com.qiniu.util.StringUtils; +import cn.hutool.core.util.StrUtil; import com.weather.common.annotation.DataFilter; import com.weather.common.constant.Constant; import com.weather.common.exception.ErrorCode; @@ -12,6 +12,7 @@ import com.weather.common.interceptor.DataScope; import com.weather.modules.security.user.SecurityUser; import com.weather.modules.security.user.UserDetail; import com.weather.modules.sys.enums.SuperAdminEnum; +import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @@ -28,6 +29,7 @@ import java.util.Map; * * @author 123 */ +@Slf4j @Aspect @Component public class DataFilterAspect { @@ -54,7 +56,7 @@ public class DataFilterAspect { String sqlFilter = getSqlFilter(user, point); map.put(Constant.SQL_FILTER, new DataScope(sqlFilter)); } catch (Exception e) { - + log.error("数据过滤SQL生成失败", e); } return; @@ -85,7 +87,7 @@ public class DataFilterAspect { if (CollUtil.isNotEmpty(deptIdList)) { sqlFilter.append(tableAlias).append(dataFilter.deptId()); - sqlFilter.append(" in(").append(StringUtils.join(deptIdList, ",")).append(")"); + sqlFilter.append(" in(").append(StrUtil.join(",", deptIdList)).append(")"); } //查询本人数据 diff --git a/system-admin/src/main/java/com/weather/common/handler/FieldMetaObjectHandler.java b/system-admin/src/main/java/com/weather/common/handler/FieldMetaObjectHandler.java index 422a64a..5537167 100644 --- a/system-admin/src/main/java/com/weather/common/handler/FieldMetaObjectHandler.java +++ b/system-admin/src/main/java/com/weather/common/handler/FieldMetaObjectHandler.java @@ -5,6 +5,7 @@ package com.weather.common.handler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.weather.modules.security.user.SecurityUser; import com.weather.modules.security.user.UserDetail; +import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; @@ -15,6 +16,7 @@ import java.util.Date; * * @author 123 */ +@Slf4j @Component public class FieldMetaObjectHandler implements MetaObjectHandler { private final static String CREATE_DATE = "createDate"; @@ -28,24 +30,35 @@ public class FieldMetaObjectHandler implements MetaObjectHandler { UserDetail user = SecurityUser.getUser(); Date date = new Date(); - //创建者 - strictInsertFill(metaObject, CREATOR, Long.class, user.getId()); + //创建者(用户ID为空时跳过填充,避免写入无效数据) + if (user.getId() != null) { + strictInsertFill(metaObject, CREATOR, Long.class, user.getId()); + } else { + log.warn("insertFill: 用户ID为空,跳过creator/deptId/updater字段填充"); + } //创建时间 strictInsertFill(metaObject, CREATE_DATE, Date.class, date); //创建者所属部门 - strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId()); + if (user.getDeptId() != null) { + strictInsertFill(metaObject, DEPT_ID, Long.class, user.getDeptId()); + } //更新者 - strictInsertFill(metaObject, UPDATER, Long.class, user.getId()); + if (user.getId() != null) { + strictInsertFill(metaObject, UPDATER, Long.class, user.getId()); + } //更新时间 strictInsertFill(metaObject, UPDATE_DATE, Date.class, date); } @Override public void updateFill(MetaObject metaObject) { - //更新者 - strictUpdateFill(metaObject, UPDATER, Long.class, SecurityUser.getUserId()); + Long userId = SecurityUser.getUserId(); + //更新者(用户ID为空时跳过填充) + if (userId != null) { + strictUpdateFill(metaObject, UPDATER, Long.class, userId); + } //更新时间 strictUpdateFill(metaObject, UPDATE_DATE, Date.class, new Date()); } diff --git a/system-admin/src/main/java/com/weather/common/interceptor/DataFilterInterceptor.java b/system-admin/src/main/java/com/weather/common/interceptor/DataFilterInterceptor.java index 0cabe84..04159a1 100644 --- a/system-admin/src/main/java/com/weather/common/interceptor/DataFilterInterceptor.java +++ b/system-admin/src/main/java/com/weather/common/interceptor/DataFilterInterceptor.java @@ -5,9 +5,9 @@ package com.weather.common.interceptor; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; +import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.Expression; -import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.select.PlainSelect; @@ -25,6 +25,7 @@ import java.util.Map; * * @author 123 */ +@Slf4j public class DataFilterInterceptor implements InnerInterceptor { @Override @@ -67,16 +68,20 @@ public class DataFilterInterceptor implements InnerInterceptor { Select select = (Select) CCJSqlParserUtil.parse(buildSql); PlainSelect plainSelect = (PlainSelect) select.getSelectBody(); + // 将过滤条件解析为真实的 SQL 表达式,而非 StringValue 字符串字面量 + Expression filterExpression = CCJSqlParserUtil.parseExpression(scope.getSqlFilter()); + Expression expression = plainSelect.getWhere(); - if(expression == null){ - plainSelect.setWhere(new StringValue(scope.getSqlFilter())); - }else{ - AndExpression andExpression = new AndExpression(expression, new StringValue(scope.getSqlFilter())); + if (expression == null) { + plainSelect.setWhere(filterExpression); + } else { + AndExpression andExpression = new AndExpression(expression, filterExpression); plainSelect.setWhere(andExpression); } - return select.toString().replaceAll("'", ""); - }catch (JSQLParserException e){ + return select.toString(); + } catch (JSQLParserException e) { + log.error("数据过滤SQL解析失败,回退到原始SQL", e); return buildSql; } } diff --git a/system-admin/src/main/java/com/weather/modules/security/oauth2/Oauth2Filter.java b/system-admin/src/main/java/com/weather/modules/security/oauth2/Oauth2Filter.java index 742a1e7..05d50ed 100644 --- a/system-admin/src/main/java/com/weather/modules/security/oauth2/Oauth2Filter.java +++ b/system-admin/src/main/java/com/weather/modules/security/oauth2/Oauth2Filter.java @@ -11,6 +11,7 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.web.filter.authc.AuthenticatingFilter; @@ -24,6 +25,7 @@ import java.io.IOException; * * @author 123 */ +@Slf4j public class Oauth2Filter extends AuthenticatingFilter { @Override @@ -81,7 +83,7 @@ public class Oauth2Filter extends AuthenticatingFilter { String json = JsonUtils.toJsonString(r); httpResponse.getWriter().print(json); } catch (IOException e1) { - + log.error("登录失败响应写入异常", e1); } return false; diff --git a/system-admin/src/main/java/com/weather/modules/security/user/SecurityUser.java b/system-admin/src/main/java/com/weather/modules/security/user/SecurityUser.java index 6a06c83..72d2f40 100644 --- a/system-admin/src/main/java/com/weather/modules/security/user/SecurityUser.java +++ b/system-admin/src/main/java/com/weather/modules/security/user/SecurityUser.java @@ -2,8 +2,11 @@ package com.weather.modules.security.user; +import com.weather.modules.sys.enums.SuperAdminEnum; import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * 用户 @@ -12,6 +15,8 @@ import org.apache.shiro.subject.Subject; */ public class SecurityUser { + private static final Logger log = LoggerFactory.getLogger(SecurityUser.class); + public static Subject getSubject() { try { return SecurityUtils.getSubject(); @@ -32,7 +37,13 @@ public class SecurityUser { } } UserDetail fallback = UserContextHolder.get(); - return fallback != null ? fallback : new UserDetail(); + if (fallback != null) { + return fallback; + } + log.warn("无法获取当前用户信息,Subject和ThreadLocal均为空,返回空UserDetail"); + UserDetail empty = new UserDetail(); + empty.setSuperAdmin(SuperAdminEnum.NO.value()); + return empty; } /** diff --git a/system-admin/src/main/java/com/weather/modules/sys/alert/SseAlertService.java b/system-admin/src/main/java/com/weather/modules/sys/alert/SseAlertService.java index 4f51dbe..60a3d29 100644 --- a/system-admin/src/main/java/com/weather/modules/sys/alert/SseAlertService.java +++ b/system-admin/src/main/java/com/weather/modules/sys/alert/SseAlertService.java @@ -24,12 +24,15 @@ public class SseAlertService { private final Set emitters = new CopyOnWriteArraySet<>(); + /** SSE 连接超时时间(30分钟),避免客户端异常断开后连接泄漏 */ + private static final long SSE_TIMEOUT_MS = 30 * 60 * 1000L; + /** * 创建并注册一个新的 SSE 连接。 * 绑定清理回调:连接完成/超时/异常时自动移除。 */ public SseEmitter createEmitter() { - SseEmitter emitter = new SseEmitter(0L); // 无超时 + SseEmitter emitter = new SseEmitter(SSE_TIMEOUT_MS); emitters.add(emitter); log.info("SSE 连接已建立,当前连接数: {}", emitters.size()); diff --git a/system-admin/src/main/java/com/weather/modules/sys/service/impl/SysDictTypeServiceImpl.java b/system-admin/src/main/java/com/weather/modules/sys/service/impl/SysDictTypeServiceImpl.java index 5d7791f..bbab37c 100644 --- a/system-admin/src/main/java/com/weather/modules/sys/service/impl/SysDictTypeServiceImpl.java +++ b/system-admin/src/main/java/com/weather/modules/sys/service/impl/SysDictTypeServiceImpl.java @@ -18,9 +18,8 @@ import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.Arrays; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** * 字典类型 @@ -87,11 +86,14 @@ public class SysDictTypeServiceImpl extends BaseServiceImpl getAllList() { List typeList = baseDao.getDictTypeList(); List dataList = sysDictDataDao.getDictDataList(); + + // 按 dictTypeId 分组,O(n+m) 替代 O(n*m) 嵌套循环 + Map> dataMap = dataList.stream() + .collect(Collectors.groupingBy(DictData::getDictTypeId)); for (DictType type : typeList) { - for (DictData data : dataList) { - if (type.getId().equals(data.getDictTypeId())) { - type.getDataList().add(data); - } + List matched = dataMap.get(type.getId()); + if (matched != null) { + type.getDataList().addAll(matched); } } return typeList; diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataImportManager.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataImportManager.java index 290314a..f55dff9 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataImportManager.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataImportManager.java @@ -1,8 +1,6 @@ package com.weather.modules.weather.dailydata; import com.alibaba.excel.EasyExcel; -import com.alibaba.excel.context.AnalysisContext; -import com.alibaba.excel.event.AnalysisEventListener; import com.weather.modules.weather.dailydata.dto.ImportProgress; import com.weather.modules.weather.dailydata.service.WeatherDailyDataService; import com.weather.modules.weather.dailydata.vo.WeatherExcelVO; @@ -13,6 +11,7 @@ import com.weather.common.redis.RedisKeys; import com.weather.common.redis.RedisUtils; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; @@ -22,6 +21,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; @Slf4j @Component @@ -30,6 +30,10 @@ public class WeatherDataImportManager { private final WeatherDailyDataService weatherDailyDataService; private final RedisUtils redisUtils; + + /** 已完成/失败任务的保留时间(毫秒) */ + private static final long TASK_TTL_MS = TimeUnit.HOURS.toMillis(1); + private final Map tasks = new ConcurrentHashMap<>(); public String submitImport(MultipartFile file) throws IOException { @@ -67,23 +71,14 @@ public class WeatherDataImportManager { private void processImport(String taskId, File tempFile) { ImportProgress progress = tasks.get(taskId); - AnalysisEventListener countListener = new AnalysisEventListener<>() { - @Override - public void invoke(WeatherExcelVO data, AnalysisContext context) { - progress.setTotalRows(progress.getTotalRows() + 1); - } - @Override - public void doAfterAllAnalysed(AnalysisContext context) { - log.info("任务 {} 共 {} 行", taskId, progress.getTotalRows()); - } - }; - EasyExcel.read(tempFile, WeatherExcelVO.class, countListener).sheet().doRead(); - - EasyExcel.read(tempFile, WeatherExcelVO.class, new WeatherDataListener(weatherDailyDataService, progress)) + // 单次读取:WeatherDataListener 同时负责计数和入库,避免双次 I/O + EasyExcel.read(tempFile, WeatherExcelVO.class, + new WeatherDataListener(weatherDailyDataService, progress)) .sheet() .doRead(); progress.setStatus("COMPLETED"); + progress.setCompletedAt(System.currentTimeMillis()); log.info("导入任务 {} 完成,共导入 {} 行", taskId, progress.getProcessedRows().get()); // 导入完成后清除天气汇总缓存,等待下次定时任务刷新 @@ -96,6 +91,36 @@ public class WeatherDataImportManager { } public ImportProgress getProgress(String taskId) { - return tasks.get(taskId); + ImportProgress progress = tasks.get(taskId); + if (progress != null && isExpired(progress)) { + tasks.remove(taskId); + return null; + } + return progress; + } + + private boolean isExpired(ImportProgress progress) { + if ("PROCESSING".equals(progress.getStatus())) { + return false; + } + Long completedAt = progress.getCompletedAt(); + return completedAt != null && System.currentTimeMillis() - completedAt > TASK_TTL_MS; + } + + /** + * 定时清理过期任务,每30分钟执行一次 + */ + @Scheduled(fixedRate = 30 * 60 * 1000) + public void cleanExpiredTasks() { + int removed = 0; + for (Map.Entry entry : tasks.entrySet()) { + if (isExpired(entry.getValue())) { + tasks.remove(entry.getKey()); + removed++; + } + } + if (removed > 0) { + log.info("清理过期导入任务 {} 个,当前剩余 {} 个", removed, tasks.size()); + } } } diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataListener.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataListener.java index 8fcef1b..9f9b854 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataListener.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/WeatherDataListener.java @@ -35,6 +35,11 @@ public class WeatherDataListener extends AnalysisEventListener { @Override public void invoke(WeatherExcelVO data, AnalysisContext context) { + // 同时计数(替代独立的 countListener,避免双次读取) + if (progress != null) { + progress.incrementTotalRows(); + } + WeatherDailyDataEntity entity = ConvertUtils.sourceToTarget(data, WeatherDailyDataEntity.class); try { diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/controller/WeatherDailyDataController.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/controller/WeatherDailyDataController.java index ca211f8..d7f71e1 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/controller/WeatherDailyDataController.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/controller/WeatherDailyDataController.java @@ -30,9 +30,8 @@ import io.swagger.v3.oas.annotations.Parameters; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.multipart.MultipartFile; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** @@ -66,13 +65,21 @@ public class WeatherDailyDataController { @RequiresPermissions("dailyweather:weatherdailydata:page") public Result> page(@Parameter(hidden = true) @RequestParam Map params){ PageData page = weatherDailyDataService.page(params); - page.getList().forEach(item -> { - Long stationId = item.getStationId(); - WeatherStationDTO station = weatherStationService.getByStationCode(stationId); - if (station != null) { - item.setStationName(station.getStationName()); - } - }); + + // 批量查询站点名称,避免 N+1 问题 + Set stationIds = page.getList().stream() + .map(WeatherDailyDataDto::getStationId) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + if (!stationIds.isEmpty()) { + Map stationMap = weatherStationService.getByStationCodes(stationIds); + page.getList().forEach(item -> { + WeatherStationDTO station = stationMap.get(item.getStationId()); + if (station != null) { + item.setStationName(station.getStationName()); + } + }); + } return new Result>().ok(page); } diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/dao/WeatherDailyDataDao.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/dao/WeatherDailyDataDao.java index 3a84bf9..4e011bf 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/dao/WeatherDailyDataDao.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/dao/WeatherDailyDataDao.java @@ -22,18 +22,13 @@ public interface WeatherDailyDataDao extends BaseDao { List selectSummarizeList(@Param(Constants.WRAPPER) LambdaQueryWrapper wrapper); /** - * 按站点 + 月-日 + 年份范围查询汇总数据 + * 按站点 + 精确日期列表查询汇总数据(缓存未命中降级路径) + * 使用等值 IN 查询命中 uk_station_date 唯一索引,避免 MONTH/DAY/YEAR 函数导致全表扫描 * @param stationIds 站点 ID 列表 - * @param month 月份 1-12 - * @param day 日 1-31 - * @param startYear 起始年份 - * @param endYear 结束年份 + * @param dates 精确日期列表,格式 yyyy-MM-dd,如 ["2020-07-01","2021-07-01"] */ List selectSummarizeByMonthDay(@Param("stationIds") List stationIds, - @Param("month") int month, - @Param("day") int day, - @Param("startYear") int startYear, - @Param("endYear") int endYear); + @Param("dates") List dates); int insertBatchMultiRow(@Param("list") List list); } \ No newline at end of file diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/dto/ImportProgress.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/dto/ImportProgress.java index ecffb73..d61dde8 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/dto/ImportProgress.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/dto/ImportProgress.java @@ -7,12 +7,26 @@ import java.util.concurrent.atomic.AtomicInteger; @Data public class ImportProgress { private String taskId; - private volatile int totalRows; + private final AtomicInteger totalRows = new AtomicInteger(0); private final AtomicInteger processedRows = new AtomicInteger(0); private volatile String status; private volatile String errorMessage; + /** 任务完成时间戳,用于过期清理 */ + private volatile Long completedAt; public ImportProgress(String taskId) { this.taskId = taskId; } + + public int getTotalRows() { + return totalRows.get(); + } + + public void setTotalRows(int totalRows) { + this.totalRows.set(totalRows); + } + + public void incrementTotalRows() { + totalRows.incrementAndGet(); + } } diff --git a/system-admin/src/main/java/com/weather/modules/weather/dailydata/service/impl/WeatherDailyDataServiceImpl.java b/system-admin/src/main/java/com/weather/modules/weather/dailydata/service/impl/WeatherDailyDataServiceImpl.java index 9ff69e8..31edd8c 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/dailydata/service/impl/WeatherDailyDataServiceImpl.java +++ b/system-admin/src/main/java/com/weather/modules/weather/dailydata/service/impl/WeatherDailyDataServiceImpl.java @@ -242,18 +242,22 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl用 MONTH/DAY 定位具体日期 + YEAR BETWEEN 限定年份范围,避免日期 BETWEEN 全量问题

+ * 直接查询数据库获取汇总数据(缓存未命中降级路径)。 + *

生成精确日期列表使用 IN 等值查询,命中 uk_station_date 唯一索引, + * 避免 MONTH()/DAY()/YEAR() 函数导致全表扫描。

*/ private List querySummarizeFromDb( List stationIds, LocalDate startTime, LocalDate endTime) { + int month = startTime.getMonthValue(); + int day = startTime.getDayOfMonth(); + List dates = new ArrayList<>(); + for (int y = startTime.getYear(); y <= endTime.getYear(); y++) { + dates.add(String.format("%04d-%02d-%02d", y, month, day)); + } + List list = weatherDailyDataDao.selectSummarizeByMonthDay( - stationIds, - startTime.getMonthValue(), - startTime.getDayOfMonth(), - startTime.getYear(), - endTime.getYear()); + stationIds, dates); return BeanUtil.copyToList(list, DailyWeatherSummarizeDto.class); } diff --git a/system-admin/src/main/java/com/weather/modules/weather/filescan/controller/FileDownloadController.java b/system-admin/src/main/java/com/weather/modules/weather/filescan/controller/FileDownloadController.java index eeb8cf5..072d3ce 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/filescan/controller/FileDownloadController.java +++ b/system-admin/src/main/java/com/weather/modules/weather/filescan/controller/FileDownloadController.java @@ -1,13 +1,15 @@ package com.weather.modules.weather.filescan.controller; -import com.weather.common.constant.Constant; +import cn.hutool.core.util.StrUtil; import com.weather.modules.weather.filescan.dto.WeatherFileScanRecordDTO; import com.weather.modules.weather.filescan.service.WeatherFileScanRecordService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; +import jakarta.servlet.http.HttpServletRequest; import org.apache.shiro.authz.annotation.RequiresPermissions; import org.springframework.core.io.FileSystemResource; +import org.springframework.http.CacheControl; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -16,6 +18,10 @@ import org.springframework.web.bind.annotation.*; import java.io.File; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.TimeUnit; @RestController @RequestMapping("filescan/file") @@ -25,10 +31,17 @@ public class FileDownloadController { @Resource private WeatherFileScanRecordService weatherFileScanRecordService; + /** 文件缓存最大有效期(小时),图片类文件变更频率极低 */ + private static final long FILE_CACHE_MAX_AGE_HOURS = 1; + /** 文件缓存有效期(天),用于 Last-Modified 比较 */ + private static final long FILE_CACHE_MAX_AGE_DAYS = 7; + @GetMapping("display/{id}") @Operation(summary = "获取展示文件") @RequiresPermissions("filescan:record:display") - public ResponseEntity displayFile(@PathVariable("id") Long id) { + public ResponseEntity displayFile( + @PathVariable("id") Long id, HttpServletRequest request) { + WeatherFileScanRecordDTO record = weatherFileScanRecordService.get(id); if (record == null || record.getDisplayPath() == null) { return ResponseEntity.notFound().build(); @@ -39,25 +52,57 @@ public class FileDownloadController { return ResponseEntity.notFound().build(); } - FileSystemResource resource = new FileSystemResource(file); - String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; - String ext = record.getFileExt(); - if (ext == null) { - // fall through to default - } else if ("png".equalsIgnoreCase(ext)) { - contentType = MediaType.IMAGE_PNG_VALUE; - } else if ("gif".equalsIgnoreCase(ext)) { - contentType = MediaType.IMAGE_GIF_VALUE; - } else if ("jpg".equalsIgnoreCase(ext) || "jpeg".equalsIgnoreCase(ext)) { - contentType = MediaType.IMAGE_JPEG_VALUE; - } else if ("txt".equalsIgnoreCase(ext)) { - contentType = MediaType.TEXT_PLAIN_VALUE; + // ETag 使用文件 MD5 hash,扫描时已计算 + String eTag = "\"" + (StrUtil.isNotBlank(record.getMd5Hash()) ? record.getMd5Hash() : file.lastModified()) + "\""; + long lastModified = file.lastModified(); + + // 条件请求:If-None-Match(ETag 匹配则返回 304) + String ifNoneMatch = request.getHeader(HttpHeaders.IF_NONE_MATCH); + if (StrUtil.isNotBlank(ifNoneMatch) && ifNoneMatch.equals(eTag)) { + return ResponseEntity.status(304) + .eTag(eTag) + .cacheControl(CacheControl.maxAge(FILE_CACHE_MAX_AGE_HOURS, TimeUnit.HOURS).cachePublic()) + .build(); } + // 条件请求:If-Modified-Since(文件未修改则返回 304) + long ifModifiedSince = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); + if (ifModifiedSince > 0 && lastModified / 1000 <= ifModifiedSince / 1000) { + return ResponseEntity.status(304) + .eTag(eTag) + .lastModified(lastModified) + .cacheControl(CacheControl.maxAge(FILE_CACHE_MAX_AGE_HOURS, TimeUnit.HOURS).cachePublic()) + .build(); + } + + FileSystemResource resource = new FileSystemResource(file); + String contentType = resolveContentType(record.getFileExt()); + return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType)) .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + URLEncoder.encode(record.getFileName(), StandardCharsets.UTF_8) + "\"") + .eTag(eTag) + .lastModified(lastModified) + .cacheControl(CacheControl.maxAge(FILE_CACHE_MAX_AGE_HOURS, TimeUnit.HOURS) + .cachePublic() + .staleWhileRevalidate(FILE_CACHE_MAX_AGE_DAYS, TimeUnit.DAYS)) .body(resource); } + + private String resolveContentType(String ext) { + if (ext == null) return MediaType.APPLICATION_OCTET_STREAM_VALUE; + return switch (ext.toLowerCase()) { + case "png" -> MediaType.IMAGE_PNG_VALUE; + case "gif" -> MediaType.IMAGE_GIF_VALUE; + case "jpg", "jpeg" -> MediaType.IMAGE_JPEG_VALUE; + case "svg" -> "image/svg+xml"; + case "webp" -> "image/webp"; + case "bmp" -> "image/bmp"; + case "txt" -> MediaType.TEXT_PLAIN_VALUE; + case "json" -> MediaType.APPLICATION_JSON_VALUE; + case "pdf" -> MediaType.APPLICATION_PDF_VALUE; + default -> MediaType.APPLICATION_OCTET_STREAM_VALUE; + }; + } } diff --git a/system-admin/src/main/java/com/weather/modules/weather/filescan/service/impl/WeatherFileScanRecordServiceImpl.java b/system-admin/src/main/java/com/weather/modules/weather/filescan/service/impl/WeatherFileScanRecordServiceImpl.java index f8ca567..60efa74 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/filescan/service/impl/WeatherFileScanRecordServiceImpl.java +++ b/system-admin/src/main/java/com/weather/modules/weather/filescan/service/impl/WeatherFileScanRecordServiceImpl.java @@ -20,7 +20,7 @@ import com.weather.modules.weather.filescan.dto.ModelFileItemVO; import com.weather.modules.weather.filescan.dto.WeatherFileScanRecordDTO; import com.weather.modules.weather.filescan.entity.WeatherFileScanRecordEntity; import com.weather.modules.weather.filescan.service.WeatherFileScanRecordService; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -35,12 +35,16 @@ import java.util.stream.Collectors; @Slf4j @Service -@AllArgsConstructor +@RequiredArgsConstructor public class WeatherFileScanRecordServiceImpl extends CrudServiceImpl implements WeatherFileScanRecordService { private final SysDeptService sysDeptService; private final SysDeptDao sysDeptDao; + /** 部门名称缓存,部门数据变化频率极低,缓存5分钟 */ + private volatile Map deptNameCache; + private volatile long deptNameCacheExpireAt; + @Override public PageData page(Map params) { IPage page = baseDao.selectPage( @@ -52,10 +56,24 @@ public class WeatherFileScanRecordServiceImpl extends CrudServiceImpl getDeptNameMap() { + long now = System.currentTimeMillis(); + if (deptNameCache != null && now < deptNameCacheExpireAt) { + return deptNameCache; + } + synchronized (this) { + if (deptNameCache != null && now < deptNameCacheExpireAt) { + return deptNameCache; + } + deptNameCache = sysDeptService.list(Map.of()).stream() + .collect(Collectors.toMap(SysDeptDTO::getId, SysDeptDTO::getName, (a, b) -> a)); + deptNameCacheExpireAt = now + 5 * 60 * 1000; + return deptNameCache; + } + } + private void enrichDeptNames(List list) { - List depts = sysDeptService.list(Map.of()); - Map deptMap = depts.stream() - .collect(Collectors.toMap(SysDeptDTO::getId, SysDeptDTO::getName, (a, b) -> a)); + Map deptMap = getDeptNameMap(); for (WeatherFileScanRecordDTO dto : list) { if (dto.getDeptId() != null) { dto.setDeptName(deptMap.get(dto.getDeptId())); diff --git a/system-admin/src/main/java/com/weather/modules/weather/station/service/WeatherStationService.java b/system-admin/src/main/java/com/weather/modules/weather/station/service/WeatherStationService.java index 977842b..4242623 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/station/service/WeatherStationService.java +++ b/system-admin/src/main/java/com/weather/modules/weather/station/service/WeatherStationService.java @@ -8,6 +8,7 @@ import com.weather.modules.weather.station.entity.WeatherStationEntity; import java.util.List; import java.util.Map; +import java.util.Set; /** * 气象站点表 @@ -25,6 +26,11 @@ public interface WeatherStationService extends CrudService getByStationCodes(Set stationCodes); + List getStationByDeptId(); List getStationByStationName(); diff --git a/system-admin/src/main/java/com/weather/modules/weather/station/service/impl/WeatherStationServiceImpl.java b/system-admin/src/main/java/com/weather/modules/weather/station/service/impl/WeatherStationServiceImpl.java index 21ee24c..4f51e4d 100644 --- a/system-admin/src/main/java/com/weather/modules/weather/station/service/impl/WeatherStationServiceImpl.java +++ b/system-admin/src/main/java/com/weather/modules/weather/station/service/impl/WeatherStationServiceImpl.java @@ -5,8 +5,11 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.weather.common.constant.Constant; import com.weather.common.page.PageData; +import com.weather.common.redis.RedisKeys; +import com.weather.common.redis.RedisUtils; import com.weather.common.service.impl.CrudServiceImpl; import com.weather.common.utils.ConvertUtils; +import com.weather.modules.security.user.SecurityUser; import com.weather.modules.sys.service.SysDeptService; import com.weather.modules.weather.station.dao.WeatherStationDao; import com.weather.modules.weather.station.dto.WeatherStationDTO; @@ -14,11 +17,13 @@ import com.weather.modules.weather.station.dto.WeatherStationInfoSimpleDto; import com.weather.modules.weather.station.entity.WeatherStationEntity; import com.weather.modules.weather.station.service.WeatherStationService; import cn.hutool.core.util.StrUtil; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; /** * 气象站点表 @@ -26,10 +31,12 @@ import java.util.Map; * @author Mark 123 * @since 1.0.0 2026-03-05 */ +@Slf4j @Service -@AllArgsConstructor +@RequiredArgsConstructor public class WeatherStationServiceImpl extends CrudServiceImpl implements WeatherStationService { private final SysDeptService sysDeptService; + private final RedisUtils redisUtils; @Override public QueryWrapper getWrapper(Map params) { String stationName = (String) params.get("stationName"); @@ -59,12 +66,57 @@ public class WeatherStationServiceImpl extends CrudServiceImpl getByStationCodes(Set stationCodes) { + if (stationCodes == null || stationCodes.isEmpty()) { + return Collections.emptyMap(); + } + List codeList = stationCodes.stream().map(String::valueOf).collect(Collectors.toList()); + QueryWrapper wrapper = new QueryWrapper<>(); + wrapper.in("station_code", codeList); + List entities = baseDao.selectList(wrapper); + return entities.stream() + .collect(Collectors.toMap( + e -> Long.valueOf(e.getStationCode()), + e -> ConvertUtils.sourceToTarget(e, WeatherStationDTO.class), + (a, b) -> a)); + } + + /** 站点列表缓存有效期(小时),站点变更频率极低 */ + private static final long STATION_LIST_CACHE_HOURS = 2; + + @SuppressWarnings("unchecked") @Override public List getStationByDeptId() { + Long deptId = SecurityUser.getDeptId(); + String cacheKey = RedisKeys.getStationListKey(deptId != null ? deptId : 0L); + + // 命中缓存直接返回(Jackson 反序列化为 List,需校验元素类型) + Object cached = redisUtils.get(cacheKey); + if (cached instanceof List list) { + if (!list.isEmpty()) { + Object first = list.get(0); + if (first instanceof WeatherStationInfoSimpleDto) { + return (List) list; + } + // Jackson 反序列化后元素是 LinkedHashMap,转换回 DTO + if (first instanceof Map) { + return (List) list.stream() + .map(m -> BeanUtil.copyProperties(m, WeatherStationInfoSimpleDto.class)) + .toList(); + } + } + } + QueryWrapper wrapper = new QueryWrapper<>(); wrapper = sysDeptService.limitedQueryScope(wrapper); List entities = baseDao.selectList(wrapper); - return BeanUtil.copyToList(entities, WeatherStationInfoSimpleDto.class); + List result = BeanUtil.copyToList(entities, WeatherStationInfoSimpleDto.class); + + // 缓存结果,站点数据变更频率极低(2小时) + redisUtils.set(cacheKey, result, RedisUtils.HOUR_ONE_EXPIRE * STATION_LIST_CACHE_HOURS); + log.debug("站点列表缓存已更新,共 {} 条", result.size()); + return result; } @Override @@ -72,4 +124,31 @@ public class WeatherStationServiceImpl extends CrudServiceImpl #{sid} - AND MONTH(observe_date) = #{month} - AND DAY(observe_date) = #{day} - AND YEAR(observe_date) BETWEEN #{startYear} AND #{endYear} + AND observe_date IN + + #{d} + ORDER BY station_id, observe_date diff --git a/system-common/src/main/java/com/weather/common/redis/RedisKeys.java b/system-common/src/main/java/com/weather/common/redis/RedisKeys.java index 7c4ba5b..f0f43ca 100644 --- a/system-common/src/main/java/com/weather/common/redis/RedisKeys.java +++ b/system-common/src/main/java/com/weather/common/redis/RedisKeys.java @@ -56,6 +56,20 @@ public class RedisKeys { return "sys:user:permissions:" + userId; } + /** + * 站点列表缓存Key(按部门权限区分) + */ + public static String getStationListKey(Long deptId) { + return "weather:station:list:" + deptId; + } + + /** + * 站点列表缓存匹配模式(站点变更时清除所有部门缓存) + */ + public static String getStationListPattern() { + return "weather:station:list:*"; + } + /** * 天气汇总缓存Key(按站点分组,月-日维度) */ diff --git a/weather-data-ui/index.html b/weather-data-ui/index.html index 9d67175..493c858 100644 --- a/weather-data-ui/index.html +++ b/weather-data-ui/index.html @@ -4,7 +4,7 @@ - 气象数据 - 管理系统 + 气象数据平台