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