项目名称修改,性能优化,结构优化

This commit is contained in:
2026-07-01 11:52:27 +08:00
parent c7114a4381
commit e358a01ee7
35 changed files with 567 additions and 239 deletions
@@ -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(")");
}
//查询本人数据
@@ -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());
}
@@ -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;
}
}
@@ -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;
@@ -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;
}
/**
@@ -24,12 +24,15 @@ public class SseAlertService {
private final Set<SseEmitter> 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());
@@ -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<SysDictTypeDao, SysD
public List<DictType> getAllList() {
List<DictType> typeList = baseDao.getDictTypeList();
List<DictData> dataList = sysDictDataDao.getDictDataList();
// 按 dictTypeId 分组,O(n+m) 替代 O(n*m) 嵌套循环
Map<Long, List<DictData>> 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<DictData> matched = dataMap.get(type.getId());
if (matched != null) {
type.getDataList().addAll(matched);
}
}
return typeList;
@@ -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<String, ImportProgress> 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<WeatherExcelVO> 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<String, ImportProgress> entry : tasks.entrySet()) {
if (isExpired(entry.getValue())) {
tasks.remove(entry.getKey());
removed++;
}
}
if (removed > 0) {
log.info("清理过期导入任务 {} 个,当前剩余 {} 个", removed, tasks.size());
}
}
}
@@ -35,6 +35,11 @@ public class WeatherDataListener extends AnalysisEventListener<WeatherExcelVO> {
@Override
public void invoke(WeatherExcelVO data, AnalysisContext context) {
// 同时计数(替代独立的 countListener,避免双次读取)
if (progress != null) {
progress.incrementTotalRows();
}
WeatherDailyDataEntity entity = ConvertUtils.sourceToTarget(data, WeatherDailyDataEntity.class);
try {
@@ -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<PageData<WeatherDailyDataDto>> page(@Parameter(hidden = true) @RequestParam Map<String, Object> params){
PageData<WeatherDailyDataDto> 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<Long> stationIds = page.getList().stream()
.map(WeatherDailyDataDto::getStationId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (!stationIds.isEmpty()) {
Map<Long, WeatherStationDTO> stationMap = weatherStationService.getByStationCodes(stationIds);
page.getList().forEach(item -> {
WeatherStationDTO station = stationMap.get(item.getStationId());
if (station != null) {
item.setStationName(station.getStationName());
}
});
}
return new Result<PageData<WeatherDailyDataDto>>().ok(page);
}
@@ -22,18 +22,13 @@ public interface WeatherDailyDataDao extends BaseDao<WeatherDailyDataEntity> {
List<WeatherDailyDataEntity> selectSummarizeList(@Param(Constants.WRAPPER) LambdaQueryWrapper<WeatherDailyDataEntity> 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<WeatherDailyDataEntity> selectSummarizeByMonthDay(@Param("stationIds") List<Long> stationIds,
@Param("month") int month,
@Param("day") int day,
@Param("startYear") int startYear,
@Param("endYear") int endYear);
@Param("dates") List<String> dates);
int insertBatchMultiRow(@Param("list") List<WeatherDailyDataEntity> list);
}
@@ -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();
}
}
@@ -242,18 +242,22 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
}
/**
* 直接查询数据库获取汇总数据
* <p>用 MONTH/DAY 定位具体日期 + YEAR BETWEEN 限定年份范围,避免日期 BETWEEN 全量问题</p>
* 直接查询数据库获取汇总数据(缓存未命中降级路径)。
* <p>生成精确日期列表使用 IN 等值查询,命中 uk_station_date 唯一索引,
* 避免 MONTH()/DAY()/YEAR() 函数导致全表扫描。</p>
*/
private List<DailyWeatherSummarizeDto> querySummarizeFromDb(
List<Long> stationIds, LocalDate startTime, LocalDate endTime) {
int month = startTime.getMonthValue();
int day = startTime.getDayOfMonth();
List<String> dates = new ArrayList<>();
for (int y = startTime.getYear(); y <= endTime.getYear(); y++) {
dates.add(String.format("%04d-%02d-%02d", y, month, day));
}
List<WeatherDailyDataEntity> list = weatherDailyDataDao.selectSummarizeByMonthDay(
stationIds,
startTime.getMonthValue(),
startTime.getDayOfMonth(),
startTime.getYear(),
endTime.getYear());
stationIds, dates);
return BeanUtil.copyToList(list, DailyWeatherSummarizeDto.class);
}
@@ -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<org.springframework.core.io.Resource> displayFile(@PathVariable("id") Long id) {
public ResponseEntity<org.springframework.core.io.Resource> 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-MatchETag 匹配则返回 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;
};
}
}
@@ -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<WeatherFileScanRecordDao, WeatherFileScanRecordEntity, WeatherFileScanRecordDTO> implements WeatherFileScanRecordService {
private final SysDeptService sysDeptService;
private final SysDeptDao sysDeptDao;
/** 部门名称缓存,部门数据变化频率极低,缓存5分钟 */
private volatile Map<Long, String> deptNameCache;
private volatile long deptNameCacheExpireAt;
@Override
public PageData<WeatherFileScanRecordDTO> page(Map<String, Object> params) {
IPage<WeatherFileScanRecordEntity> page = baseDao.selectPage(
@@ -52,10 +56,24 @@ public class WeatherFileScanRecordServiceImpl extends CrudServiceImpl<WeatherFil
return pageData;
}
private Map<Long, String> 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<WeatherFileScanRecordDTO> list) {
List<SysDeptDTO> depts = sysDeptService.list(Map.of());
Map<Long, String> deptMap = depts.stream()
.collect(Collectors.toMap(SysDeptDTO::getId, SysDeptDTO::getName, (a, b) -> a));
Map<Long, String> deptMap = getDeptNameMap();
for (WeatherFileScanRecordDTO dto : list) {
if (dto.getDeptId() != null) {
dto.setDeptName(deptMap.get(dto.getDeptId()));
@@ -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<WeatherStationEntity,
WeatherStationDTO getByStationCode(Long stationCode);
/**
* 批量查询站点信息,避免 N+1 问题
*/
Map<Long, WeatherStationDTO> getByStationCodes(Set<Long> stationCodes);
List<WeatherStationInfoSimpleDto> getStationByDeptId();
List<WeatherStationInfoSimpleDto> getStationByStationName();
@@ -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<WeatherStationDao, WeatherStationEntity, WeatherStationDTO> implements WeatherStationService {
private final SysDeptService sysDeptService;
private final RedisUtils redisUtils;
@Override
public QueryWrapper<WeatherStationEntity> getWrapper(Map<String, Object> params) {
String stationName = (String) params.get("stationName");
@@ -59,12 +66,57 @@ public class WeatherStationServiceImpl extends CrudServiceImpl<WeatherStationDao
return ConvertUtils.sourceToTarget(baseDao.selectOne(wrapper),WeatherStationDTO.class);
}
@Override
public Map<Long, WeatherStationDTO> getByStationCodes(Set<Long> stationCodes) {
if (stationCodes == null || stationCodes.isEmpty()) {
return Collections.emptyMap();
}
List<String> codeList = stationCodes.stream().map(String::valueOf).collect(Collectors.toList());
QueryWrapper<WeatherStationEntity> wrapper = new QueryWrapper<>();
wrapper.in("station_code", codeList);
List<WeatherStationEntity> 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<WeatherStationInfoSimpleDto> getStationByDeptId() {
Long deptId = SecurityUser.getDeptId();
String cacheKey = RedisKeys.getStationListKey(deptId != null ? deptId : 0L);
// 命中缓存直接返回(Jackson 反序列化为 List<Map>,需校验元素类型)
Object cached = redisUtils.get(cacheKey);
if (cached instanceof List list) {
if (!list.isEmpty()) {
Object first = list.get(0);
if (first instanceof WeatherStationInfoSimpleDto) {
return (List<WeatherStationInfoSimpleDto>) list;
}
// Jackson 反序列化后元素是 LinkedHashMap,转换回 DTO
if (first instanceof Map) {
return (List<WeatherStationInfoSimpleDto>) list.stream()
.map(m -> BeanUtil.copyProperties(m, WeatherStationInfoSimpleDto.class))
.toList();
}
}
}
QueryWrapper<WeatherStationEntity> wrapper = new QueryWrapper<>();
wrapper = sysDeptService.limitedQueryScope(wrapper);
List<WeatherStationEntity> entities = baseDao.selectList(wrapper);
return BeanUtil.copyToList(entities, WeatherStationInfoSimpleDto.class);
List<WeatherStationInfoSimpleDto> 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<WeatherStationDao
return List.of();
}
/** 清除站点列表缓存(站点增删改时调用) */
private void invalidateStationCache() {
redisUtils.deleteByPattern(RedisKeys.getStationListPattern());
log.debug("站点列表缓存已清除");
}
@Override
public boolean insert(WeatherStationEntity entity) {
boolean result = super.insert(entity);
invalidateStationCache();
return result;
}
@Override
public boolean updateById(WeatherStationEntity entity) {
boolean result = super.updateById(entity);
invalidateStationCache();
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(Long[] ids) {
super.delete(ids);
invalidateStationCache();
}
}
@@ -49,9 +49,10 @@
<foreach collection="stationIds" item="sid" open="(" separator="," close=")">
#{sid}
</foreach>
AND MONTH(observe_date) = #{month}
AND DAY(observe_date) = #{day}
AND YEAR(observe_date) BETWEEN #{startYear} AND #{endYear}
AND observe_date IN
<foreach collection="dates" item="d" open="(" separator="," close=")">
#{d}
</foreach>
ORDER BY station_id, observe_date
</select>