diff --git a/src/main/java/com/southern/power/grid/controller/DnerController.java b/src/main/java/com/southern/power/grid/controller/DnerController.java index 9baf03c..768b9cf 100644 --- a/src/main/java/com/southern/power/grid/controller/DnerController.java +++ b/src/main/java/com/southern/power/grid/controller/DnerController.java @@ -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> queryAreaTree(@RequestBody @Valid AreaTreeReq req) { return Result.success(dnerSiteAreaConfigurationService.queryAreaTree(req)); @@ -82,9 +87,20 @@ public class DnerController { * @return 返回结果 */ @PostMapping("/excel/import") - public Result importExcel(@RequestParam("file") MultipartFile file) { - String taskNo = importTaskService.importExcel(file); - return Result.success(taskNo); + public Result 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 downloadExcel(@PathVariable Long attachmentId) { + return dnerEventAttachmentService.downloadExcel(attachmentId); } // 2. 查询导入进度 diff --git a/src/main/java/com/southern/power/grid/controller/DnerEventController.java b/src/main/java/com/southern/power/grid/controller/DnerEventController.java index 748d84c..e7919ca 100644 --- a/src/main/java/com/southern/power/grid/controller/DnerEventController.java +++ b/src/main/java/com/southern/power/grid/controller/DnerEventController.java @@ -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; /** diff --git a/src/main/java/com/southern/power/grid/entity/DnerEventAttachment.java b/src/main/java/com/southern/power/grid/entity/DnerEventAttachment.java index 57f352d..0bd237e 100644 --- a/src/main/java/com/southern/power/grid/entity/DnerEventAttachment.java +++ b/src/main/java/com/southern/power/grid/entity/DnerEventAttachment.java @@ -31,6 +31,11 @@ public class DnerEventAttachment { */ private String fileName; + /** + * 存储在本地的唯一文件名 + */ + private String storedFileName; + /** * 文件存储路径 */ diff --git a/src/main/java/com/southern/power/grid/entity/DnerHourlyPowerOutageEvent.java b/src/main/java/com/southern/power/grid/entity/DnerHourlyPowerOutageEvent.java index ef23c18..28ef46e 100644 --- a/src/main/java/com/southern/power/grid/entity/DnerHourlyPowerOutageEvent.java +++ b/src/main/java/com/southern/power/grid/entity/DnerHourlyPowerOutageEvent.java @@ -23,6 +23,11 @@ public class DnerHourlyPowerOutageEvent { @TableId(type = IdType.AUTO) private Long id; + /** + * 事件ID + */ + private Long eventId; + /** * 地区编码 */ diff --git a/src/main/java/com/southern/power/grid/entity/ImportTask.java b/src/main/java/com/southern/power/grid/entity/ImportTask.java index 389c691..d686645 100644 --- a/src/main/java/com/southern/power/grid/entity/ImportTask.java +++ b/src/main/java/com/southern/power/grid/entity/ImportTask.java @@ -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; } diff --git a/src/main/java/com/southern/power/grid/listener/DataExcelListener.java b/src/main/java/com/southern/power/grid/listener/DataExcelListener.java index 71c87bf..b318c27 100644 --- a/src/main/java/com/southern/power/grid/listener/DataExcelListener.java +++ b/src/main/java/com/southern/power/grid/listener/DataExcelListener.java @@ -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 { // 每批次大小 - private static final int BATCH_SIZE = 500; + @Value("${file.import.batch-size}") + private int BATCH_SIZE; // 缓存数据 private final List cacheList = new ArrayList<>(BATCH_SIZE); @@ -92,7 +94,7 @@ public class DataExcelListener extends AnalysisEventListener { private void batchInsert() { try { // 批量插入(500条) - hourlyOutageExcelProcessService.process(cacheList); + hourlyOutageExcelProcessService.process(cacheList, task.getEventId()); success += cacheList.size(); } catch (Exception e) { fail += cacheList.size(); diff --git a/src/main/java/com/southern/power/grid/service/DnerEventAttachmentService.java b/src/main/java/com/southern/power/grid/service/DnerEventAttachmentService.java index 020075d..adb75f4 100644 --- a/src/main/java/com/southern/power/grid/service/DnerEventAttachmentService.java +++ b/src/main/java/com/southern/power/grid/service/DnerEventAttachmentService.java @@ -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 { + /** + * 下载附件 + * + * @param attachmentId 附件ID + * @return 结果 + */ + ResponseEntity downloadExcel(Long attachmentId); } \ No newline at end of file diff --git a/src/main/java/com/southern/power/grid/service/IFileService.java b/src/main/java/com/southern/power/grid/service/IFileService.java new file mode 100644 index 0000000..a35ba49 --- /dev/null +++ b/src/main/java/com/southern/power/grid/service/IFileService.java @@ -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; +} diff --git a/src/main/java/com/southern/power/grid/service/IImportTaskService.java b/src/main/java/com/southern/power/grid/service/IImportTaskService.java index 15699d6..c30ac96 100644 --- a/src/main/java/com/southern/power/grid/service/IImportTaskService.java +++ b/src/main/java/com/southern/power/grid/service/IImportTaskService.java @@ -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); } diff --git a/src/main/java/com/southern/power/grid/service/impl/DnerEventAttachmentServiceImpl.java b/src/main/java/com/southern/power/grid/service/impl/DnerEventAttachmentServiceImpl.java index 4a92e7a..fb89ccc 100644 --- a/src/main/java/com/southern/power/grid/service/impl/DnerEventAttachmentServiceImpl.java +++ b/src/main/java/com/southern/power/grid/service/impl/DnerEventAttachmentServiceImpl.java @@ -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 implements DnerEventAttachmentService { + @Override + public ResponseEntity 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); + } } diff --git a/src/main/java/com/southern/power/grid/service/impl/FileServiceImpl.java b/src/main/java/com/southern/power/grid/service/impl/FileServiceImpl.java new file mode 100644 index 0000000..74782f9 --- /dev/null +++ b/src/main/java/com/southern/power/grid/service/impl/FileServiceImpl.java @@ -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().set("is_latest", 0) + .eq("event_id", eventId)); + // 插入记录 + dnerEventAttachmentMapper.insert(record); + // 删除旧的分时图和K线图数据 + dnerHourlyPowerOutageEventMapper.delete( + new QueryWrapper().eq("event_id", eventId)); + dnerDailyPowerOutageEventMapper.delete( + new QueryWrapper().eq("event_id", eventId)); + return record.getId(); + } +} diff --git a/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java b/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java index 1e6fb2a..42a98de 100644 --- a/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java +++ b/src/main/java/com/southern/power/grid/service/impl/HourlyOutageExcelProcessService.java @@ -57,7 +57,7 @@ public class HourlyOutageExcelProcessService { * 对外主入口:处理一批 Excel 解析后的数据,按逻辑补齐气象数据并批量入库 */ @Transactional - public void process(List excelRows) { + public void process(List 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 { // 已存在,执行累加逻辑 diff --git a/src/main/java/com/southern/power/grid/service/impl/ImportTaskServiceImpl.java b/src/main/java/com/southern/power/grid/service/impl/ImportTaskServiceImpl.java index 00308e3..240204d 100644 --- a/src/main/java/com/southern/power/grid/service/impl/ImportTaskServiceImpl.java +++ b/src/main/java/com/southern/power/grid/service/impl/ImportTaskServiceImpl.java @@ -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= #{param.avgTempStartDateTime} and t2.data_time #{param.currentDateTime} - and t2.temperature > #{param.pastTimeAvgTempCount}) + having avg(t2.temperature) > #{param.pastTimeAvgTempCount}) # 过去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 #{param.currentDateTime} - and t2.extreme_wind_speed_hourly > #{param.pastTimeMaxWindCount}) + having avg(t2.extreme_wind_speed_hourly) > #{param.pastTimeMaxWindCount}) # 过去X小时的停电时长超Y小时 @@ -92,7 +92,7 @@ where t1.district_code = t2.org_code and t2.data_time >= #{param.powerOutageRatioStartDateTime} and t2.data_time #{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}) @@ -128,7 +128,7 @@ where t1.district_code = t2.org_code and t2.data_time >= #{param.avgTempStartDateTime} and t2.data_time #{param.currentDateTime} - and t2.temperature > #{param.pastTimeAvgTempCount}) + having avg(t2.temperature) > #{param.pastTimeAvgTempCount}) # 过去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 #{param.currentDateTime} - and t2.extreme_wind_speed_hourly > #{param.pastTimeMaxWindCount}) + having avg(t2.extreme_wind_speed_hourly) > #{param.pastTimeMaxWindCount}) # 过去X小时的停电时长超Y小时 @@ -168,7 +168,7 @@ where t1.district_code = t2.org_code and t2.data_time >= #{param.powerOutageRatioStartDateTime} and t2.data_time #{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}) diff --git a/src/main/resources/sql/20260313-001.sql b/src/main/resources/sql/20260313-001.sql index 5ad6587..b0c1088 100644 --- a/src/main/resources/sql/20260313-001.sql +++ b/src/main/resources/sql/20260313-001.sql @@ -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 '小时降水量', @@ -302,15 +305,16 @@ CREATE TABLE `dner_event` -- 附件表 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 '文件名', - `file_path` VARCHAR(512) NOT NULL COMMENT '文件存储路径', - `file_type` VARCHAR(32) NOT NULL COMMENT '文件类型', - `file_size` BIGINT NOT NULL COMMENT '文件大小(字节)', - `is_latest` TINYINT NOT NULL DEFAULT '1' COMMENT '是否最新附件(1:是 0:否)', - `creator` VARCHAR(64) NOT NULL COMMENT '上传人', - `create_time` DATETIME default current_timestamp NOT NULL COMMENT '上传时间', + `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 '文件大小(字节)', + `is_latest` TINYINT NOT NULL DEFAULT '1' COMMENT '是否最新附件(1:是 0:否)', + `creator` VARCHAR(64) NOT NULL COMMENT '上传人', + `create_time` DATETIME default current_timestamp NOT NULL COMMENT '上传时间', PRIMARY KEY (`id`), KEY `idx_event_id` (`event_id`), CONSTRAINT `fk_attachment_event` FOREIGN KEY (`event_id`) REFERENCES `dner_event` (`id`)