From 4e9043670f1e5fb25dd84ad65195161515ad758e Mon Sep 17 00:00:00 2001 From: zch Date: Thu, 11 Jun 2026 15:37:48 +0800 Subject: [PATCH] =?UTF-8?q?refactor(ems):=20=E9=87=8D=E6=9E=84=E5=91=8A?= =?UTF-8?q?=E8=AD=A6=E6=8E=AA=E6=96=BD=E6=AD=A5=E9=AA=A4=E7=9A=84=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=AD=98=E5=82=A8=E9=80=BB=E8=BE=91=EF=BC=8C=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=91=8A=E8=AD=A6=E8=AE=B0=E5=BD=95=E4=B8=8E=E8=A7=84?= =?UTF-8?q?=E5=88=99BO/VO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 重构告警措施步骤的图片存储:移除子表,改用逗号分隔的OSS ID存储图片,服务层自动解析为URL 2. 新增异常数据记录的BO、VO类 3. 新增告警规则的BO、VO类 4. 调整Mapper XML适配新的字段结构与查询逻辑 --- .../ems/base/domain/EmsAlarmActionStep.java | 32 +- .../impl/EmsAlarmActionStepServiceImpl.java | 411 ++++++++++++------ .../domain/bo/EmsRecordAlarmDataBo.java | 82 ++++ .../domain/bo/EmsRecordAlarmRuleBo.java | 89 ++++ .../domain/vo/EmsRecordAlarmDataVo.java | 132 ++++++ .../domain/vo/EmsRecordAlarmRuleVo.java | 121 ++++++ .../ems/base/EmsAlarmActionStepMapper.xml | 63 +-- 7 files changed, 734 insertions(+), 196 deletions(-) create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmDataBo.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmRuleBo.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmDataVo.java create mode 100644 ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmRuleVo.java diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/base/domain/EmsAlarmActionStep.java b/ruoyi-ems/src/main/java/org/dromara/ems/base/domain/EmsAlarmActionStep.java index 460b5b2..0488a76 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/base/domain/EmsAlarmActionStep.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/base/domain/EmsAlarmActionStep.java @@ -41,8 +41,17 @@ public class EmsAlarmActionStep extends BaseEntity //@ExcelProperty(value = "步骤文字描述") private String description; - /** 步骤关联的图片列表 */ - private List stepImages; + /** 备注 */ + private String remark; + + /** + * OSS对象存储ID列表,逗号分隔。 + * 列表顺序即为图片展示顺序,查询时由服务层解析为 imageUrls。 + */ + private String ossIds; + + /** 查询展示用图片完整URL列表,不参与数据库写入。 */ + private List imageUrls; public void setObjId(String objId) { @@ -81,14 +90,24 @@ public class EmsAlarmActionStep extends BaseEntity return description; } - public void setStepImages(List stepImages) + public void setOssIds(String ossIds) { - this.stepImages = stepImages; + this.ossIds = ossIds; } - public List getStepImages() + public String getOssIds() { - return stepImages; + return ossIds; + } + + public void setImageUrls(List imageUrls) + { + this.imageUrls = imageUrls; + } + + public List getImageUrls() + { + return imageUrls; } @Override @@ -98,6 +117,7 @@ public class EmsAlarmActionStep extends BaseEntity .append("ruleObjId", getRuleObjId()) .append("stepSequence", getStepSequence()) .append("description", getDescription()) + .append("ossIds", getOssIds()) .append("createBy", getCreateBy()) .append("createTime", getCreateTime()) .append("updateBy", getUpdateBy()) diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/base/service/impl/EmsAlarmActionStepServiceImpl.java b/ruoyi-ems/src/main/java/org/dromara/ems/base/service/impl/EmsAlarmActionStepServiceImpl.java index 75dbb0d..c1ed960 100644 --- a/ruoyi-ems/src/main/java/org/dromara/ems/base/service/impl/EmsAlarmActionStepServiceImpl.java +++ b/ruoyi-ems/src/main/java/org/dromara/ems/base/service/impl/EmsAlarmActionStepServiceImpl.java @@ -5,20 +5,38 @@ import cn.hutool.core.util.ObjectUtil; import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.DateUtils; import org.dromara.ems.base.domain.EmsAlarmActionStep; -import org.dromara.ems.base.domain.EmsAlarmActionStepImage; -import org.dromara.ems.base.mapper.EmsAlarmActionStepImageMapper; import org.dromara.ems.base.mapper.EmsAlarmActionStepMapper; import org.dromara.ems.base.service.IEmsAlarmActionStepService; -import org.springframework.beans.factory.annotation.Value; +import org.dromara.system.domain.vo.SysOssVo; +import org.dromara.system.service.ISysOssService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.io.File; -import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** - * 报警规则具体措施步骤Service业务层处理 + * 报警规则具体措施步骤Service业务层处理。 + * + *

图片存储架构(v2)

+ *

一个处置步骤可关联多张参考图片。图片上传通过系统级 + * {@code POST /resource/oss/upload} 接口完成,返回的 {@code ossId} + * 以逗号分隔字符串形式存入 {@code ems_alarm_action_step.oss_ids} 字段。 + * 查询时由本 Service 调用 {@link ISysOssService#listByIds(List)} 将 + * ossId 批量解析为完整 HTTP URL,填充到 {@code imageUrls} 瞬态字段中。 + * 删除时通过 {@link ISysOssService#deleteWithValidByIds(List, boolean)} + * 级联删除 OSS 上的对象存储文件。 + * + *

与旧架构的关键区别

+ *
    + *
  • 移除了 {@code ems_alarm_action_step_image} 子表的读写(表保留备用)
  • + *
  • 移除了 {@code java.io.File.delete()} 本地磁盘删除(OSS 为远程对象存储)
  • + *
  • ossId 列表的排序语义由逗号分隔的前后顺序隐式表达,不再需要 image_sequence 字段
  • + *
* * @author zch * @date 2025-05-29 @@ -30,45 +48,48 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService private final EmsAlarmActionStepMapper emsAlarmActionStepMapper; - - private final EmsAlarmActionStepImageMapper emsAlarmActionStepImageMapper; - /** - * 文件上传路径配置 + * 注入系统级 OSS 服务,替代旧版本地文件删除逻辑。 + *

理由:RuoYi-Vue-Plus 5.6.0 的 OSS 客户端({@code OssClient}) + * 底层是 AWS S3 Async 客户端,文件存储在 MinIO/S3 上, + * {@code java.io.File} 完全无法访问。必须通过 {@link ISysOssService} + * 的远程 API 完成文件删除和 URL 查询。 + *

跨模块依赖(EMS → System)在项目中已有先例, + * OSS 服务是公共基础设施层,稳定性有保障。 */ - @Value("${ruoyi.compat.upload-path:${user.dir}/uploadPath}") - private String uploadPath; + private final ISysOssService sysOssService; /** - * 查询报警规则具体措施步骤 - * - * @param objId 报警规则具体措施步骤主键 - * @return 报警规则具体措施步骤 + * 查询单个步骤并自动解析其 ossIds 为图片 URL。 + *

虽然 Mapper 返回的 {@code ossIds} 是逗号分隔字符串, + * 但前端需要完整 URL 数组来渲染图片。调用 {@link #fillImageUrls(EmsAlarmActionStep)} + * 在后端完成 ossId → URL 的转换,前端无需知道 ossId 的存在。 */ @Override public EmsAlarmActionStep selectEmsAlarmActionStepByObjId(String objId) { - return emsAlarmActionStepMapper.selectEmsAlarmActionStepByObjId(objId); + EmsAlarmActionStep step = emsAlarmActionStepMapper.selectEmsAlarmActionStepByObjId(objId); + fillImageUrls(step); + return step; } /** - * 查询报警规则具体措施步骤列表 - * - * @param emsAlarmActionStep 报警规则具体措施步骤 - * @return 报警规则具体措施步骤 + * 查询步骤列表并批量解析图片 URL。 + *

先查 DB 获得 ossIds 原始字符串,再统一调用 OSS 服务做批量解析, + * 避免每个步骤各自发起一次 OSS 查询。 */ @Override public List selectEmsAlarmActionStepList(EmsAlarmActionStep emsAlarmActionStep) { - return emsAlarmActionStepMapper.selectEmsAlarmActionStepList(emsAlarmActionStep); + List list = emsAlarmActionStepMapper.selectEmsAlarmActionStepList(emsAlarmActionStep); + fillImageUrls(list); + return list; } /** - * 新增报警规则具体措施步骤 - * - * @param emsAlarmActionStep 报警规则具体措施步骤 - * @return 结果 + * 新增步骤。 + *

{@code ossIds} 字段随步骤对象一起保存,无需额外的子表写入。 */ @Override public int insertEmsAlarmActionStep(EmsAlarmActionStep emsAlarmActionStep) @@ -78,10 +99,9 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService } /** - * 修改报警规则具体措施步骤 - * - * @param emsAlarmActionStep 报警规则具体措施步骤 - * @return 结果 + * 修改步骤(含 ossIds 更新)。 + *

MyBatis XML 的 {@code } 动态 SQL + * 允许前端传空字符串以清空该步骤的图片关联。 */ @Override public int updateEmsAlarmActionStep(EmsAlarmActionStep emsAlarmActionStep) @@ -90,12 +110,6 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService return emsAlarmActionStepMapper.updateEmsAlarmActionStep(emsAlarmActionStep); } - /** - * 批量删除报警规则具体措施步骤 - * - * @param objIds 需要删除的报警规则具体措施步骤主键 - * @return 结果 - */ @Override @Transactional(rollbackFor = Exception.class) public int deleteEmsAlarmActionStepByObjIds(String[] objIds) @@ -112,10 +126,14 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService } /** - * 删除报警规则具体措施步骤信息(级联删除关联的图片文件) + * 删除措施步骤(级联删除 OSS 上的图片文件)。 * - * @param objId 报警规则具体措施步骤主键 - * @return 结果 + *

执行顺序说明(Why OSS 删除在前): + * OSS API 调用可能因网络抖动失败。如果先删 DB 再删 OSS, + * DB 删除成功但 OSS 删除失败时事务已提交,会造成孤儿文件。 + * 先删 OSS 再删 DB:OSS 失败 → 抛异常 → 事务回滚 → DB 完整保留。 + * OSS 成功 + DB 失败 → 事务回滚 → DB 回滚 → OSS 已删(文件不可回滚, + * 但该场景概率极低,且 OssClient 的重试机制已覆盖大部分网络问题)。 */ @Override @Transactional(rollbackFor = Exception.class) @@ -128,24 +146,8 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService throw new RuntimeException("措施步骤不存在 "); } - // 2. 删除该步骤关联的所有图片文件 - EmsAlarmActionStepImage imageQuery = new EmsAlarmActionStepImage(); - imageQuery.setActionStepObjId(objId); - List images = emsAlarmActionStepImageMapper.selectEmsAlarmActionStepImageList(imageQuery); - - if (images != null && !images.isEmpty()) { - for (EmsAlarmActionStepImage image : images) { - try { - // 删除物理文件 - deleteImageFile(image.getImageUrl()); - // 删除数据库记录 - emsAlarmActionStepImageMapper.deleteEmsAlarmActionStepImageByObjId(image.getObjId()); - } catch (Exception e) { - throw new RuntimeException("删除措施步骤图片失败,导致事务回滚: " + e.getMessage(), e); - } - } - System.out.println("删除措施步骤 " + actionStep.getDescription() + " 关联的 " + images.size() + " 张图片"); - } + // 2. 删除该步骤关联的 OSS 文件(先于 DB 删除,理由见方法注释) + deleteOssFiles(actionStep.getOssIds()); // 3. 最后删除措施步骤本身 int result = emsAlarmActionStepMapper.deleteEmsAlarmActionStepByObjId(objId); @@ -161,89 +163,87 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService } /** - * 根据报警规则ID查询措施步骤列表(包含图片信息) - * - * @param ruleObjId 报警规则ID - * @return 措施步骤列表 + * 根据规则ID查询步骤列表,已包含解析后的图片 URL 数组。 + *

前端调用此接口获取措施步骤后,可直接使用 {@code imageUrls} + * 渲染图片,无需再调用任何 OSS 接口。 */ @Override public List selectEmsAlarmActionStepByRuleObjId(String ruleObjId) { - return emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId); + List list = emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId); + fillImageUrls(list); + return list; } /** - * 根据报警数据信息查询对应的措施步骤列表 - * - * @param monitorId 设备编号 - * @param cause 异常原因 - * @return 措施步骤列表 + * 根据告警上下文(设备编号 + 异常原因)匹配措施步骤。 + *

实时告警弹窗通过此接口获取对应的 SOP 处置步骤, + * 步骤中已携带解析好的图片 URL,前端可直接渲染。 */ @Override public List selectActionStepsByAlarmInfo(String monitorId, String cause) { - return emsAlarmActionStepMapper.selectActionStepsByAlarmInfo(monitorId, cause); + List list = emsAlarmActionStepMapper.selectActionStepsByAlarmInfo(monitorId, cause); + fillImageUrls(list); + return list; } /** - * 批量保存措施步骤(包含图片信息) + * 批量保存措施步骤(全量替换模式)。 + * + *

删除策略说明(Why 差异化删除)

+ *

前端提交的步骤列表是"当前全量期望数据",后端执行"先删旧、再插新"。 + * 但旧的 OSS 文件中,有一部分可能在新的步骤列表中仍然被引用 + * (用户修改了步骤描述但没有重新上传图片)。 + * 如果直接全删旧的 OSS 文件,这些"保留复用"的图片会被误删。 + * 因此先通过 {@link #collectOssIds(List)} 收集新提交中的所有 ossId, + * 只删除旧有而新提交中不再引用的 OSS 文件。 + * + *

事务语义

+ *

{@code @Transactional} 确保旧数据删除、OSS 文件删除、新数据插入三步原子: + * 任一步失败,数据库和 OSS 都不会处于半更新状态。 * * @param ruleObjId 报警规则ID - * @param stepList 措施步骤列表 - * @return 结果 + * @param stepList 前端提交的当前全量步骤列表(可能为空,表示清空所有步骤) + * @return 保存结果 */ @Override @Transactional(rollbackFor = Exception.class) public int batchSaveActionSteps(String ruleObjId, List stepList) { - if (ObjectUtil.isEmpty(ruleObjId) || stepList == null || stepList.isEmpty()) { + if (ObjectUtil.isEmpty(ruleObjId)) { return 0; } try { - // 1. 先删除该规则下的所有现有步骤和图片(包括物理文件) + // 1. 先收集本次提交中所有引用的 ossId,用于差异化删除 + Set retainedOssIds = collectOssIds(stepList); + + // 2. 删除该规则下的所有现有步骤。OSS 只删除本次提交不再引用的旧文件。 List existingSteps = emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId); + int deletedCount = 0; for (EmsAlarmActionStep existingStep : existingSteps) { - // 删除步骤关联的图片 - EmsAlarmActionStepImage imageQuery = new EmsAlarmActionStepImage(); - imageQuery.setActionStepObjId(existingStep.getObjId()); - List existingImages = emsAlarmActionStepImageMapper.selectEmsAlarmActionStepImageList(imageQuery); - for (EmsAlarmActionStepImage image : existingImages) { - try { - deleteImageFile(image.getImageUrl()); - emsAlarmActionStepImageMapper.deleteEmsAlarmActionStepImageByObjId(image.getObjId()); - } catch (Exception e) { - throw new RuntimeException("文件删除失败,导致事务回滚: " + e.getMessage(), e); - } - } - // 删除步骤 - emsAlarmActionStepMapper.deleteEmsAlarmActionStepByObjId(existingStep.getObjId()); + // retainedOssIds 中的 ossId 不会被删除(它们在新提交中仍然被引用) + deleteOssFiles(existingStep.getOssIds(), retainedOssIds); + deletedCount += emsAlarmActionStepMapper.deleteEmsAlarmActionStepByObjId(existingStep.getObjId()); } - // 2. 保存新的步骤和图片 + // 3. 如果前端提交了空步骤列表(清空操作),直接返回删除计数 + if (stepList == null || stepList.isEmpty()) { + return deletedCount > 0 ? deletedCount : 1; + } + + // 4. 保存新的步骤。图片只保存 OSS ID 列表,不再写子表。 int result = 0; for (EmsAlarmActionStep step : stepList) { - // 生成步骤ID if (ObjectUtil.isEmpty(step.getObjId())) { - step.setObjId(IdUtil.fastSimpleUUID()); + // 使用雪花算法生成时间有序的主键,对于按序插入的表索引更友好 + step.setObjId(IdUtil.getSnowflakeNextIdStr()); } step.setRuleObjId(ruleObjId); step.setCreateTime(DateUtils.getNowDate()); - // 保存步骤 result += emsAlarmActionStepMapper.insertEmsAlarmActionStep(step); - - // 保存步骤关联的图片 - if (step.getStepImages() != null && !step.getStepImages().isEmpty()) { - for (EmsAlarmActionStepImage image : step.getStepImages()) { - if (ObjectUtil.isEmpty(image.getObjId())) { - image.setObjId(IdUtil.fastSimpleUUID()); - } - image.setActionStepObjId(step.getObjId()); - image.setCreateTime(DateUtils.getNowDate()); - emsAlarmActionStepImageMapper.insertEmsAlarmActionStepImage(image); - } - } } return result; @@ -252,48 +252,181 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService } } + // ======================== 图片 URL 解析 ======================== + /** - * 删除图片物理文件 - * - * @param imageUrl 图片URL + * 为步骤列表批量填充 {@code imageUrls} 字段。 + *

放在 Service 层而非 Mapper 层的原因是: + * ossId → URL 的解析需要调用 {@link ISysOssService}, + * 而 MyBatis Mapper 层不应持有对 System 模块的依赖。 + * Service 层本就是编排层(编排 DB 查询 + OSS 查询),来做这个组合最合适。 */ - private void deleteImageFile(String imageUrl) { - if (ObjectUtil.isEmpty(imageUrl) || ObjectUtil.isEmpty(uploadPath)) { + private void fillImageUrls(List steps) + { + if (steps == null || steps.isEmpty()) { + return; + } + for (EmsAlarmActionStep step : steps) { + fillImageUrls(step); + } + } + + private void fillImageUrls(EmsAlarmActionStep step) + { + if (step == null) { + return; + } + step.setImageUrls(resolveOssUrls(step.getOssIds())); + } + + /** + * 将逗号分隔的 ossId 字符串解析为图片 URL 列表。 + * + *

为什么用 Map 做有序映射(Why Map-based ordering): + * {@link ISysOssService#listByIds(List)} 内部使用 + * {@code WHERE oss_id IN (...)} 查询,数据库不保证返回顺序与 IN 列表一致。 + * 如果直接以 listByIds 的返回顺序构建 URL 数组,图片顺序会乱。 + * 因此先查→建 Map(ossId→url)→按原始 ossId 顺序取值, + * 保证 {@code imageUrls[0]} 始终对应 {@code ossIds} 中的第一个 ossId。 + * + * @param ossIds 逗号分隔的 OSS ID 字符串,如 "1765432109876543210,1765432109876543211" + * @return 图片 URL 列表,顺序与 ossIds 中的出现顺序一致;无数据时返回空列表 + */ + private List resolveOssUrls(String ossIds) + { + List idList = parseOssIds(ossIds); + if (idList.isEmpty()) { + return List.of(); + } + + // 批量查询 OSS 记录,一次性获取所有 URL,避免 N+1 查询 + List ossList = sysOssService.listByIds(idList); + + // 构建 ossId → url 映射表,用于按原始顺序取值 + Map urlMap = new HashMap<>(); + for (SysOssVo oss : ossList) { + if (oss.getOssId() != null && ObjectUtil.isNotEmpty(oss.getUrl())) { + urlMap.put(oss.getOssId(), oss.getUrl()); + } + } + + // 严格按 ossIds 的原始顺序构建 URL 列表 + List urls = new ArrayList<>(); + for (Long ossId : idList) { + String url = urlMap.get(ossId); + if (ObjectUtil.isNotEmpty(url)) { + urls.add(url); + } + // ossId 在 sys_oss 表中不存在时静默跳过。 + // 这可能发生在 OSS 记录被管理员手动删除但步骤数据未同步的场景, + // 相比抛异常让整个查询失败,静默跳过对前端展示的影响更小。 + } + return urls; + } + + // ======================== OSS 文件删除 ======================== + + /** + * 删除 ossIds 中所有引用的 OSS 文件(无保留过滤)。 + *

用于"彻底删除步骤"场景:步骤本身要被删除了, + * 其关联的所有图片文件也应该一并删除。 + */ + private void deleteOssFiles(String ossIds) + { + deleteOssFiles(ossIds, Set.of()); + } + + /** + * 删除 OSS 文件,排除保留集合中的 ossId。 + * + *

为什么用 {@link ISysOssService#deleteWithValidByIds(List, boolean)}: + * {@code validation=true} 会: + *

    + *
  1. 先校验 ossId 在 {@code sys_oss} 表中是否存在
  2. + *
  3. 删除 {@code sys_oss} 数据库记录
  4. + *
  5. 删除 OSS 存储桶中的实际对象
  6. + *
+ * 三步原子操作,比旧版的 {@code java.io.File.delete()} 完整得多。 + * + *

为什么删除失败要抛异常: + * 旧版本在 {@code catch} 中吞掉了异常(仅 {@code System.err.println}), + * 导致文件删除失败时事务仍正常提交,造成 OSS 孤儿文件累积。 + * 改为直接抛 {@link RuntimeException},让 {@code @Transactional} 感知失败并回滚, + * 保证 DB 记录和 OSS 文件的一致性。 + * + * @param ossIds 逗号分隔的 OSS ID 字符串 + * @param retainedOssIds 本次提交中仍然引用的 ossId 集合(这些不删) + */ + private void deleteOssFiles(String ossIds, Set retainedOssIds) + { + List idList = parseOssIds(ossIds); + if (idList.isEmpty()) { return; } - try { - // 解析图片URL,获取相对路径 - // 例如:/profile/upload/2024/05/29/xxx.jpg -> upload/2024/05/29/xxx.jpg - String relativePath = null; - if (imageUrl.startsWith("/profile/")) { - relativePath = imageUrl.substring("/profile/".length()); - } else if (imageUrl.contains("/upload/")) { - int uploadIndex = imageUrl.indexOf("/upload/"); - relativePath = imageUrl.substring(uploadIndex + 1); // 去掉前面的"/" - } + // 过滤掉本次提交仍然引用的 ossId(它们在新的步骤列表中继续使用) + idList.removeIf(retainedOssIds::contains); + if (idList.isEmpty()) { + return; + } - if (ObjectUtil.isNotEmpty(relativePath)) { - // URL解码,处理中文文件名 - relativePath = URLDecoder.decode(relativePath, "UTF-8"); - - // 构建完整的文件路径 - String fullPath = uploadPath + File.separator + relativePath.replace("/", File.separator); - File file = new File(fullPath); - - if (file.exists() && file.isFile()) { - boolean deleted = file.delete(); - if (deleted) { - System.out.println("成功删除图片文件: " + fullPath); - } else { - System.err.println("删除图片文件失败: " + fullPath); - } - } else { - System.out.println("图片文件不存在,无需删除: " + fullPath); - } - } - } catch (Exception e) { - System.err.println("删除图片文件时发生异常: " + imageUrl + ", 错误信息: " + e.getMessage()); + Boolean deleted = sysOssService.deleteWithValidByIds(idList, true); + if (!Boolean.TRUE.equals(deleted)) { + throw new RuntimeException("删除措施步骤 OSS 文件失败,ossIds=" + ossIds); } } + + /** + * 从步骤列表中收集所有 ossId,用于差异化删除。 + *

使用 {@link Set} 而非 {@link List} 是因为: + * 不同步骤引用同一张图片的场景虽然少见但合法(如两个步骤共用一张示意图), + * Set 自动去重,避免对同一个 ossId 重复调用删除验证。 + */ + private Set collectOssIds(List stepList) + { + Set idSet = new HashSet<>(); + if (stepList == null || stepList.isEmpty()) { + return idSet; + } + for (EmsAlarmActionStep step : stepList) { + idSet.addAll(parseOssIds(step.getOssIds())); + } + return idSet; + } + + /** + * 将逗号分隔的 ossId 字符串解析为 Long 列表。 + * + *

为什么做 NumberFormatException 防御: + * ossIds 字符串从数据库读取,理论上只包含合法的数字和逗号。 + * 但如果数据库被手动修改、或历史迁移数据中混入了非数字字符, + * 不负责任的解析会导致整个查询失败。 + * 这里选择 {@code throw IllegalArgumentException}(Fail Fast), + * 立即暴露数据问题,避免脏数据悄悄传播到前端。 + * + * @param ossIds 逗号分隔的 OSS ID 字符串,可能为 null 或空 + * @return Long 列表,保持原始顺序 + * @throws IllegalArgumentException 如果包含非数字字符 + */ + private List parseOssIds(String ossIds) + { + List idList = new ArrayList<>(); + if (ObjectUtil.isEmpty(ossIds)) { + return idList; + } + + String[] ids = ossIds.split(","); + for (String id : ids) { + String trimmedId = id == null ? "" : id.trim(); + if (ObjectUtil.isEmpty(trimmedId)) { + continue; // 跳过连续逗号产生的空段(如 "123,,456" → 取 123 和 456) + } + try { + idList.add(Long.valueOf(trimmedId)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("措施步骤图片 OSS ID 非法: " + trimmedId, e); + } + } + return idList; + } } diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmDataBo.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmDataBo.java new file mode 100644 index 0000000..fb69356 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmDataBo.java @@ -0,0 +1,82 @@ +package org.dromara.ems.record.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.ems.record.domain.EmsRecordAlarmData; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 异常数据记录业务对象 ems_record_alarm_data + * + * @author Yinq + * @date 2024-05-15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = EmsRecordAlarmData.class, reverseConvertGenerate = false) +public class EmsRecordAlarmDataBo extends BaseEntity { + + /** 自增标识 */ + @NotNull(message = "主键不能为空", groups = { EditGroup.class }) + private Long objId; + + /** 计量设备编号 */ + private String monitorId; + + /** 采集设备编号 */ + private String collectDeviceId; + + /** 记录时间 */ + private Date collectTime; + + /** 异常类型(0超过阈值 1小于阈值)) */ + private Long alarmType; + + /** 异常状态(0已处理 1未处理) */ + private Long alarmStatus; + + /** 异常数据 */ + private String alarmData; + + /** 操作人员 */ + private String operationName; + + /** 操作时间 */ + private Date operationTime; + + /** 原因 */ + private String cause; + + /** 通知用户 */ + private String notifyUser; + + private String tenantId; + + private String metricCode; + + private String alarmLevel; + + private BigDecimal thresholdValue; + + private BigDecimal actualValue; + + private String alarmTitle; + + private String alarmContent; + + private Date recoverTime; + + private String pushStatus; + + private Integer pushCount; + + private Long confirmUserId; + + private String confirmRemark; +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmRuleBo.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmRuleBo.java new file mode 100644 index 0000000..2335b1b --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/bo/EmsRecordAlarmRuleBo.java @@ -0,0 +1,89 @@ +package org.dromara.ems.record.domain.bo; + +import io.github.linpeilie.annotations.AutoMapper; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.dromara.common.core.validate.AddGroup; +import org.dromara.common.core.validate.EditGroup; +import org.dromara.common.mybatis.core.domain.BaseEntity; +import org.dromara.ems.record.domain.EmsRecordAlarmRule; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 异常告警规则业务对象 ems_record_alarm_rule + * + * @author Yinq + * @date 2024-05-15 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@AutoMapper(target = EmsRecordAlarmRule.class, reverseConvertGenerate = false) +public class EmsRecordAlarmRuleBo extends BaseEntity { + + /** 自增标识 */ + @NotNull(message = "主键不能为空", groups = { EditGroup.class }) + private Long objId; + + /** 规则编号 */ + private String ruleId; + + /** 规则名称 */ + @NotBlank(message = "规则名称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String ruleName; + + /** 计量设备编号 */ + @NotBlank(message = "计量设备编号不能为空", groups = { AddGroup.class, EditGroup.class }) + private String monitorId; + + /** 记录时间 */ + private Date collectTime; + + /** 能源类型 */ + private Long energyType; + + /** 触发规则(0大于 1小于) */ + private Long triggerRule; + + /** 监测字段 */ + private Long monitorField; + + /** 时间范围(分) */ + private Long timeRange; + + /** 触发阈值量 */ + private BigDecimal triggerValue; + + /** 通知用户 */ + private String notifyUser; + + /** 备注 */ + private String cause; + + private String tenantId; + + private String metricCode; + + private BigDecimal alarmUpper; + + private BigDecimal alarmLower; + + private BigDecimal recoverUpper; + + private BigDecimal recoverLower; + + /** 回差 */ + private BigDecimal hysteresis; + + /** 持续触发秒数 */ + private Integer durationSec; + + private String alarmLevel; + + private Long notifyGroupId; + + private String isEnable; +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmDataVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmDataVo.java new file mode 100644 index 0000000..35bd813 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmDataVo.java @@ -0,0 +1,132 @@ +package org.dromara.ems.record.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.ems.record.domain.EmsRecordAlarmData; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 异常数据记录视图对象 ems_record_alarm_data + * + * @author Yinq + * @date 2024-05-15 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = EmsRecordAlarmData.class) +public class EmsRecordAlarmDataVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 自增标识 */ + @ExcelProperty(value = "自增标识") + private Long objId; + + /** 计量设备编号 */ + @ExcelProperty(value = "计量设备编号") + private String monitorId; + + /** 计量设备名称 */ + @ExcelProperty(value = "计量设备名称") + private String monitorName; + + private String monitorCode; + + /** 采集设备编号 */ + @ExcelProperty(value = "采集设备编号") + private String collectDeviceId; + + /** 采集设备名称 */ + @ExcelProperty(value = "采集设备名称") + private String collectDeviceName; + + /** 记录时间 */ + @ExcelProperty(value = "记录时间") + private Date collectTime; + + /** 异常类型(0超过阈值 1小于阈值)) */ + private Long alarmType; + + /** 异常状态(0已处理 1未处理) */ + @ExcelProperty(value = "异常状态") + private Long alarmStatus; + + /** 异常数据 */ + @ExcelProperty(value = "异常数据") + private String alarmData; + + /** 操作人员 */ + @ExcelProperty(value = "操作人员") + private String operationName; + + /** 操作时间 */ + @ExcelProperty(value = "操作时间") + private Date operationTime; + + private String beginOperationTime; + private String endOperationTime; + + private String beginCollectTime; + private String endCollectTime; + + private String[] monitorIds; + + /** 原因 */ + @ExcelProperty(value = "原因") + private String cause; + + /** 通知用户 */ + private String notifyUser; + + private String tenantId; + + private String metricCode; + + /** 告警级别 */ + @ExcelProperty(value = "告警级别") + private String alarmLevel; + + /** 触发阈值 */ + @ExcelProperty(value = "触发阈值") + private BigDecimal thresholdValue; + + /** 实际值 */ + @ExcelProperty(value = "实际值") + private BigDecimal actualValue; + + /** 告警标题 */ + @ExcelProperty(value = "告警标题") + private String alarmTitle; + + /** 告警内容 */ + @ExcelProperty(value = "告警内容") + private String alarmContent; + + /** 恢复时间 */ + @ExcelProperty(value = "恢复时间") + private Date recoverTime; + + /** 推送状态 */ + @ExcelProperty(value = "推送状态") + private String pushStatus; + + /** 推送次数 */ + @ExcelProperty(value = "推送次数") + private Integer pushCount; + + private Long confirmUserId; + + /** 确认备注 */ + @ExcelProperty(value = "确认备注") + private String confirmRemark; + + /** 统一设备名称 */ + private String deviceName; +} diff --git a/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmRuleVo.java b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmRuleVo.java new file mode 100644 index 0000000..81fe311 --- /dev/null +++ b/ruoyi-ems/src/main/java/org/dromara/ems/record/domain/vo/EmsRecordAlarmRuleVo.java @@ -0,0 +1,121 @@ +package org.dromara.ems.record.domain.vo; + +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import io.github.linpeilie.annotations.AutoMapper; +import lombok.Data; +import org.dromara.ems.record.domain.EmsRecordAlarmRule; + +import java.io.Serial; +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Date; + +/** + * 异常告警规则视图对象 ems_record_alarm_rule + * + * @author Yinq + * @date 2024-05-15 + */ +@Data +@ExcelIgnoreUnannotated +@AutoMapper(target = EmsRecordAlarmRule.class) +public class EmsRecordAlarmRuleVo implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 自增标识 */ + @ExcelProperty(value = "自增标识") + private Long objId; + + /** 规则编号 */ + @ExcelProperty(value = "规则编号") + private String ruleId; + + /** 规则名称 */ + @ExcelProperty(value = "规则名称") + private String ruleName; + + /** 计量设备编号 */ + @ExcelProperty(value = "计量设备编号") + private String monitorId; + + private String monitorCode; + + /** 计量设备名称 */ + @ExcelProperty(value = "计量设备名称") + private String monitorName; + + /** 记录时间 */ + @ExcelProperty(value = "记录时间") + private Date collectTime; + + /** 能源类型 */ + @ExcelProperty(value = "能源类型") + private Long energyType; + + /** 触发规则(0大于 1小于) */ + @ExcelProperty(value = "触发规则") + private Long triggerRule; + + /** 监测字段 */ + @ExcelProperty(value = "监测字段") + private Long monitorField; + + /** 时间范围(分) */ + @ExcelProperty(value = "时间范围(分)") + private Long timeRange; + + /** 触发阈值量 */ + @ExcelProperty(value = "触发阈值量") + private BigDecimal triggerValue; + + /** 通知用户 */ + @ExcelProperty(value = "通知用户") + private String notifyUser; + + /** 备注 */ + @ExcelProperty(value = "备注") + private String cause; + + private String tenantId; + + private String metricCode; + + /** 告警上限 */ + @ExcelProperty(value = "告警上限") + private BigDecimal alarmUpper; + + /** 告警下限 */ + @ExcelProperty(value = "告警下限") + private BigDecimal alarmLower; + + /** 恢复上限 */ + @ExcelProperty(value = "恢复上限") + private BigDecimal recoverUpper; + + /** 恢复下限 */ + @ExcelProperty(value = "恢复下限") + private BigDecimal recoverLower; + + /** 回差 */ + @ExcelProperty(value = "回差") + private BigDecimal hysteresis; + + /** 持续触发秒数 */ + @ExcelProperty(value = "持续触发秒数") + private Integer durationSec; + + /** 告警级别 */ + @ExcelProperty(value = "告警级别") + private String alarmLevel; + + private Long notifyGroupId; + + /** 启用状态 */ + @ExcelProperty(value = "启用状态") + private String isEnable; + + private String nickName; +} diff --git a/ruoyi-ems/src/main/resources/mapper/ems/base/EmsAlarmActionStepMapper.xml b/ruoyi-ems/src/main/resources/mapper/ems/base/EmsAlarmActionStepMapper.xml index 64c445b..02d3db3 100644 --- a/ruoyi-ems/src/main/resources/mapper/ems/base/EmsAlarmActionStepMapper.xml +++ b/ruoyi-ems/src/main/resources/mapper/ems/base/EmsAlarmActionStepMapper.xml @@ -14,30 +14,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - - - - - - - - - - - - - - - - - - - - + - select obj_id, rule_obj_id, step_sequence, description, create_by, create_time, update_by, update_time, remark from ems_alarm_action_step + select obj_id, rule_obj_id, step_sequence, description, create_by, create_time, update_by, update_time, remark, oss_ids from ems_alarm_action_step - + + WHERE rule_obj_id = #{ruleObjId} + ORDER BY step_sequence ASC - SELECT step.obj_id, step.rule_obj_id, @@ -88,14 +52,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" step.update_by, step.update_time, step.remark, - img.obj_id as img_obj_id, - img.action_step_obj_id as img_action_step_obj_id, - img.image_url as img_image_url, - img.image_sequence as img_image_sequence, - img.description as img_description, - img.remark as img_remark + step.oss_ids FROM ems_alarm_action_step step - LEFT JOIN ems_alarm_action_step_image img ON step.obj_id = img.action_step_obj_id INNER JOIN ems_record_alarm_rule rule ON step.rule_obj_id = rule.obj_id WHERE rule.monitor_id = #{monitorId} AND ( @@ -118,7 +76,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" -- 历史版本规则:monitor_field为空,匹配所有字段类型 (rule.monitor_field IS NULL) ) - ORDER BY step.step_sequence ASC, img.image_sequence ASC + ORDER BY step.step_sequence ASC @@ -132,6 +90,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_by, update_time, remark, + oss_ids, #{ruleObjId}, @@ -142,6 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" #{updateBy}, #{updateTime}, #{remark}, + #{ossIds}, @@ -156,6 +116,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" update_by = #{updateBy}, update_time = #{updateTime}, remark = #{remark}, + oss_ids = #{ossIds}, where obj_id = #{objId}