From 27e24880b840c45e6b7d4659fde6391baa846a28 Mon Sep 17 00:00:00 2001 From: panbaolin <13071138970@163.com> Date: Mon, 24 Nov 2025 11:55:22 +0800 Subject: [PATCH] =?UTF-8?q?1.=E5=AE=8C=E6=88=90=E5=A4=A9=E6=B0=94=E9=A2=84?= =?UTF-8?q?=E6=8A=A5=E8=AE=A1=E7=AE=97=E4=BB=BB=E5=8A=A1=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=202.=E4=BF=AE=E6=94=B9=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=E5=85=A8=E5=B1=80form-data=E8=AF=B7=E6=B1=82=E7=BC=96=E7=A0=81?= =?UTF-8?q?=E6=A0=BC=E5=BC=8F=203.=E5=AE=8C=E6=88=90=E8=BE=93=E8=BF=90?= =?UTF-8?q?=E6=A8=A1=E6=8B=9F=E8=B4=A1=E7=8C=AE=E5=88=86=E6=9E=90=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jeecg/common/constant/CommonConstant.java | 12 +- .../enums/TransportTaskStatusEnum.java | 2 +- .../enums/TransportTimingAnalysisEnum.java | 31 ++ .../enums/WeatherForecastDatasourceEnum.java | 22 + .../constant/enums/WeatherModelEnum.java | 26 -- .../constant/enums/WeatherTaskStatusEnum.java | 4 + .../properties/SystemStorageProperties.java | 15 + .../config/security/CopyTokenFilter.java | 1 + .../modules/base/entity/WeatherData.java | 8 +- .../modules/base/entity/WeatherTask.java | 55 ++- .../modules/base/entity/WeatherTaskLog.java | 5 +- .../controller/StationDataController.java | 2 +- .../TransportResultDataController.java | 49 +++ .../service/TransportResultDataService.java | 47 +++ .../jeecg/service/TransportTaskService.java | 2 +- .../impl/TransportResultDataServiceImpl.java | 392 +++++++++++++++++- .../impl/TransportTaskServiceImpl.java | 11 +- .../org/jeecg/task/TransportTaskExec.java | 7 +- .../util/BilinearInterpolatorWithMath.java | 175 ++++++++ .../org/jeecg/vo/ContributionAnalysisVO.java | 39 ++ .../java/org/jeecg/vo/TaskStationsVO.java | 17 + .../controller/WeatherTaskController.java | 14 +- .../org/jeecg/service/WeatherDataService.java | 10 +- .../org/jeecg/service/WeatherTaskService.java | 29 +- .../service/impl/WeatherDataServiceImpl.java | 15 +- .../service/impl/WeatherTaskServiceImpl.java | 139 ++++++- .../java/org/jeecg/task/ProgressEvent.java | 28 ++ .../java/org/jeecg/task/ProgressMonitor.java | 50 +++ .../java/org/jeecg/task/ProgressQueue.java | 39 ++ .../java/org/jeecg/task/WeatherTaskExec.java | 321 ++++++++++++++ .../java/org/jeecg/vo/ForecastFileVO.java | 26 ++ 31 files changed, 1527 insertions(+), 66 deletions(-) create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTimingAnalysisEnum.java create mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherForecastDatasourceEnum.java delete mode 100644 jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherModelEnum.java create mode 100644 jeecg-module-transport/src/main/java/org/jeecg/util/BilinearInterpolatorWithMath.java create mode 100644 jeecg-module-transport/src/main/java/org/jeecg/vo/ContributionAnalysisVO.java create mode 100644 jeecg-module-transport/src/main/java/org/jeecg/vo/TaskStationsVO.java create mode 100644 jeecg-module-weather/src/main/java/org/jeecg/task/ProgressEvent.java create mode 100644 jeecg-module-weather/src/main/java/org/jeecg/task/ProgressMonitor.java create mode 100644 jeecg-module-weather/src/main/java/org/jeecg/task/ProgressQueue.java create mode 100644 jeecg-module-weather/src/main/java/org/jeecg/task/WeatherTaskExec.java create mode 100644 jeecg-module-weather/src/main/java/org/jeecg/vo/ForecastFileVO.java diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java index f35ba03..8441c60 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/CommonConstant.java @@ -270,7 +270,7 @@ public interface CommonConstant { /** * 报表允许设计开发的角色 */ - public static String[] allowDevRoles = new String[]{"lowdeveloper", "admin"}; + String[] allowDevRoles = new String[]{"lowdeveloper", "admin"}; /** * 缓存用户最后一次收到消息通知的时间 KEY @@ -286,4 +286,14 @@ public interface CommonConstant { * 缓存所有核设施key */ String ALL_NUCLEARFACILITY = "nuclearfacility"; + + /** + * 输运模拟贡献分析数据KEY + */ + String TRANSPORT_CONTRIBUTION_ANALYSIS = "transport:contribution_analysis:"; + + /** + * 输运模拟时序分析数据KEY + */ + String TRANSPORT_TIMING_ANALYSIS = "transport:timing_analysis:"; } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTaskStatusEnum.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTaskStatusEnum.java index ac40494..48421e2 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTaskStatusEnum.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTaskStatusEnum.java @@ -8,7 +8,7 @@ public enum TransportTaskStatusEnum { /** * 执行失败 */ - ERROR(-1), + FAILURE(-1), /** * 未开始 */ diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTimingAnalysisEnum.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTimingAnalysisEnum.java new file mode 100644 index 0000000..f85b3f0 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/TransportTimingAnalysisEnum.java @@ -0,0 +1,31 @@ +package org.jeecg.common.constant.enums; + +public enum TransportTimingAnalysisEnum { + + /** + * 3小时 + */ + THREE_HOURS(3), + /** + * 6小时 + */ + SIX_HOURS(-1), + /** + * 12小时 + */ + TWELVE_HOURS(-1), + /** + * 24小时 + */ + TWENTY_FOUR_HOURS(-1); + + private Integer key; + + TransportTimingAnalysisEnum(Integer key) { + this.key = key; + } + + public Integer getKey(){ + return this.key; + } +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherForecastDatasourceEnum.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherForecastDatasourceEnum.java new file mode 100644 index 0000000..d2939c2 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherForecastDatasourceEnum.java @@ -0,0 +1,22 @@ +package org.jeecg.common.constant.enums; + +/** + * 盘古和Graphcast气象预测模型使用的数据源 + */ +public enum WeatherForecastDatasourceEnum { + + + CDS(1), + LOCATION_FILE(2); + + private Integer key; + + WeatherForecastDatasourceEnum(Integer key) { + this.key = key; + } + + public Integer getKey(){ + return this.key; + } + +} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherModelEnum.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherModelEnum.java deleted file mode 100644 index eb13f54..0000000 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherModelEnum.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.jeecg.common.constant.enums; - -/** - * 预测模型说明枚举 - */ -public enum WeatherModelEnum { - - /** - * 盘古模型 - */ - PANGU(1), - /** - * Graphcast - */ - GRAPHCAST(2); - - private Integer key; - - WeatherModelEnum(Integer key) { - this.key = key; - } - - public Integer getKey(){ - return this.key; - } -} diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherTaskStatusEnum.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherTaskStatusEnum.java index 6d01f24..5392436 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherTaskStatusEnum.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/constant/enums/WeatherTaskStatusEnum.java @@ -5,6 +5,10 @@ package org.jeecg.common.constant.enums; */ public enum WeatherTaskStatusEnum { + /** + * 执行失败 + */ + FAILURE(-1), /** * 未开始 */ diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/properties/SystemStorageProperties.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/properties/SystemStorageProperties.java index 5767fd4..4d9bb0c 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/common/properties/SystemStorageProperties.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/properties/SystemStorageProperties.java @@ -53,4 +53,19 @@ public class SystemStorageProperties { * CMAQ数据存储路径 */ private String cmaqPath; + + /** + * ai-models 安装地址 + */ + private String aiModelsPath; + + /** + * 盘古模型执行路径 + */ + private String panguModelExecPath; + + /** + * graphcast模型执行路径 + */ + private String graphcastModelExecPath; } diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/CopyTokenFilter.java b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/CopyTokenFilter.java index de18fbe..a1e33dc 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/CopyTokenFilter.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/config/security/CopyTokenFilter.java @@ -23,6 +23,7 @@ import java.io.IOException; public class CopyTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + request.setCharacterEncoding("UTF-8"); // 以下为undertow定制代码,如切换其它servlet容器,需要同步更换 HttpServletRequestImpl undertowRequest = (HttpServletRequestImpl) request; String token = request.getHeader("Authorization"); diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherData.java b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherData.java index 2e5c2f6..d66af6c 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherData.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherData.java @@ -51,7 +51,7 @@ public class WeatherData implements Serializable { private LocalDateTime dataStartTime; /** - * 数据来源(1-盘古模型,2-graphcast,3-cra40,4-ncep,5-t1h,6-fnl) + * 数据来源(1-盘古模型,2-graphcast,3-cra40,4-ncep,5-fnl,6-t1h) */ @TableField(value = "data_source") private Integer dataSource; @@ -62,12 +62,6 @@ public class WeatherData implements Serializable { @TableField(value = "file_path") private String filePath; - /** - * 创建人 - */ - @TableField(value = "create_by") - private String createBy; - /** * 创建时间 */ diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTask.java b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTask.java index aec56dc..93fb638 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTask.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTask.java @@ -1,23 +1,37 @@ package org.jeecg.modules.base.entity; +import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Null; import lombok.Data; -import org.jeecg.common.system.base.entity.BaseEntity; import org.jeecg.common.validgroup.InsertGroup; import org.jeecg.common.validgroup.UpdateGroup; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.multipart.MultipartFile; import java.time.LocalDate; +import java.util.Date; /** * 天气预测任务表 */ @Data @TableName("stas_weather_task") -public class WeatherTask extends BaseEntity { +public class WeatherTask{ + + /** + * ID + */ + @Null(message = "ID必须为空",groups = { InsertGroup.class}) + @NotNull(message = "ID不能为空",groups = { UpdateGroup.class}) + @TableId(type = IdType.INPUT) + @Schema(description = "ID") + private java.lang.String id; /** * 任务名称 @@ -27,18 +41,18 @@ public class WeatherTask extends BaseEntity { private String taskName; /** - * 任务状态(0-未开始,1-运行中,2-已完成) + * 任务状态(-1失败,0-未开始,1-运行中,2-已完成) */ @Null(message = "任务状态必须为空",groups = {InsertGroup.class, UpdateGroup.class}) @TableField(value = "task_status") private Integer taskStatus; /** - * 耗时(小时),默认0 + * 耗时(分钟),默认0 */ @Null(message = "耗时参数必须为空",groups = {InsertGroup.class, UpdateGroup.class}) @TableField(value = "time_consuming") - private Integer timeConsuming; + private Double timeConsuming; /** * 预测模型(1-盘古、2-Graphcast) @@ -79,7 +93,38 @@ public class WeatherTask extends BaseEntity { /** * 如果data_sources为2,则存储本地文件路径 */ + @Null(message = "预测文件本地路径必须为空",groups = {InsertGroup.class, UpdateGroup.class}) @TableField(value = "input_file") private String inputFile; + /** + * 预测文件 + */ + @TableField(exist = false) + private MultipartFile file; + + /** + * 创建人 + */ + private String createBy; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新人 + */ + private String updateBy; + + /** + * 更新时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + } \ No newline at end of file diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTaskLog.java b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTaskLog.java index 9931279..e438cf6 100644 --- a/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTaskLog.java +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/modules/base/entity/WeatherTaskLog.java @@ -7,9 +7,8 @@ import com.baomidou.mybatisplus.annotation.TableName; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; - import java.io.Serializable; -import java.time.LocalDateTime; +import java.util.Date; /** * 天气预测任务日志表 @@ -41,6 +40,6 @@ public class WeatherTaskLog implements Serializable { * 创建时间 */ @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") - private LocalDateTime createTime; + private Date createTime; } \ No newline at end of file diff --git a/jeecg-module-transport/src/main/java/org/jeecg/controller/StationDataController.java b/jeecg-module-transport/src/main/java/org/jeecg/controller/StationDataController.java index 8c7fc7b..f07b03b 100644 --- a/jeecg-module-transport/src/main/java/org/jeecg/controller/StationDataController.java +++ b/jeecg-module-transport/src/main/java/org/jeecg/controller/StationDataController.java @@ -25,7 +25,7 @@ public class StationDataController { return Result.OK(stationDataService.getAllStations()); } - @AutoLog(value = "查询所有台站") + @AutoLog(value = "查询所有设施") @Operation(summary = "查询所有设施") @GetMapping("getAllNuclearfacility") public Result getAllNuclearfacility(){ diff --git a/jeecg-module-transport/src/main/java/org/jeecg/controller/TransportResultDataController.java b/jeecg-module-transport/src/main/java/org/jeecg/controller/TransportResultDataController.java index d371096..0666c17 100644 --- a/jeecg-module-transport/src/main/java/org/jeecg/controller/TransportResultDataController.java +++ b/jeecg-module-transport/src/main/java/org/jeecg/controller/TransportResultDataController.java @@ -1,6 +1,7 @@ package org.jeecg.controller; import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.RequiredArgsConstructor; import org.jeecg.common.api.vo.Result; @@ -43,6 +44,40 @@ public class TransportResultDataController { return Result.OK(transportResultDataService.getConformityAnalysis(taskId,stationId,nuclideName,facilityName)); } + @AutoLog(value = "查询贡献分析数据") + @Operation(summary = "查询贡献分析数据") + @GetMapping("getContributionAnalysis") + public Result getContributionAnalysis(@NotNull(message = "任务id不能为空") Integer taskId, + @NotBlank(message = "台站编码不能为空") String stationCode) { + return Result.OK(transportResultDataService.getContributionAnalysis(taskId,stationCode)); + } + + @AutoLog(value = "处理贡献分析数据-测试接口") + @Operation(summary = "处理贡献分析数据-测试接口") + @GetMapping("handleContributionAnalysis") + public Result handleContributionAnalysis(@NotNull(message = "任务id不能为空") Integer taskId) { + transportResultDataService.handleContributionAnalysis(taskId); + return Result.OK(); + } + + @AutoLog(value = "查询反演核设施时序数据接口") + @Operation(summary = "查询反演核设施时序数据接口") + @GetMapping("getTimingAnalysis") + public Result getTimingAnalysis(@NotNull(message = "任务id不能为空") Integer taskId, + @NotBlank(message = "台站编码不能为空") String stationCode, + @NotBlank(message = "核设施名称不能为空") String facilityName, + @NotNull(message = "时间序号不能为空") Integer timeNum) { + return Result.OK(transportResultDataService.getTimingAnalysis(taskId,stationCode,facilityName,timeNum)); + } + + @AutoLog(value = "处理反演核设施时序数据-测试接口") + @Operation(summary = "处理反演核设施时序数据-测试接口") + @GetMapping("handleTimingAnalysis") + public Result handleTimingAnalysis(@NotNull(message = "任务id不能为空") Integer taskId) { + transportResultDataService.handleTimingAnalysis(taskId); + return Result.OK(); + } + @AutoLog(value = "查询任务所属核设施数据") @Operation(summary = "查询任务所属核设施数据") @GetMapping("getTaskFacility") @@ -50,4 +85,18 @@ public class TransportResultDataController { return Result.OK(transportResultDataService.getTaskFacility(taskId)); } + @AutoLog(value = "查询任务所属台站数据") + @Operation(summary = "查询任务所属台站数据") + @GetMapping("getTaskStations") + public Result getTaskStations(@NotNull(message = "任务id不能为空") Integer taskId) { + return Result.OK(transportResultDataService.getTaskStations(taskId)); + } + + @AutoLog(value = "查询任务所属NC层级数据") + @Operation(summary = "查询任务所属NC层级数据") + @GetMapping("getTaskNCHeightLevel") + public Result getTaskNCHeightLevel(@NotNull(message = "任务id不能为空") Integer taskId) { + return Result.OK(transportResultDataService.getTaskNCHeightLevel(taskId)); + } + } diff --git a/jeecg-module-transport/src/main/java/org/jeecg/service/TransportResultDataService.java b/jeecg-module-transport/src/main/java/org/jeecg/service/TransportResultDataService.java index 44cd3bb..d87d56a 100644 --- a/jeecg-module-transport/src/main/java/org/jeecg/service/TransportResultDataService.java +++ b/jeecg-module-transport/src/main/java/org/jeecg/service/TransportResultDataService.java @@ -1,6 +1,8 @@ package org.jeecg.service; +import org.jeecg.vo.ContributionAnalysisVO; import org.jeecg.vo.QueryDiffusionVO; +import org.jeecg.vo.TaskStationsVO; import java.util.List; import java.util.Map; @@ -37,4 +39,49 @@ public interface TransportResultDataService { * @return */ List getTaskFacility(Integer taskId); + + /** + * 获取贡献分析数据 + * @param taskId + * @param stationCode + * @return + */ + ContributionAnalysisVO getContributionAnalysis(Integer taskId, String stationCode); + + /** + * 处理贡献分析数据存入缓存 + * @param taskId + * @return + */ + void handleContributionAnalysis(Integer taskId); + + /** + * 查询任务所属台站数据 + * @param taskId + * @return + */ + List getTaskStations(Integer taskId); + + /** + * 查询任务所属NC层级数据 + * @param taskId + * @return + */ + Map getTaskNCHeightLevel(Integer taskId); + + /** + * 查询反演核设施时序数据接口 + * @param taskId + * @param stationCode + * @param facilityName + * @param timeNum + */ + Map getTimingAnalysis(Integer taskId,String stationCode,String facilityName,Integer timeNum); + + /** + * 处理反演核设施时序数据存入缓存 + * @param taskId + * @return + */ + void handleTimingAnalysis(Integer taskId); } diff --git a/jeecg-module-transport/src/main/java/org/jeecg/service/TransportTaskService.java b/jeecg-module-transport/src/main/java/org/jeecg/service/TransportTaskService.java index d6a73b8..0d458fe 100644 --- a/jeecg-module-transport/src/main/java/org/jeecg/service/TransportTaskService.java +++ b/jeecg-module-transport/src/main/java/org/jeecg/service/TransportTaskService.java @@ -77,7 +77,7 @@ public interface TransportTaskService extends IService { * @param taskId * @param minute */ - void updateTaskTimeConsuming(Integer taskId, Double minute); + void updateTaskStatusToCompleted(Integer taskId, Double minute); /** * 删除任务日志 diff --git a/jeecg-module-transport/src/main/java/org/jeecg/service/impl/TransportResultDataServiceImpl.java b/jeecg-module-transport/src/main/java/org/jeecg/service/impl/TransportResultDataServiceImpl.java index 6de60e6..51979ef 100644 --- a/jeecg-module-transport/src/main/java/org/jeecg/service/impl/TransportResultDataServiceImpl.java +++ b/jeecg-module-transport/src/main/java/org/jeecg/service/impl/TransportResultDataServiceImpl.java @@ -2,22 +2,31 @@ package org.jeecg.service.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.date.DateUtil; +import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.constant.enums.TransportTaskModeEnum; +import org.jeecg.common.constant.enums.TransportTimingAnalysisEnum; import org.jeecg.common.properties.TransportSimulationProperties; import org.jeecg.common.util.NcUtil; +import org.jeecg.common.util.RedisUtil; import org.jeecg.modules.base.entity.TransportTask; import org.jeecg.modules.base.entity.TransportTaskChild; +import org.jeecg.modules.base.entity.configuration.GardsNuclearfacility; import org.jeecg.modules.base.entity.configuration.GardsStations; import org.jeecg.modules.base.mapper.TransportTaskChildMapper; import org.jeecg.modules.base.mapper.TransportTaskMapper; import org.jeecg.service.StationDataService; import org.jeecg.service.TransportResultDataService; +import org.jeecg.util.BilinearInterpolatorWithMath; import org.jeecg.vo.ConcModValVo; +import org.jeecg.vo.ContributionAnalysisVO; import org.jeecg.vo.QueryDiffusionVO; +import org.jeecg.vo.TaskStationsVO; import org.springframework.stereotype.Service; import ucar.ma2.Array; import ucar.ma2.DataType; @@ -36,6 +45,7 @@ import java.time.ZoneId; import java.util.*; import java.util.stream.Collectors; +@Slf4j @Service @RequiredArgsConstructor public class TransportResultDataServiceImpl implements TransportResultDataService { @@ -44,6 +54,7 @@ public class TransportResultDataServiceImpl implements TransportResultDataServic private final TransportSimulationProperties simulationProperties; private final TransportTaskChildMapper transportTaskChildMapper; private final StationDataService stationDataService; + private final RedisUtil redisUtil; private final static String FORWARD="forward"; private final static String BACK_FORWARD="backward"; @@ -236,7 +247,6 @@ public class TransportResultDataServiceImpl implements TransportResultDataServic } List> xeResults = stationDataService.getXeResults(stationId, transportTask.getStartTime(), transportTask.getEndTime(), nuclideName); - System.out.println(xeResults); //获取nc文件路径 String path = this.getForwardTaskNCPath(transportTask); try (NetcdfFile ncFile = NetcdfFile.open(path.toString())) { @@ -273,6 +283,7 @@ public class TransportResultDataServiceImpl implements TransportResultDataServic List modValList = new ArrayList<>(); for(int i=pointNum;i<=maxPointNum;i++){ for(int k=0;k queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TransportTaskChild::getTaskId,taskId); + List stationInfos = this.transportTaskChildMapper.selectList(queryWrapper); + + //所以核设施数据 + List facilitys = stationDataService.getAllNuclearfacility(); + + NetcdfFile ncFile = null; + try { + for(TransportTaskChild stationInfo :stationInfos) { + //每个台站的结果数据 + ContributionAnalysisVO contributionAnalysisVO = new ContributionAnalysisVO(); + //存储台站每天的浓度值数据 + Map stationEveryDayConcDatas = new LinkedHashMap<>(); + //存储核设施每天的浓度值数据 + Map> facilityEveryDayConcDatas = new HashMap<>(); + //饼图数据 + Map pipeChartData = new HashMap<>(); + //总浓度值 + Double totalConc = 0D; + //获取nc文件路径 + String path = this.getBackForwardTaskNCPath(transportTask,stationInfo.getStationCode()); + ncFile = NetcdfFile.open(path.toString()); + List lonData = NcUtil.getNCList(ncFile, "longitude"); + List latData = NcUtil.getNCList(ncFile, "latitude"); + List timeData = NcUtil.getNCList(ncFile, "time"); + Variable spec001Mr = ncFile.findVariable("spec001_mr"); + for(int k=0;k0){ + if (!facilityEveryDayConcDatas.containsKey(dayStr)) { + Map facilityConcMap = new HashMap<>(); + facilityConcMap.put(facility.getFacilityName(),facilityConc); + facilityEveryDayConcDatas.put(dayStr,facilityConcMap); + }else { + Map facilityConcMap = facilityEveryDayConcDatas.get(dayStr); + if (!facilityConcMap.containsKey(facility.getFacilityName())) { + facilityConcMap.put(facility.getFacilityName(),facilityConc); + }else { + facilityConcMap.put(facility.getFacilityName(),facilityConcMap.get(facility.getFacilityName())+facilityConc); + } + } + } + } + } + //计算本模拟时间内总浓度数据 + totalConc = stationEveryDayConcDatas.values().stream().mapToDouble(Double::doubleValue).sum(); + //处理饼图数据 + if(CollUtil.isNotEmpty(facilityEveryDayConcDatas)){ + for (Map facilityConcMap : facilityEveryDayConcDatas.values()) { + Set> entries = facilityConcMap.entrySet(); + for (Map.Entry entry : entries) { + if (!pipeChartData.containsKey(entry.getKey())) { + pipeChartData.put(entry.getKey(),entry.getValue()); + }else { + pipeChartData.put(entry.getKey(),pipeChartData.get(entry.getKey())+entry.getValue()); + } + } + } + } + //处理返回值 + contributionAnalysisVO.setTotalConc(totalConc); + contributionAnalysisVO.setStationEveryDayConcDatas(stationEveryDayConcDatas); + contributionAnalysisVO.setFacilityEveryDayConcDatas(facilityEveryDayConcDatas); + contributionAnalysisVO.setNuclideNames(new ArrayList<>(pipeChartData.keySet())); + contributionAnalysisVO.setPipeChartData(pipeChartData); + redisUtil.set(CommonConstant.TRANSPORT_CONTRIBUTION_ANALYSIS+transportTask.getId()+":"+stationInfo.getStationCode(), contributionAnalysisVO); + ncFile.close(); + } + }catch (IOException | InvalidRangeException e) { + throw new RuntimeException(e); + }finally { + try { + if(ncFile !=null){ + ncFile.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + /** + * 查询任务所属台站数据 + * @param taskId + * @return + */ + @Override + public List getTaskStations(Integer taskId) { + TransportTask transportTask = this.transportTaskMapper.selectById(taskId); + if(Objects.isNull(transportTask)){ + throw new RuntimeException("此任务不存在"); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TransportTaskChild::getTaskId,taskId); + queryWrapper.select(TransportTaskChild::getId,TransportTaskChild::getStationCode); + queryWrapper.orderByAsc(TransportTaskChild::getId); + List transportTaskChildren = transportTaskChildMapper.selectList(queryWrapper); + if(CollUtil.isEmpty(transportTaskChildren)){ + throw new RuntimeException("此任务站点信息不存在,请确认任务配置信息"); + } + //本任务模拟的台站数据 + List taskStationsVOList = new ArrayList<>(); + for (int i = 0; i getTaskNCHeightLevel(Integer taskId) { + Map resultMap = new HashMap<>(); + + TransportTask transportTask = this.transportTaskMapper.selectById(taskId); + if(Objects.isNull(transportTask)){ + throw new RuntimeException("此任务不存在"); + } + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TransportTaskChild::getTaskId,taskId); + queryWrapper.select(TransportTaskChild::getId,TransportTaskChild::getStationCode); + queryWrapper.orderByAsc(TransportTaskChild::getId); + List transportTaskChildren = transportTaskChildMapper.selectList(queryWrapper); + if(CollUtil.isEmpty(transportTaskChildren)){ + throw new RuntimeException("此任务站点信息不存在,请确认任务配置信息"); + } + //获取nc文件路径 + String path = ""; + if(TransportTaskModeEnum.FORWARD.getKey().equals(transportTask.getTaskMode())){ + path = this.getForwardTaskNCPath(transportTask); + }else if(TransportTaskModeEnum.BACK_FORWARD.getKey().equals(transportTask.getTaskMode())){ + path = this.getBackForwardTaskNCPath(transportTask,transportTaskChildren.get(0).getStationCode()); + } + if(!FileUtil.exist(path)){ + throw new RuntimeException("此任务模拟结果不存在,请确认任务运行状态"); + } + //全局属性 + try (NetcdfFile ncFile = NetcdfFile.open(path.toString())) { + Variable heightVar = ncFile.findVariable("height"); + Array data = heightVar.read(); + int[] shape = data.getShape(); + Index index = data.getIndex(); + for(int i=0;i getTimingAnalysis(Integer taskId,String stationCode,String facilityName,Integer timeNum) { + TransportTask transportTask = transportTaskMapper.selectById(taskId); + if(Objects.isNull(transportTask)){ + throw new RuntimeException("此任务不存在"); + } + //包含从缓存中拿 + String key = CommonConstant.TRANSPORT_TIMING_ANALYSIS+transportTask.getId()+":"+stationCode+":"+facilityName; + if(redisUtil.hasKey(key)){ + Map concValMap = (LinkedHashMap)redisUtil.get(key); + //如果是3小时纬度,直接范围,因为最小是3小时 + if (TransportTimingAnalysisEnum.THREE_HOURS.getKey().equals(timeNum)) { + concValMap.forEach((k,v)->{ + //纳克转换为毫克需除以1000000 + BigDecimal concValue = new BigDecimal(v); + BigDecimal finalValue = concValue.divide(new BigDecimal(1000000)).setScale(5,BigDecimal.ROUND_HALF_UP); + concValMap.put(k,finalValue.doubleValue()); + }); + return concValMap; + }else{ + Map resultMap = new LinkedHashMap<>(); + Instant endTimeInstant = transportTask.getEndTime().toInstant(); + LocalDateTime endTime = LocalDateTime.ofInstant(endTimeInstant, ZoneId.systemDefault()); + + Instant startTimeInstant = transportTask.getStartTime().toInstant(); + LocalDateTime startTime = LocalDateTime.ofInstant(startTimeInstant, ZoneId.systemDefault()); + + boolean flag = true; + while(flag){ + String dayTimeStr = LocalDateTimeUtil.format(startTime, "yyyy-MM-dd HH:mm:ss"); + //纳克转换为毫克需除以1000000 + BigDecimal concValue = new BigDecimal(concValMap.get(dayTimeStr)); + BigDecimal finalValue = concValue.divide(new BigDecimal(1000000)).setScale(5,BigDecimal.ROUND_HALF_UP); + resultMap.put(dayTimeStr,finalValue.doubleValue()); + startTime = startTime.plusHours(timeNum); + if(startTime.isEqual(endTime)){ + flag = false; + } + } + return resultMap; + } + } + return Map.of(); + } + + /** + * 处理反演核设施时序数据存入缓存 + * @param taskId + * @return + */ + @Override + public void handleTimingAnalysis(Integer taskId) { + TransportTask transportTask = this.transportTaskMapper.selectById(taskId); + + //查询需要处理数据的台站 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(TransportTaskChild::getTaskId,taskId); + List stationInfos = this.transportTaskChildMapper.selectList(queryWrapper); + + //所以核设施数据 + List facilitys = stationDataService.getAllNuclearfacility(); + + NetcdfFile ncFile = null; + try { + for(TransportTaskChild stationInfo :stationInfos) { + log.info("处理"+stationInfo.getStationCode()+"台站数据"); + Map> everyFacilityConcDatas = new HashMap<>(); + //获取nc文件路径 + String path = this.getBackForwardTaskNCPath(transportTask,stationInfo.getStationCode()); + ncFile = NetcdfFile.open(path.toString()); + List lonData = NcUtil.getNCList(ncFile, "longitude"); + List latData = NcUtil.getNCList(ncFile, "latitude"); + List timeData = NcUtil.getNCList(ncFile, "time"); + Variable spec001Mr = ncFile.findVariable("spec001_mr"); + for (GardsNuclearfacility facility : facilitys){ + //存储台站每步的浓度值数据 + Map everyStepConcDatas = new LinkedHashMap<>(); + for(int k=0;k{ + String key = CommonConstant.TRANSPORT_TIMING_ANALYSIS+transportTask.getId()+":"+stationInfo.getStationCode()+":"+facilityName; + redisUtil.set(key, facilityConcData); + }); + } + ncFile.close(); + } + }catch (IOException | InvalidRangeException e) { + throw new RuntimeException(e); + }finally { + try { + if(ncFile !=null){ + ncFile.close(); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + /** * 找到集合中里最接近目标值的值 * @param data @@ -406,4 +737,63 @@ public class TransportResultDataServiceImpl implements TransportResultDataServic path.append("grid_time_"+DateUtil.format(transportTask.getEndTime(),"yyyyMMddHHmmss")+".nc"); return path.toString(); } + + /** + * 在步长为 0.25° 的递增经纬度数组中,查找指定位置的索引 + * - lons[0] 是起始经度 + * - 步长严格为 0.25 + * - target 一定存在 + */ + public int findTargetIndex(List datas, double target) { + // 计算偏移量(单位:0.25°) + double offset = (target - datas.get(0)) / 0.25; + + // 四舍五入取整(抵抗浮点误差,如 166.5 - 166.0 = 0.5000000001 → 2.000...) + int index = (int) Math.round(offset); + + // 安全检查(可选) + if (index < 0 || index >= datas.size() || Math.abs(datas.get(index) - target) > 1e-5) { + System.out.println(datas); + throw new IllegalArgumentException("经度 " + target + " 不在数组中"); + } + return index; + } + + /** + * 获取指定位置的浓度数据 + * @param lonData nc文件的经度数据 + * @param latData nc文件的纬度数据 + * @param targetLon 目标位置的经度 + * @param targetLat 目标位置的纬度 + * @param k 时间索引 + * @param spec001Mr nc文件浓度数据变量 + * @return + * @throws InvalidRangeException + * @throws IOException + */ + private Double getTargetSiteConc(List lonData,List latData,Double targetLon,Double targetLat,int k,Variable spec001Mr) throws InvalidRangeException, IOException { + int lonIndex = this.findTargetIndex(lonData, targetLon.intValue()-0.125); + int latIndex = this.findTargetIndex(latData, targetLat.intValue()-0.125); + //nageclass=1, pointspec=1, time=30, height=6, latitude=710, longitude=1430 + int[] origin = {0,0,k,0, (latIndex-6),(lonIndex-6)}; + int[] section = {1, 1,1,1,12,12}; + Array levelData = spec001Mr.read(origin,section); + double[] pointData = (double[]) levelData.get1DJavaArray(DataType.DOUBLE); + Map siteData = new LinkedHashMap<>(); + int nlon_patch = 12,patchLatStart = latIndex - 6,patchLonStart = lonIndex - 6; + for(int i=0;i data; + private final double delta; + private final double offset; + + public GridData(Map data, double delta, double offset) { + this.data = data; + this.delta = delta; + this.offset = offset; + } + + /** + * 计算双线性插值 + */ + public Double interpolate(double lon, double lat) { + //精确匹配格点(避免边界问题) + Point target = new Point(lon, lat); + Double exactValue = data.get(target); + if (exactValue != null && !Double.isNaN(exactValue)) { + return exactValue; // 直接返回,不插值 + } + + //1. 定位网格单元:使用 Math.floor 确保负数正确处理 + // 关键:避免 (int) 强转的 truncation 问题 + int i = (int) Math.floor((lon - offset) / delta); + int j = (int) Math.floor((lat - offset) / delta); + + // 计算四个角点坐标 + double lon0 = i * delta + offset; + double lon1 = lon0 + delta; + double lat0 = j * delta + offset; + double lat1 = lat0 + delta; + + //2. 防御性检查:目标点是否真的在 [lon0, lon1) × [lat0, lat1) 内? + // 使用 Math.abs + 容差避免浮点误差导致误判 + final double TOL = 1e-10; + if (lon < lon0 - TOL || lon > lon1 + TOL || + lat < lat0 - TOL || lat > lat1 + TOL) { + System.err.printf("Warning: (%.6f, %.6f) is outside computed cell [%.3f~%.3f]×[%.3f~%.3f]%n", + lon, lat, lon0, lon1, lat0, lat1); + // 可选:自动修正索引 + i = (int) Math.floor((lon - offset) / delta); // 再算一次 + j = (int) Math.floor((lat - offset) / delta); + lon0 = i * delta + offset; + lon1 = lon0 + delta; + lat0 = j * delta + offset; + lat1 = lat0 + delta; + } + + //3. 构造四邻点并查值 + Point[] points = { + new Point(lon0, lat0), // SW + new Point(lon1, lat0), // SE + new Point(lon0, lat1), // NW + new Point(lon1, lat1) // NE + }; + Double[] values = new Double[4]; + boolean hasMissing = false; + for (int k = 0; k < 4; k++) { + values[k] = data.get(points[k]); + if (values[k] == null || Double.isNaN(values[k])) { + hasMissing = true; + System.err.println("Missing/NaN at: " + points[k].lon + ", " + points[k].lat); + } + } + if (hasMissing) return null; + + //4. 计算权重:使用 Math.max/Math.min 裁剪到 [0,1](防浮点误差溢出) + double wx = (lon - lon0) / delta; + double wy = (lat - lat0) / delta; + + // 关键增强:防止 wx/wy 因浮点误差略超 [0,1] + wx = Math.max(0.0, Math.min(1.0, wx)); // 等价于 clamp(wx, 0, 1) + wy = Math.max(0.0, Math.min(1.0, wy)); + + //5. 双线性插值公式(核心,无需 Math 复杂函数) + double v00 = values[0], v10 = values[1], v01 = values[2], v11 = values[3]; + double val = (1 - wx) * (1 - wy) * v00 + + wx * (1 - wy) * v10 + + (1 - wx) * wy * v01 + + wx * wy * v11; + + //6. 最终检查:结果是否合理? + // 使用 Math.abs + isFinite 防 NaN/Inf + if (!Double.isFinite(val)) { + System.err.println("Interpolation produced invalid value: " + val); + return null; + } + return val; + } + + /** + * 批量插值(高效处理多个点) + * 展示 Math.copySign / Math.IEEEremainder 等高级用法(可选场景) + */ + public double[] interpolateBatch(double[] lons, double[] lats) { + if (lons.length != lats.length) + throw new IllegalArgumentException("Length mismatch"); + + double[] results = new double[lons.length]; + + for (int idx = 0; idx < lons.length; idx++) { + Double val = interpolate(lons[idx], lats[idx]); + results[idx] = (val != null) ? val : Double.NaN; + + // 可选:对经度做环绕处理(全球数据常见) + // 例如:将 -181° 映射到 179° + double adjustedLon = lons[idx]; + if (Math.abs(adjustedLon) > 180.0) { + // 使用 IEEE 754 标准取余(比 % 更可靠) + adjustedLon = Math.IEEEremainder(adjustedLon + 180.0, 360.0) - 180.0; + // 保持符号一致性(-0.0 → 0.0) + adjustedLon = Math.copySign(adjustedLon, adjustedLon); + } + // 然后用 adjustedLon 重新插值... + } + return results; + } + } + + // ===== 演示:Math 库增强版用法 ===== + public static void main(String[] args) { + Map data = new LinkedHashMap<>(); + data.put(new Point(7.875, 47.875), 195.53140258789062); + data.put(new Point(8.125, 47.875), 9.2166166305542); + data.put(new Point(7.875, 48.125), 40.02995681762695); + data.put(new Point(8.125, 48.125), 11.597820281982422); + + GridData grid = new GridData(data, 0.25, 0.125); + + // 测试正常点 + System.out.printf("Normal: %.8f%n", + grid.interpolate(7.9, 47.9)); + + // 测试边界点(wx/wy 接近 1) +// System.out.printf("Edge case: %.8f%n", +// grid.interpolate(-53.625, -36.375)); // 应等于 NE 点值 + } +} diff --git a/jeecg-module-transport/src/main/java/org/jeecg/vo/ContributionAnalysisVO.java b/jeecg-module-transport/src/main/java/org/jeecg/vo/ContributionAnalysisVO.java new file mode 100644 index 0000000..4fcb573 --- /dev/null +++ b/jeecg-module-transport/src/main/java/org/jeecg/vo/ContributionAnalysisVO.java @@ -0,0 +1,39 @@ +package org.jeecg.vo; + +import lombok.Data; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * 贡献分析数据VO + */ +@Data +public class ContributionAnalysisVO { + + /** + * X轴台站每天的浓度值数据 + */ + private Map stationEveryDayConcDatas; + + /** + * 核设施每天的浓度值数据 + */ + private Map> facilityEveryDayConcDatas; + + /** + * 涉及的核设施名称 + */ + private List nuclideNames; + + /** + * 饼图数据 + */ + private Map pipeChartData; + + /** + * 总浓度值 + */ + private Double totalConc; +} diff --git a/jeecg-module-transport/src/main/java/org/jeecg/vo/TaskStationsVO.java b/jeecg-module-transport/src/main/java/org/jeecg/vo/TaskStationsVO.java new file mode 100644 index 0000000..933c283 --- /dev/null +++ b/jeecg-module-transport/src/main/java/org/jeecg/vo/TaskStationsVO.java @@ -0,0 +1,17 @@ +package org.jeecg.vo; + +import lombok.Data; + +@Data +public class TaskStationsVO { + + private Integer stationId; + + private Integer stationNum; + + private String stationCode; + + private Double lon; + + private Double lat; +} diff --git a/jeecg-module-weather/src/main/java/org/jeecg/controller/WeatherTaskController.java b/jeecg-module-weather/src/main/java/org/jeecg/controller/WeatherTaskController.java index 34bcb48..b5b9e7f 100644 --- a/jeecg-module-weather/src/main/java/org/jeecg/controller/WeatherTaskController.java +++ b/jeecg-module-weather/src/main/java/org/jeecg/controller/WeatherTaskController.java @@ -12,6 +12,7 @@ import org.jeecg.common.validgroup.InsertGroup; import org.jeecg.common.validgroup.UpdateGroup; import org.jeecg.modules.base.entity.WeatherTask; import org.jeecg.service.WeatherTaskService; +import org.jeecg.vo.ForecastFileVO; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -44,7 +45,8 @@ public class WeatherTaskController { @AutoLog(value = "新增天气预测任务") @Operation(summary = "新增天气预测任务") @PostMapping("create") - public Result create(@RequestBody @Validated(value = InsertGroup.class) WeatherTask weatherTask){ + public Result create(@Validated(value = InsertGroup.class) WeatherTask weatherTask){ + System.out.println(weatherTask); weatherTaskService.cteate(weatherTask); return Result.OK(); } @@ -59,7 +61,7 @@ public class WeatherTaskController { @AutoLog(value = "修改天气预测任务") @Operation(summary = "修改天气预测任务") @PutMapping("update") - public Result update(@RequestBody @Validated(value = UpdateGroup.class) WeatherTask weatherTask){ + public Result update(@Validated(value = UpdateGroup.class) WeatherTask weatherTask){ weatherTaskService.update(weatherTask); return Result.OK(); } @@ -72,6 +74,14 @@ public class WeatherTaskController { return Result.OK(); } + @AutoLog(value = "启动任务") + @Operation(summary = "启动任务") + @PutMapping("runTask") + public Result runTask(@NotBlank(message = "任务ID不能为空") String taskId){ + weatherTaskService.runTask(taskId); + return Result.OK(); + } + @AutoLog(value = "获取天气预测任务过程日志") @Operation(summary = "获取天气预测任务过程日志") @GetMapping("getTaskLog") diff --git a/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherDataService.java b/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherDataService.java index d8bbf5a..b650e7d 100644 --- a/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherDataService.java +++ b/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherDataService.java @@ -3,15 +3,11 @@ package org.jeecg.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.IService; import org.jeecg.common.system.query.PageRequest; -import org.jeecg.modules.base.entity.StasDataSource; import org.jeecg.modules.base.entity.WeatherData; import org.jeecg.vo.*; - -import java.io.IOException; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; -import java.util.Map; public interface WeatherDataService extends IService { @@ -54,4 +50,10 @@ public interface WeatherDataService extends IService { * 处理静态气象数据入库接口,比上传快 */ void handleStaticDataToDB(String path,Integer dataSource); + + /** + * 保存模型预测好的文件信息到WeatherData + * @param weatherData + */ + void saveFileInfo(WeatherData weatherData); } diff --git a/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherTaskService.java b/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherTaskService.java index e362145..2158ca8 100644 --- a/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherTaskService.java +++ b/jeecg-module-weather/src/main/java/org/jeecg/service/WeatherTaskService.java @@ -53,7 +53,7 @@ public interface WeatherTaskService extends IService { * 运行任务 * @param id */ - void runTask(Integer id); + void runTask(String id); /** * 获取任务运行日志 @@ -61,4 +61,31 @@ public interface WeatherTaskService extends IService { * @return */ List getTaskLog(String taskId); + + /** + * 保存任务过程日志 + * @param log + */ + void saveLog(WeatherTaskLog log); + + /** + * 修改任务运行状态 + * @param taskId + * @param taskStatus + */ + void updateTaskStatus(String taskId,Integer taskStatus); + + /** + * 根据任务id删除任务日志 + * @param taskId + */ + void deleteTaskLog(String taskId); + + /** + * 修改任务状态为结束 + * @param taskId + * @param timeConsuming + */ + void updateTaskStatusToCompleted(String taskId, Double timeConsuming); + } diff --git a/jeecg-module-weather/src/main/java/org/jeecg/service/impl/WeatherDataServiceImpl.java b/jeecg-module-weather/src/main/java/org/jeecg/service/impl/WeatherDataServiceImpl.java index ed1802e..5818c52 100644 --- a/jeecg-module-weather/src/main/java/org/jeecg/service/impl/WeatherDataServiceImpl.java +++ b/jeecg-module-weather/src/main/java/org/jeecg/service/impl/WeatherDataServiceImpl.java @@ -2,6 +2,7 @@ package org.jeecg.service.impl; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.StringPool; @@ -435,8 +436,7 @@ public class WeatherDataServiceImpl extends ServiceImpl implements WeatherTaskService { private final WeatherTaskLogMapper weatherTaskLogMapper; + private final SystemStorageProperties systemStorageProperties; + private final WeatherDataMapper weatherDataMapper; /** * 分页查询任务列表 @@ -73,11 +84,40 @@ public class WeatherTaskServiceImpl extends ServiceImpl checkDateWrapper = new LambdaQueryWrapper<>(); checkDateWrapper.eq(WeatherTask::getStartDate,weatherTask.getStartDate()); checkDateWrapper.eq(WeatherTask::getStartTime,weatherTask.getStartTime()); + checkDateWrapper.eq(WeatherTask::getPredictionModel,weatherTask.getPredictionModel()); WeatherTask checkDateResult = this.getOne(checkDateWrapper); if(Objects.nonNull(checkDateResult)){ throw new RuntimeException("此当前预测时间为参数的任务已存在"); } + //手动获取id + String id = IdWorker.getIdStr(); + weatherTask.setId(id); weatherTask.setTaskStatus(WeatherTaskStatusEnum.NOT_STARTED.getKey()); + if (WeatherForecastDatasourceEnum.LOCATION_FILE.getKey().equals(weatherTask.getDataSources())){ + try { + MultipartFile file = weatherTask.getFile(); + //构造文件名称 + StringBuilder fileName = new StringBuilder(); + fileName.append(file.getOriginalFilename().substring(0,file.getOriginalFilename().lastIndexOf("."))); + fileName.append("_"+id+"."); + fileName.append(WeatherFileSuffixEnum.GRIB.getValue()); + + //文件保存路径 + StringBuilder filePath = new StringBuilder(); + filePath.append(this.systemStorageProperties.getPanguModelExecPath()); + filePath.append(File.separator); + filePath.append(fileName); + + File storageFile = new File(filePath.toString()); + if (storageFile.exists()){ + storageFile.delete(); + } + file.transferTo(storageFile); + weatherTask.setInputFile(fileName.toString()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } this.save(weatherTask); } @@ -115,8 +155,9 @@ public class WeatherTaskServiceImpl extends ServiceImpl checkDateWrapper = new LambdaQueryWrapper<>(); checkDateWrapper.eq(WeatherTask::getStartDate,weatherTask.getStartDate()); checkDateWrapper.eq(WeatherTask::getStartTime,weatherTask.getStartTime()); + checkDateWrapper.eq(WeatherTask::getPredictionModel,weatherTask.getPredictionModel()); WeatherTask checkDateResult = this.getOne(checkDateWrapper); - if(Objects.nonNull(checkDateResult) && !weatherTask.getId().equals(checkNameResult.getId())){ + if(Objects.nonNull(checkDateResult) && Objects.nonNull(checkNameResult) && !weatherTask.getId().equals(checkNameResult.getId())){ throw new RuntimeException("此当前预测时间为参数的任务已存在"); } queryResult.setTaskName(weatherTask.getTaskName()); @@ -125,7 +166,35 @@ public class WeatherTaskServiceImpl extends ServiceImpl queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(WeatherTaskLog::getTaskId,taskId); + this.weatherTaskLogMapper.delete(queryWrapper); + } + + /** + * 修改任务状态为结束 + * + * @param taskId + * @param timeConsuming + */ + @Transactional(rollbackFor = RuntimeException.class) + @Override + public void updateTaskStatusToCompleted(String taskId, Double timeConsuming) { + WeatherTask weatherTask = this.baseMapper.selectById(taskId); + weatherTask.setTaskStatus(WeatherTaskStatusEnum.COMPLETED.getKey()); + weatherTask.setTimeConsuming(timeConsuming); + this.updateById(weatherTask); + } } diff --git a/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressEvent.java b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressEvent.java new file mode 100644 index 0000000..11440f0 --- /dev/null +++ b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressEvent.java @@ -0,0 +1,28 @@ +package org.jeecg.task; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 日志事件 + */ +@Data +@NoArgsConstructor +public class ProgressEvent implements Serializable{ + + private static final long serialVersionUID = 1L; + + private String taskId; + + /** + * 过程日志 + */ + private String content; + + public ProgressEvent(String taskId, String content) { + this.taskId = taskId; + this.content = content; + } +} diff --git a/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressMonitor.java b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressMonitor.java new file mode 100644 index 0000000..63b3861 --- /dev/null +++ b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressMonitor.java @@ -0,0 +1,50 @@ +package org.jeecg.task; + +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jeecg.modules.base.entity.WeatherTaskLog; +import org.jeecg.service.WeatherTaskService; +import org.springframework.stereotype.Component; +import java.util.Objects; + +/** + * 日志监测线程 + * @author 86187 + * + */ +@Component +@RequiredArgsConstructor +@Slf4j +public class ProgressMonitor{ + + private final WeatherTaskService weatherTaskService; + + @PostConstruct + public void start() { + ProgressMonitorThread monitor = new ProgressMonitorThread(); + monitor.setName("气象预测日志监测线程"); + monitor.start(); + } + + private class ProgressMonitorThread extends Thread{ + + @Override + public void run() { + for(;;) { + try { + ProgressEvent event = ProgressQueue.getInstance().take(); + if(Objects.nonNull(event)) { + WeatherTaskLog log = new WeatherTaskLog(); + log.setTaskId(event.getTaskId()); + log.setLogContent(event.getContent()); + weatherTaskService.saveLog(log); + } + }catch (Exception e){ + log.error("气象预测日志存储线程异常,日志存储失败,原因为:{}",e.getMessage()); + e.printStackTrace(); + } + } + } + } +} \ No newline at end of file diff --git a/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressQueue.java b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressQueue.java new file mode 100644 index 0000000..058f07b --- /dev/null +++ b/jeecg-module-weather/src/main/java/org/jeecg/task/ProgressQueue.java @@ -0,0 +1,39 @@ +package org.jeecg.task; + +import java.util.LinkedList; + +/** + * 日志队列 + */ +public class ProgressQueue { + + private final LinkedList queue = new LinkedList<>(); + + private static ProgressQueue progressQueue = new ProgressQueue(); + + public static ProgressQueue getInstance() { + return progressQueue; + } + + public void offer(ProgressEvent event) { + synchronized (queue) { + queue.addLast(event); + queue.notify(); + } + } + + public ProgressEvent take(){ + synchronized (queue) { + if(queue.isEmpty()) { + try { + queue.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + ProgressEvent event = queue.removeFirst(); + return event; + } + } + +} diff --git a/jeecg-module-weather/src/main/java/org/jeecg/task/WeatherTaskExec.java b/jeecg-module-weather/src/main/java/org/jeecg/task/WeatherTaskExec.java new file mode 100644 index 0000000..b1aea08 --- /dev/null +++ b/jeecg-module-weather/src/main/java/org/jeecg/task/WeatherTaskExec.java @@ -0,0 +1,321 @@ +package org.jeecg.task; + +import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.date.LocalDateTimeUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.StopWatch; +import org.jeecg.common.constant.enums.WeatherDataSourceEnum; +import org.jeecg.common.constant.enums.WeatherForecastDatasourceEnum; +import org.jeecg.common.constant.enums.WeatherTaskStatusEnum; +import org.jeecg.common.exception.JeecgFileUploadException; +import org.jeecg.common.properties.SystemStorageProperties; +import org.jeecg.common.util.NcUtil; +import org.jeecg.modules.base.entity.WeatherData; +import org.jeecg.modules.base.entity.WeatherTask; +import org.jeecg.modules.base.entity.WeatherTaskLog; +import org.jeecg.modules.base.mapper.WeatherDataMapper; +import org.jeecg.service.WeatherDataService; +import org.jeecg.service.WeatherTaskService; + +import java.io.*; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +/** + * 气象预测任务执行线程 + */ +public class WeatherTaskExec extends Thread{ + + private WeatherTask weatherTask; + private WeatherTaskService weatherTaskService; + private SystemStorageProperties systemStorageProperties; + private WeatherDataMapper weatherDataMapper; + + /** + * 初始化 + */ + public void init(WeatherTask weatherTask, + WeatherTaskService weatherTaskService, + SystemStorageProperties systemStorageProperties, + WeatherDataMapper weatherDataMapper){ + this.weatherTask = weatherTask; + this.weatherTaskService = weatherTaskService; + this.systemStorageProperties = systemStorageProperties; + this.weatherDataMapper = weatherDataMapper; + } + + @Override + public void run() { + this.execute(); + } + + /** + * 执行任务 + */ + public void execute() { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + try{ + //修改任务状态为执行中 + this.weatherTaskService.updateTaskStatus(this.weatherTask.getId(), WeatherTaskStatusEnum.IN_OPERATION.getKey()); + //如果此任务已存在历史日志,先清除 + this.weatherTaskService.deleteTaskLog(this.weatherTask.getId()); + //执行模拟 + this.execSimulation(); + }catch (Exception e){ + String taskErrorLog = "任务执行失败,原因:"+e.getMessage(); + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),taskErrorLog)); + throw e; + }finally { + //添加任务耗时 + stopWatch.stop(); + long seconds = stopWatch.getTime(TimeUnit.SECONDS); + double min = seconds/60D; + BigDecimal bgMin = new BigDecimal(min); + BigDecimal result = bgMin.setScale(2, RoundingMode.HALF_UP); + this.weatherTaskService.updateTaskStatusToCompleted(this.weatherTask.getId(),result.doubleValue()); + String taskCompletedLog = "任务执行完成,耗时:"+result+"分钟"; + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),taskCompletedLog)); + } + } + + /** + * 执行模拟 + */ + private void execSimulation(){ + try { + //处理开始日志 + String forecastModel = ""; + String startLogFormat = ""; + String startTimeFormat = LocalDateTimeUtil.format(this.weatherTask.getStartDate(),"yyyy-MM-dd HH:mm:ss"); + String forecastTime = this.weatherTask.getLeadTime().toString(); + if (WeatherDataSourceEnum.GRAPHCAST.getKey().equals(this.weatherTask.getPredictionModel())){ + forecastModel = WeatherDataSourceEnum.GRAPHCAST.getValue(); + }else if(WeatherDataSourceEnum.PANGU.getKey().equals(this.weatherTask.getPredictionModel())){ + forecastModel = WeatherDataSourceEnum.PANGU.getValue(); + } + if(this.weatherTask.getDataSources().equals(WeatherForecastDatasourceEnum.CDS.getKey())){ + startLogFormat = "任务开始,本次使用预测模型为:%s,预测开始时间为:%s,预测时长为:%s,前置数据采用CDS在线数据"; + }else { + startLogFormat = "任务开始,本次使用预测模型为:%s,预测开始时间为:%s,预测时长为:%s,前置数据采用离线数据:"+this.weatherTask.getInputFile(); + } + String startLog = String.format(startLogFormat,forecastModel,startTimeFormat,forecastTime); + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),startLog)); + + //处理运行命令 + List command = new ArrayList<>(); + String aiModelsPath = systemStorageProperties.getAiModelsPath()+File.separator+"ai-models"; + String fileName = "panguweather_output_"+this.weatherTask.getId()+".grib"; + if(this.weatherTask.getDataSources().equals(WeatherForecastDatasourceEnum.CDS.getKey())){ + command.add(aiModelsPath); + command.add("--input"); + command.add("cds"); + command.add("--date"); + command.add(LocalDateTimeUtil.format(this.weatherTask.getStartDate(),"yyyyMMdd")); + command.add("--time"); + command.add(this.weatherTask.getStartTime().toString()); + command.add("--lead-time"); + command.add(this.weatherTask.getLeadTime().toString()); + command.add("--path"); + command.add(fileName); + command.add("--assets"); + command.add("assets-panguweather"); + command.add("panguweather"); + }else if (this.weatherTask.getDataSources().equals(WeatherForecastDatasourceEnum.LOCATION_FILE.getKey())){ + //离线文件还需处理 + command.add(aiModelsPath); + command.add("--file"); + command.add(this.weatherTask.getInputFile()); + command.add("--lead-time"); + command.add(this.weatherTask.getLeadTime().toString()); + command.add("--path"); + command.add(fileName); + command.add("--assets"); + command.add("assets-panguweather"); + command.add("panguweather"); + } + String execLog = "执行任务命令,开始模拟,命令为:"+command; + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),execLog)); + ProcessBuilder processBuilder = new ProcessBuilder(command); + processBuilder.directory(new File(systemStorageProperties.getPanguModelExecPath())); + processBuilder.redirectErrorStream(true); + Process process = processBuilder.start(); + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), "UTF-8")); + //读取输出日志 + String line; + while ((line = reader.readLine()) != null) { + if(StrUtil.isNotBlank(line)){ + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),line)); + } + } + //等待进程结束 + process.waitFor(); + + String handleGribLog = "预测结束,开始处理grib文件"; + ProgressQueue.getInstance().offer(new ProgressEvent(this.weatherTask.getId(),handleGribLog)); + LocalDateTime startTime = this.weatherTask.getStartDate().atTime(this.weatherTask.getStartTime(), 0, 0); + //grib_copy命令 + String gribCopyCommandPath = systemStorageProperties.getAiModelsPath()+File.separator+"grib_copy"; + //grib_set命令 + String gribSetCommandPath = systemStorageProperties.getAiModelsPath()+File.separator+"grib_set"; + //需删除文件 + List delFiles = new ArrayList<>(); + //需移动文件 + List moveFiles = new ArrayList<>(); + //公用gribProcessBuilder + ProcessBuilder gribProcessBuilder = new ProcessBuilder(); + gribProcessBuilder.directory(new File(systemStorageProperties.getPanguModelExecPath())); + //定义名称后缀,执行grib_set命令时去除,否则命名会冲突 + String gribFileSuffix = "_not_grib_set"; + //把grib文件切割成每6小时一份 + int step = 6; + int i=this.weatherTask.getStartTime(); + while(i <= this.weatherTask.getLeadTime()){ + String gribCopyFileName = "panguweather_"+LocalDateTimeUtil.format(startTime,"yyyyMMddHH")+gribFileSuffix+".grib"; + delFiles.add(gribCopyFileName); + //切割grib文件命令 + List gribCopyCommand = new ArrayList<>(); + gribCopyCommand.add(gribCopyCommandPath); + gribCopyCommand.add("-w"); + gribCopyCommand.add("step="+i); + gribCopyCommand.add(fileName); + gribCopyCommand.add(gribCopyFileName); + gribProcessBuilder.command(gribCopyCommand); + Process gribCopyProcess = gribProcessBuilder.start(); + gribCopyProcess.waitFor(); + + //重新设置reftime信息 + String gribSetFileName = "panguweather_"+LocalDateTimeUtil.format(startTime,"yyyyMMddHH")+".grib"; + moveFiles.add(gribSetFileName); + String date = LocalDateTimeUtil.format(startTime,"yyyyMMdd"); + String time = LocalDateTimeUtil.format(startTime,"HHmm"); + List gribSetCommand = new ArrayList<>(); + gribSetCommand.add(gribSetCommandPath); + gribSetCommand.add("-s"); + gribSetCommand.add("dataDate="+date+",dataTime="+time+",endStep="+0); + gribSetCommand.add(gribCopyFileName); + gribSetCommand.add(gribSetFileName); + gribProcessBuilder.command(gribSetCommand); + Process gribSetProcess = gribProcessBuilder.start(); + gribSetProcess.waitFor(); + i+=step; + startTime = startTime.plusHours(step); + } + if (CollUtil.isNotEmpty(moveFiles)) { + for (String moveFile : moveFiles) { + File srcFile = new File(systemStorageProperties.getPanguModelExecPath()+File.separator+moveFile); + if (srcFile.exists()) { + File targetFile = new File(this.getPanguWeatherPath()+File.separator+File.separator+moveFile); + FileUtil.move(srcFile,targetFile,true); + //保存生成的气象数据存储到数据库 + this.saveGribInfoToDB(targetFile); + } + } + } + //删除gribCopy产生的文件 + for (String delFile : delFiles) { + File srcFile = new File(systemStorageProperties.getPanguModelExecPath()+File.separator+delFile); + if (srcFile.exists()) { + srcFile.delete(); + } + } + //删除预测的结果文件 + String outputFilePath = systemStorageProperties.getPanguModelExecPath()+File.separator+fileName; + File outputFile = new File(outputFilePath); + if(outputFile.exists()){ + outputFile.delete(); + } + + //如果是本地文件进行预测,预测结束后删除文件 + if(this.weatherTask.getDataSources().equals(WeatherForecastDatasourceEnum.LOCATION_FILE.getKey())){ + String inputFilePath = systemStorageProperties.getPanguModelExecPath()+File.separator+this.weatherTask.getInputFile(); + File inputFile = new File(inputFilePath); + if(inputFile.exists()){ + inputFile.delete(); + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 保存生成的气象数据存储到数据库 + * @param file + */ + private void saveGribInfoToDB(File file){ + //获取文件数据开始日期 + String reftime = NcUtil.getReftime(file.getAbsolutePath()); + if(StringUtils.isBlank(reftime)) { + throw new JeecgFileUploadException("解析气象文件起始时间数据异常,此文件可能损坏"); + } + //计算文件大小M + BigDecimal divideVal = new BigDecimal("1024"); + BigDecimal bg = new BigDecimal(file.length()); + BigDecimal fileSize = bg.divide(divideVal).divide(divideVal).setScale(2, RoundingMode.HALF_UP); + //处理文件数据开始时间 + Instant instant = Instant.parse(reftime); + LocalDateTime utcDateTime = LocalDateTime.ofInstant(instant, ZoneId.of("UTC")); + //计算文件MD5值 + String md5Val = ""; + try (InputStream is = new FileInputStream(file.getAbsolutePath())) { + md5Val = DigestUtils.md5Hex(is); + }catch (Exception e){ + throw new RuntimeException(file.getName()+"MD5值计算失败"); + } + //校验文件是否存在,存在删除从新新增 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(WeatherData::getFileName,file.getName()); + WeatherData queryResult = weatherDataMapper.selectOne(queryWrapper); + if(Objects.nonNull(queryResult)){ + weatherDataMapper.deleteById(queryResult.getId()); + } + //构建文件信息 + WeatherData weatherData = new WeatherData(); + weatherData.setFileName(file.getName()); + weatherData.setFileSize(fileSize.doubleValue()); + weatherData.setFileExt(file.getName().substring(file.getName().lastIndexOf(".")+1)); + weatherData.setDataStartTime(utcDateTime); + weatherData.setDataSource(weatherTask.getPredictionModel()); + weatherData.setFilePath(file.getAbsolutePath()); + weatherData.setMd5Value(md5Val); + weatherData.setShareTotal(1); + weatherDataMapper.insert(weatherData); + } + + /** + * 获取盘古数据存储路径 + * @return + */ + private String getPanguWeatherPath(){ + StringBuilder sb = new StringBuilder(); + sb.append(this.systemStorageProperties.getRootPath()); + sb.append(File.separator); + sb.append(this.systemStorageProperties.getPangu()); + return sb.toString(); + } + + /** + * 获取盘古数据存储路径 + * @return + */ + private String getGraphcastWeatherPath(){ + StringBuilder sb = new StringBuilder(); + sb.append(this.systemStorageProperties.getRootPath()); + sb.append(File.separator); + sb.append(this.systemStorageProperties.getGraphcast()); + return sb.toString(); + } +} diff --git a/jeecg-module-weather/src/main/java/org/jeecg/vo/ForecastFileVO.java b/jeecg-module-weather/src/main/java/org/jeecg/vo/ForecastFileVO.java new file mode 100644 index 0000000..d1457a6 --- /dev/null +++ b/jeecg-module-weather/src/main/java/org/jeecg/vo/ForecastFileVO.java @@ -0,0 +1,26 @@ +package org.jeecg.vo; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import org.springframework.web.multipart.MultipartFile; +import java.io.Serializable; + +/** + * 预测文件上传VO + */ +@Data +public class ForecastFileVO implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 任务id + */ + private String taskId; + + /** + * 上传文件 + */ + @JSONField(serialize = false) + private MultipartFile file; +}