项目名称修改,性能优化,结构优化
This commit is contained in:
@@ -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());
|
||||
}
|
||||
|
||||
+12
-7
@@ -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());
|
||||
|
||||
|
||||
+9
-7
@@ -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;
|
||||
|
||||
+41
-16
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -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 {
|
||||
|
||||
+17
-10
@@ -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);
|
||||
}
|
||||
|
||||
+4
-9
@@ -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);
|
||||
}
|
||||
+15
-1
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+11
-7
@@ -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);
|
||||
}
|
||||
|
||||
+60
-15
@@ -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-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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+23
-5
@@ -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()));
|
||||
|
||||
+6
@@ -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();
|
||||
|
||||
+84
-5
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user