2026-03-25提交:新增事件ID关联,上传附件下载附件接口

This commit is contained in:
junzhangfm 2026-03-25 15:55:15 +08:00
parent c7cbf45c2d
commit 6eaa7e3d0b
17 changed files with 318 additions and 38 deletions

View File

@ -5,6 +5,8 @@ import com.southern.power.grid.entity.*;
import com.southern.power.grid.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
@ -39,6 +41,9 @@ public class DnerController {
@Autowired
private IRegionalWeatherDataService regionalWeatherDataService;
@Autowired
private DnerEventAttachmentService dnerEventAttachmentService;
@PostMapping("/area-tree/query")
public Result<List<AreaTreeVO>> queryAreaTree(@RequestBody @Valid AreaTreeReq req) {
return Result.success(dnerSiteAreaConfigurationService.queryAreaTree(req));
@ -82,9 +87,20 @@ public class DnerController {
* @return 返回结果
*/
@PostMapping("/excel/import")
public Result<String> importExcel(@RequestParam("file") MultipartFile file) {
String taskNo = importTaskService.importExcel(file);
return Result.success(taskNo);
public Result<String> importExcel(@RequestParam("file") MultipartFile file,
@RequestParam Long eventId) {
return Result.success(importTaskService.importExcel(file, eventId));
}
/**
* 下载附件
*
* @param attachmentId 附件ID
* @return 响应体
*/
@GetMapping("/excel/download/{attachmentId}")
public ResponseEntity<Resource> downloadExcel(@PathVariable Long attachmentId) {
return dnerEventAttachmentService.downloadExcel(attachmentId);
}
// 2. 查询导入进度

View File

@ -1,8 +1,5 @@
package com.southern.power.grid.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.southern.power.grid.common.Result;
import com.southern.power.grid.entity.DnerEvent;
import com.southern.power.grid.entity.DnerEventVO;
@ -10,7 +7,6 @@ import com.southern.power.grid.service.DnerEventService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**

View File

@ -31,6 +31,11 @@ public class DnerEventAttachment {
*/
private String fileName;
/**
* 存储在本地的唯一文件名
*/
private String storedFileName;
/**
* 文件存储路径
*/

View File

@ -23,6 +23,11 @@ public class DnerHourlyPowerOutageEvent {
@TableId(type = IdType.AUTO)
private Long id;
/**
* 事件ID
*/
private Long eventId;
/**
* 地区编码
*/

View File

@ -1,5 +1,6 @@
package com.southern.power.grid.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@ -31,4 +32,16 @@ public class ImportTask {
private Date createTime;
private Date updateTime;
/**
* 任务关联的文件ID
*/
private Long fileId;
// =============== 非数据库参数 ==================
/**
* 事件ID
*/
@TableField(exist = false)
private Long eventId;
}

View File

@ -12,6 +12,7 @@ import com.southern.power.grid.service.impl.HourlyOutageExcelProcessService;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.InputStream;
@ -31,7 +32,8 @@ import java.util.concurrent.atomic.AtomicInteger;
public class DataExcelListener extends AnalysisEventListener<DataExcelEntity> {
// 每批次大小
private static final int BATCH_SIZE = 500;
@Value("${file.import.batch-size}")
private int BATCH_SIZE;
// 缓存数据
private final List<DataExcelEntity> cacheList = new ArrayList<>(BATCH_SIZE);
@ -92,7 +94,7 @@ public class DataExcelListener extends AnalysisEventListener<DataExcelEntity> {
private void batchInsert() {
try {
// 批量插入500条
hourlyOutageExcelProcessService.process(cacheList);
hourlyOutageExcelProcessService.process(cacheList, task.getEventId());
success += cacheList.size();
} catch (Exception e) {
fail += cacheList.size();

View File

@ -2,6 +2,8 @@ package com.southern.power.grid.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.southern.power.grid.entity.DnerEventAttachment;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
/**
* 事件附件 服务接口
@ -11,4 +13,11 @@ import com.southern.power.grid.entity.DnerEventAttachment;
**/
public interface DnerEventAttachmentService extends IService<DnerEventAttachment> {
/**
* 下载附件
*
* @param attachmentId 附件ID
* @return 结果
*/
ResponseEntity<Resource> downloadExcel(Long attachmentId);
}

View File

@ -0,0 +1,21 @@
package com.southern.power.grid.service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 文件操作 服务层
*
* @author: junzhangfm
* @date: 2026/3/25
**/
public interface IFileService {
/**
* 上传excel文件
*
* @param file 文件
* @param eventId 事件ID
*/
Long uploadExcel(MultipartFile file, Long eventId) throws IOException;
}

View File

@ -10,7 +10,7 @@ import org.springframework.web.multipart.MultipartFile;
* @date: 2026/3/16
**/
public interface IImportTaskService {
String importExcel(MultipartFile file);
String importExcel(MultipartFile file, Long eventId);
ImportTask getProgress(String taskNo);
}

View File

@ -4,6 +4,18 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.southern.power.grid.dao.DnerEventAttachmentMapper;
import com.southern.power.grid.entity.DnerEventAttachment;
import com.southern.power.grid.service.DnerEventAttachmentService;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.springframework.stereotype.Service;
/**
@ -17,4 +29,47 @@ public class DnerEventAttachmentServiceImpl
extends ServiceImpl<DnerEventAttachmentMapper, DnerEventAttachment>
implements DnerEventAttachmentService {
@Override
public ResponseEntity<Resource> downloadExcel(Long attachmentId) {
// 1. 根据ID从数据库查询文件记录
DnerEventAttachment attachment = getById(attachmentId);
if (attachment == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
// 2. 获取文件真实路径
String filePath = attachment.getFilePath();
File file = new File(filePath);
// 3. 校验文件是否存在
if (!file.exists()) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null);
}
// 4. 封装文件资源
Resource resource = new FileSystemResource(file);
// 5. 处理中文名下载乱码关键
String originalFilename = attachment.getFileName();
String encodeFileName = null;
try {
encodeFileName = URLEncoder.encode(originalFilename, StandardCharsets.UTF_8.toString())
.replace("+", "%20");
} catch (UnsupportedEncodingException e) {
log.error("URLEncoder.encode error!");
throw new RuntimeException(e);
}
// 6. 构建响应头
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + encodeFileName + "\"; filename*=UTF-8''" + encodeFileName);
// 7. 返回文件流
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(file.length())
.body(resource);
}
}

View File

@ -0,0 +1,85 @@
package com.southern.power.grid.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.southern.power.grid.dao.DnerDailyPowerOutageEventMapper;
import com.southern.power.grid.dao.DnerEventAttachmentMapper;
import com.southern.power.grid.dao.DnerHourlyPowerOutageEventMapper;
import com.southern.power.grid.entity.DnerDailyPowerOutageEvent;
import com.southern.power.grid.entity.DnerEventAttachment;
import com.southern.power.grid.entity.DnerHourlyPowerOutageEvent;
import com.southern.power.grid.service.IFileService;
import com.southern.power.grid.utils.FileUploadUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
/**
* 文件操作 服务实现类
*
* @author: junzhangfm
* @date: 2026/3/25
**/
@Service
@Slf4j
public class FileServiceImpl implements IFileService {
// 读取配置文件中的存储路径
@Value("${file.upload.path}")
private String uploadPath;
@Autowired
private DnerEventAttachmentMapper dnerEventAttachmentMapper;
@Autowired
private DnerHourlyPowerOutageEventMapper dnerHourlyPowerOutageEventMapper;
@Autowired
private DnerDailyPowerOutageEventMapper dnerDailyPowerOutageEventMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Long uploadExcel(MultipartFile file, Long eventId) throws IOException {
// 1. 校验是否是 Excel
if (!FileUploadUtil.isExcel(file)) {
throw new RuntimeException("Only .xlsx or .xls format Excel files are allowed to be uploaded.");
}
// 2. 获取文件信息
String originalFilename = file.getOriginalFilename();
String suffix = FileUploadUtil.getFileSuffix(originalFilename);
String storedFilename = FileUploadUtil.generateUniqueFileName(suffix);
String absolutePath = uploadPath + storedFilename;
// 3. 保存到本地
FileUploadUtil.saveFile(file, absolutePath);
// 4. 记录到数据库
DnerEventAttachment record = new DnerEventAttachment();
record.setFileName(originalFilename);
record.setStoredFileName(storedFilename);
record.setFilePath(absolutePath);
record.setFileSize(file.getSize());
record.setFileType(suffix);
record.setCreateTime(LocalDateTime.now());
record.setCreator("admin"); // 默认admin
record.setEventId(eventId);
// 最新附件其他附件设置为0
record.setIsLatest(1);
dnerEventAttachmentMapper.update(new UpdateWrapper<DnerEventAttachment>().set("is_latest", 0)
.eq("event_id", eventId));
// 插入记录
dnerEventAttachmentMapper.insert(record);
// 删除旧的分时图和K线图数据
dnerHourlyPowerOutageEventMapper.delete(
new QueryWrapper<DnerHourlyPowerOutageEvent>().eq("event_id", eventId));
dnerDailyPowerOutageEventMapper.delete(
new QueryWrapper<DnerDailyPowerOutageEvent>().eq("event_id", eventId));
return record.getId();
}
}

View File

@ -57,7 +57,7 @@ public class HourlyOutageExcelProcessService {
* 对外主入口处理一批 Excel 解析后的数据按逻辑补齐气象数据并批量入库
*/
@Transactional
public void process(List<DataExcelEntity> excelRows) {
public void process(List<DataExcelEntity> excelRows, Long eventId) {
// 1. 加载南网区划配置表
loadNwAreaConfig();
// 2. 加载气象区划配置表
@ -121,6 +121,7 @@ public class HourlyOutageExcelProcessService {
if (existing == null) {
// 不存在创建新实体并放入 Map
DnerHourlyPowerOutageEvent entity = buildHourlyEvent(row, districtCode, regionalWeatherData);
entity.setEventId(eventId);
toInsertMap.put(key, entity);
} else {
// 已存在执行累加逻辑

View File

@ -1,11 +1,13 @@
package com.southern.power.grid.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.southern.power.grid.dao.ImportTaskMapper;
import com.southern.power.grid.entity.ImportTask;
import com.southern.power.grid.enums.ImportTaskStatusEnum;
import com.southern.power.grid.service.IFileService;
import com.southern.power.grid.service.IImportTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -32,19 +34,11 @@ public class ImportTaskServiceImpl extends ServiceImpl<ImportTaskMapper, ImportT
@Autowired
private AsyncImportServiceImpl asyncImportService;
@Autowired
private IFileService fileService;
@Override
public String importExcel(MultipartFile file) {
// 通过hutool工具类生成唯一任务号
String taskNo = IdUtil.fastSimpleUUID();
// 创建任务
ImportTask task = new ImportTask();
task.setTaskNo(taskNo);
task.setStatus(ImportTaskStatusEnum.WAITING.getCode());
task.setTotal(0);
importTaskMapper.insert(task);
// 异步执行导入
public String importExcel(MultipartFile file, Long eventId) {
// 把上传的文件流转成 **字节数组输入流**保存到内存避免Tomcat删除
InputStream inputStream;
try {
@ -53,6 +47,28 @@ public class ImportTaskServiceImpl extends ServiceImpl<ImportTaskMapper, ImportT
log.error("file exception!");
throw new RuntimeException(e);
}
// 同步上传文件到本地拒绝异步tomcat可能会删除file对象导致上传异常
Long fileId;
try {
fileId = fileService.uploadExcel(file, eventId);
} catch (IOException e) {
log.error("ImportTaskServiceImpl fileService.uploadExcel fail! {}", e.getMessage());
throw new RuntimeException(e);
}
// 通过hutool工具类生成唯一任务号
String taskNo = IdUtil.fastSimpleUUID();
// 创建任务
ImportTask task = new ImportTask();
task.setTaskNo(taskNo);
task.setStatus(ImportTaskStatusEnum.WAITING.getCode());
task.setTotal(0);
task.setFileId(fileId);
task.setEventId(eventId);
importTaskMapper.insert(task);
// 异步执行导入
asyncImportService.doAsyncImport(inputStream, task);
// 立即返回任务ID给前端

View File

@ -0,0 +1,44 @@
package com.southern.power.grid.utils;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
/**
* 文件上传工具类
*
* @author: junzhangfm
* @date: 2026/3/25
**/
public class FileUploadUtil {
// 校验是否是 Excel 文件
public static boolean isExcel(MultipartFile file) {
String suffix = getFileSuffix(file.getOriginalFilename());
return "xlsx".equals(suffix) || "xls".equals(suffix);
}
// 获取文件后缀
public static String getFileSuffix(String fileName) {
if (fileName == null || !fileName.contains(".")) {
return "";
}
return fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
}
// 生成唯一文件名防止覆盖
public static String generateUniqueFileName(String suffix) {
return UUID.randomUUID().toString().replace("-", "") + "." + suffix;
}
// 保存文件到本地
public static void saveFile(MultipartFile file, String absolutePath) throws IOException {
File dest = new File(absolutePath);
// 创建父目录
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
file.transferTo(dest);
}
}

View File

@ -24,6 +24,14 @@ spring:
max-file-size: 100MB # 单个文件最大 默认1M
max-request-size: 100MB # 整个请求最大 默认10M
# 自定义本地文件存储路径(绝对路径,不要放在项目里!)
file:
upload:
path: E:/springboot-uploads/excel/ # Windows
# path: /home/springboot/uploads/excel/ # Linux
import:
batch-size: 500 # 批量导入大小
# MyBatis-Plus配置
mybatis-plus:
configuration:

View File

@ -52,7 +52,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.avgTempStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and t2.temperature > #{param.pastTimeAvgTempCount})
having avg(t2.temperature) > #{param.pastTimeAvgTempCount})
</if>
<if test="param.maxWindPastTime != null and param.maxWindPastTime != 0">
# 过去X分钟的平均风速超过Y m/s
@ -62,7 +62,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.maxWindStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and t2.extreme_wind_speed_hourly > #{param.pastTimeMaxWindCount})
having avg(t2.extreme_wind_speed_hourly) > #{param.pastTimeMaxWindCount})
</if>
<if test="param.powerOutageTimePastTime != null and param.powerOutageTimePastTime != 0">
# 过去X小时的停电时长超Y小时
@ -92,7 +92,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.powerOutageRatioStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and (t2.not_restored_user_count/(t2.restored_user_count + t2.not_restored_user_count)) > #{param.pastTimePowerOutageRatioCount})
having avg(t2.not_restored_user_count/(t2.restored_user_count + t2.not_restored_user_count)) > #{param.pastTimePowerOutageRatioCount})
</if>
</when>
<otherwise>
@ -128,7 +128,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.avgTempStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and t2.temperature > #{param.pastTimeAvgTempCount})
having avg(t2.temperature) > #{param.pastTimeAvgTempCount})
</if>
<if test="param.maxWindPastTime != null and param.maxWindPastTime != 0">
# 过去X天的平均风速超过Y m/s
@ -138,7 +138,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.maxWindStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and t2.extreme_wind_speed_hourly > #{param.pastTimeMaxWindCount})
having avg(t2.extreme_wind_speed_hourly) > #{param.pastTimeMaxWindCount})
</if>
<if test="param.powerOutageTimePastTime != null and param.powerOutageTimePastTime != 0">
# 过去X小时的停电时长超Y小时
@ -168,7 +168,7 @@
where t1.district_code = t2.org_code
and t2.data_time >= #{param.powerOutageRatioStartDateTime}
and t2.data_time <![CDATA[ < ]]> #{param.currentDateTime}
and (t2.not_restored_user_count/(t2.restored_user_count + t2.not_restored_user_count)) > #{param.pastTimePowerOutageRatioCount})
having avg(t2.not_restored_user_count/(t2.restored_user_count + t2.not_restored_user_count)) > #{param.pastTimePowerOutageRatioCount})
</if>
</when>
<otherwise>

View File

@ -2,6 +2,7 @@
CREATE TABLE import_task
(
id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '任务ID',
file_id BIGINT NOT NULL COMMENT '文件ID',
task_no VARCHAR(64) NOT NULL UNIQUE COMMENT '任务唯一编号',
total INT DEFAULT 0 COMMENT '总数据量',
success_count INT DEFAULT 0 COMMENT '成功数量',
@ -72,6 +73,7 @@ CREATE TABLE `weather_site_area_configuration`
CREATE TABLE `dner_daily_power_outage_event`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`event_id` BIGINT(20) NOT NULL COMMENT '关联事件ID',
`org_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地区编码',
`data_time` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资料日期',
`hourly_precipitation` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '0' COMMENT '小时降水量',
@ -105,6 +107,7 @@ CREATE TABLE `dner_daily_power_outage_event`
CREATE TABLE `dner_hourly_power_outage_event`
(
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`event_id` BIGINT(20) NOT NULL COMMENT '关联事件ID',
`org_code` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '地区编码',
`data_time` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '资料时次',
`hourly_precipitation` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '小时降水量',
@ -305,6 +308,7 @@ CREATE TABLE `dner_event_attachment`
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '附件ID',
`event_id` BIGINT NOT NULL COMMENT '关联事件ID',
`file_name` VARCHAR(255) NOT NULL COMMENT '文件名',
`stored_file_name` VARCHAR(255) NOT NULL COMMENT '存储在本地的唯一文件名',
`file_path` VARCHAR(512) NOT NULL COMMENT '文件存储路径',
`file_type` VARCHAR(32) NOT NULL COMMENT '文件类型',
`file_size` BIGINT NOT NULL COMMENT '文件大小(字节)',