|
|
|
|
@ -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业务层处理。
|
|
|
|
|
*
|
|
|
|
|
* <h3>图片存储架构(v2)</h3>
|
|
|
|
|
* <p>一个处置步骤可关联多张参考图片。图片上传通过系统级
|
|
|
|
|
* {@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 上的对象存储文件。
|
|
|
|
|
*
|
|
|
|
|
* <h4>与旧架构的关键区别</h4>
|
|
|
|
|
* <ul>
|
|
|
|
|
* <li>移除了 {@code ems_alarm_action_step_image} 子表的读写(表保留备用)</li>
|
|
|
|
|
* <li>移除了 {@code java.io.File.delete()} 本地磁盘删除(OSS 为远程对象存储)</li>
|
|
|
|
|
* <li>ossId 列表的排序语义由逗号分隔的前后顺序隐式表达,不再需要 image_sequence 字段</li>
|
|
|
|
|
* </ul>
|
|
|
|
|
*
|
|
|
|
|
* @author zch
|
|
|
|
|
* @date 2025-05-29
|
|
|
|
|
@ -30,45 +48,48 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService
|
|
|
|
|
|
|
|
|
|
private final EmsAlarmActionStepMapper emsAlarmActionStepMapper;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private final EmsAlarmActionStepImageMapper emsAlarmActionStepImageMapper;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 文件上传路径配置
|
|
|
|
|
* 注入系统级 OSS 服务,替代旧版本地文件删除逻辑。
|
|
|
|
|
* <p>理由:RuoYi-Vue-Plus 5.6.0 的 OSS 客户端({@code OssClient})
|
|
|
|
|
* 底层是 AWS S3 Async 客户端,文件存储在 MinIO/S3 上,
|
|
|
|
|
* {@code java.io.File} 完全无法访问。必须通过 {@link ISysOssService}
|
|
|
|
|
* 的远程 API 完成文件删除和 URL 查询。
|
|
|
|
|
* <p>跨模块依赖(EMS → System)在项目中已有先例,
|
|
|
|
|
* OSS 服务是公共基础设施层,稳定性有保障。
|
|
|
|
|
*/
|
|
|
|
|
@Value("${ruoyi.compat.upload-path:${user.dir}/uploadPath}")
|
|
|
|
|
private String uploadPath;
|
|
|
|
|
private final ISysOssService sysOssService;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 查询报警规则具体措施步骤
|
|
|
|
|
*
|
|
|
|
|
* @param objId 报警规则具体措施步骤主键
|
|
|
|
|
* @return 报警规则具体措施步骤
|
|
|
|
|
* 查询单个步骤并自动解析其 ossIds 为图片 URL。
|
|
|
|
|
* <p>虽然 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。
|
|
|
|
|
* <p>先查 DB 获得 ossIds 原始字符串,再统一调用 OSS 服务做批量解析,
|
|
|
|
|
* 避免每个步骤各自发起一次 OSS 查询。
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<EmsAlarmActionStep> selectEmsAlarmActionStepList(EmsAlarmActionStep emsAlarmActionStep)
|
|
|
|
|
{
|
|
|
|
|
return emsAlarmActionStepMapper.selectEmsAlarmActionStepList(emsAlarmActionStep);
|
|
|
|
|
List<EmsAlarmActionStep> list = emsAlarmActionStepMapper.selectEmsAlarmActionStepList(emsAlarmActionStep);
|
|
|
|
|
fillImageUrls(list);
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 新增报警规则具体措施步骤
|
|
|
|
|
*
|
|
|
|
|
* @param emsAlarmActionStep 报警规则具体措施步骤
|
|
|
|
|
* @return 结果
|
|
|
|
|
* 新增步骤。
|
|
|
|
|
* <p>{@code ossIds} 字段随步骤对象一起保存,无需额外的子表写入。
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public int insertEmsAlarmActionStep(EmsAlarmActionStep emsAlarmActionStep)
|
|
|
|
|
@ -78,10 +99,9 @@ public class EmsAlarmActionStepServiceImpl implements IEmsAlarmActionStepService
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 修改报警规则具体措施步骤
|
|
|
|
|
*
|
|
|
|
|
* @param emsAlarmActionStep 报警规则具体措施步骤
|
|
|
|
|
* @return 结果
|
|
|
|
|
* 修改步骤(含 ossIds 更新)。
|
|
|
|
|
* <p>MyBatis XML 的 {@code <if test="ossIds != null">} 动态 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 结果
|
|
|
|
|
* <p><b>执行顺序说明(Why OSS 删除在前)</b>:
|
|
|
|
|
* 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<EmsAlarmActionStepImage> 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 数组。
|
|
|
|
|
* <p>前端调用此接口获取措施步骤后,可直接使用 {@code imageUrls}
|
|
|
|
|
* 渲染图片,无需再调用任何 OSS 接口。
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<EmsAlarmActionStep> selectEmsAlarmActionStepByRuleObjId(String ruleObjId)
|
|
|
|
|
{
|
|
|
|
|
return emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId);
|
|
|
|
|
List<EmsAlarmActionStep> list = emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId);
|
|
|
|
|
fillImageUrls(list);
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 根据报警数据信息查询对应的措施步骤列表
|
|
|
|
|
*
|
|
|
|
|
* @param monitorId 设备编号
|
|
|
|
|
* @param cause 异常原因
|
|
|
|
|
* @return 措施步骤列表
|
|
|
|
|
* 根据告警上下文(设备编号 + 异常原因)匹配措施步骤。
|
|
|
|
|
* <p>实时告警弹窗通过此接口获取对应的 SOP 处置步骤,
|
|
|
|
|
* 步骤中已携带解析好的图片 URL,前端可直接渲染。
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
public List<EmsAlarmActionStep> selectActionStepsByAlarmInfo(String monitorId, String cause)
|
|
|
|
|
{
|
|
|
|
|
return emsAlarmActionStepMapper.selectActionStepsByAlarmInfo(monitorId, cause);
|
|
|
|
|
List<EmsAlarmActionStep> list = emsAlarmActionStepMapper.selectActionStepsByAlarmInfo(monitorId, cause);
|
|
|
|
|
fillImageUrls(list);
|
|
|
|
|
return list;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 批量保存措施步骤(包含图片信息)
|
|
|
|
|
* 批量保存措施步骤(全量替换模式)。
|
|
|
|
|
*
|
|
|
|
|
* <h4>删除策略说明(Why 差异化删除)</h4>
|
|
|
|
|
* <p>前端提交的步骤列表是"当前全量期望数据",后端执行"先删旧、再插新"。
|
|
|
|
|
* 但旧的 OSS 文件中,有一部分可能在新的步骤列表中仍然被引用
|
|
|
|
|
* (用户修改了步骤描述但没有重新上传图片)。
|
|
|
|
|
* 如果直接全删旧的 OSS 文件,这些"保留复用"的图片会被误删。
|
|
|
|
|
* 因此先通过 {@link #collectOssIds(List)} 收集新提交中的所有 ossId,
|
|
|
|
|
* 只删除<em>旧有而新提交中不再引用</em>的 OSS 文件。
|
|
|
|
|
*
|
|
|
|
|
* <h4>事务语义</h4>
|
|
|
|
|
* <p>{@code @Transactional} 确保旧数据删除、OSS 文件删除、新数据插入三步原子:
|
|
|
|
|
* 任一步失败,数据库和 OSS 都不会处于半更新状态。
|
|
|
|
|
*
|
|
|
|
|
* @param ruleObjId 报警规则ID
|
|
|
|
|
* @param stepList 措施步骤列表
|
|
|
|
|
* @return 结果
|
|
|
|
|
* @param stepList 前端提交的当前全量步骤列表(可能为空,表示清空所有步骤)
|
|
|
|
|
* @return 保存结果
|
|
|
|
|
*/
|
|
|
|
|
@Override
|
|
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
|
|
public int batchSaveActionSteps(String ruleObjId, List<EmsAlarmActionStep> stepList)
|
|
|
|
|
{
|
|
|
|
|
if (ObjectUtil.isEmpty(ruleObjId) || stepList == null || stepList.isEmpty()) {
|
|
|
|
|
if (ObjectUtil.isEmpty(ruleObjId)) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// 1. 先删除该规则下的所有现有步骤和图片(包括物理文件)
|
|
|
|
|
// 1. 先收集本次提交中所有引用的 ossId,用于差异化删除
|
|
|
|
|
Set<Long> retainedOssIds = collectOssIds(stepList);
|
|
|
|
|
|
|
|
|
|
// 2. 删除该规则下的所有现有步骤。OSS 只删除本次提交不再引用的旧文件。
|
|
|
|
|
List<EmsAlarmActionStep> existingSteps = emsAlarmActionStepMapper.selectEmsAlarmActionStepByRuleObjId(ruleObjId);
|
|
|
|
|
int deletedCount = 0;
|
|
|
|
|
for (EmsAlarmActionStep existingStep : existingSteps) {
|
|
|
|
|
// 删除步骤关联的图片
|
|
|
|
|
EmsAlarmActionStepImage imageQuery = new EmsAlarmActionStepImage();
|
|
|
|
|
imageQuery.setActionStepObjId(existingStep.getObjId());
|
|
|
|
|
List<EmsAlarmActionStepImage> 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} 字段。
|
|
|
|
|
* <p>放在 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<EmsAlarmActionStep> 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 列表。
|
|
|
|
|
*
|
|
|
|
|
* <p><b>为什么用 Map 做有序映射(Why Map-based ordering)</b>:
|
|
|
|
|
* {@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<String> resolveOssUrls(String ossIds)
|
|
|
|
|
{
|
|
|
|
|
List<Long> idList = parseOssIds(ossIds);
|
|
|
|
|
if (idList.isEmpty()) {
|
|
|
|
|
return List.of();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量查询 OSS 记录,一次性获取所有 URL,避免 N+1 查询
|
|
|
|
|
List<SysOssVo> ossList = sysOssService.listByIds(idList);
|
|
|
|
|
|
|
|
|
|
// 构建 ossId → url 映射表,用于按原始顺序取值
|
|
|
|
|
Map<Long, String> urlMap = new HashMap<>();
|
|
|
|
|
for (SysOssVo oss : ossList) {
|
|
|
|
|
if (oss.getOssId() != null && ObjectUtil.isNotEmpty(oss.getUrl())) {
|
|
|
|
|
urlMap.put(oss.getOssId(), oss.getUrl());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 严格按 ossIds 的原始顺序构建 URL 列表
|
|
|
|
|
List<String> 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 文件(无保留过滤)。
|
|
|
|
|
* <p>用于"彻底删除步骤"场景:步骤本身要被删除了,
|
|
|
|
|
* 其关联的所有图片文件也应该一并删除。
|
|
|
|
|
*/
|
|
|
|
|
private void deleteOssFiles(String ossIds)
|
|
|
|
|
{
|
|
|
|
|
deleteOssFiles(ossIds, Set.of());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除 OSS 文件,排除保留集合中的 ossId。
|
|
|
|
|
*
|
|
|
|
|
* <p><b>为什么用 {@link ISysOssService#deleteWithValidByIds(List, boolean)}</b>:
|
|
|
|
|
* {@code validation=true} 会:
|
|
|
|
|
* <ol>
|
|
|
|
|
* <li>先校验 ossId 在 {@code sys_oss} 表中是否存在</li>
|
|
|
|
|
* <li>删除 {@code sys_oss} 数据库记录</li>
|
|
|
|
|
* <li>删除 OSS 存储桶中的实际对象</li>
|
|
|
|
|
* </ol>
|
|
|
|
|
* 三步原子操作,比旧版的 {@code java.io.File.delete()} 完整得多。
|
|
|
|
|
*
|
|
|
|
|
* <p><b>为什么删除失败要抛异常</b>:
|
|
|
|
|
* 旧版本在 {@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<Long> retainedOssIds)
|
|
|
|
|
{
|
|
|
|
|
List<Long> 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,用于差异化删除。
|
|
|
|
|
* <p>使用 {@link Set} 而非 {@link List} 是因为:
|
|
|
|
|
* 不同步骤引用同一张图片的场景虽然少见但合法(如两个步骤共用一张示意图),
|
|
|
|
|
* Set 自动去重,避免对同一个 ossId 重复调用删除验证。
|
|
|
|
|
*/
|
|
|
|
|
private Set<Long> collectOssIds(List<EmsAlarmActionStep> stepList)
|
|
|
|
|
{
|
|
|
|
|
Set<Long> idSet = new HashSet<>();
|
|
|
|
|
if (stepList == null || stepList.isEmpty()) {
|
|
|
|
|
return idSet;
|
|
|
|
|
}
|
|
|
|
|
for (EmsAlarmActionStep step : stepList) {
|
|
|
|
|
idSet.addAll(parseOssIds(step.getOssIds()));
|
|
|
|
|
}
|
|
|
|
|
return idSet;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将逗号分隔的 ossId 字符串解析为 Long 列表。
|
|
|
|
|
*
|
|
|
|
|
* <p><b>为什么做 NumberFormatException 防御</b>:
|
|
|
|
|
* ossIds 字符串从数据库读取,理论上只包含合法的数字和逗号。
|
|
|
|
|
* 但如果数据库被手动修改、或历史迁移数据中混入了非数字字符,
|
|
|
|
|
* 不负责任的解析会导致整个查询失败。
|
|
|
|
|
* 这里选择 {@code throw IllegalArgumentException}(Fail Fast),
|
|
|
|
|
* 立即暴露数据问题,避免脏数据悄悄传播到前端。
|
|
|
|
|
*
|
|
|
|
|
* @param ossIds 逗号分隔的 OSS ID 字符串,可能为 null 或空
|
|
|
|
|
* @return Long 列表,保持原始顺序
|
|
|
|
|
* @throws IllegalArgumentException 如果包含非数字字符
|
|
|
|
|
*/
|
|
|
|
|
private List<Long> parseOssIds(String ossIds)
|
|
|
|
|
{
|
|
|
|
|
List<Long> 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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|