From e8e2061b596da3eff9feb932e040e7c46bdef51f Mon Sep 17 00:00:00 2001 From: gulongcheng <474084054@qq.com> Date: Mon, 29 Dec 2025 16:41:41 +0800 Subject: [PATCH] =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=80=BB=E8=A7=88=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E6=8C=89=E9=92=AE=E6=9D=83=E9=99=90=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resp/data/FileMetadataInfoResp.java | 3 + .../data/model/entity/FileMetadataInfo.java | 4 ++ .../service/IFileUserPermissionService.java | 8 +++ .../impl/FileUserPermissionServiceImpl.java | 44 ++++++++++++ .../impl/MinioFileIDataFileServiceImpl.java | 68 ++++++++++++++++--- .../flowable/enums/FlowElementTypeEnums.java | 2 +- .../com/sdm/flowable/enums/NodeStateEnum.java | 7 ++ .../sdm/flowable/process/ProcessService.java | 59 ++++++++++++++-- 8 files changed, 180 insertions(+), 15 deletions(-) diff --git a/common/src/main/java/com/sdm/common/entity/resp/data/FileMetadataInfoResp.java b/common/src/main/java/com/sdm/common/entity/resp/data/FileMetadataInfoResp.java index 0e56f7a7..e1e441ba 100644 --- a/common/src/main/java/com/sdm/common/entity/resp/data/FileMetadataInfoResp.java +++ b/common/src/main/java/com/sdm/common/entity/resp/data/FileMetadataInfoResp.java @@ -114,6 +114,9 @@ public class FileMetadataInfoResp implements Serializable { @Schema(description = "完整访问URL(拼接minio网关地址 + objectKey)") private String fileUrl; + @Schema(description = "权限值") + private Integer permissionValue; + private String approvalStatus; private String approveType; private String tempMetadata; diff --git a/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java b/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java index 824ff8a0..806d07b8 100644 --- a/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java +++ b/data/src/main/java/com/sdm/data/model/entity/FileMetadataInfo.java @@ -98,6 +98,10 @@ public class FileMetadataInfo implements Serializable { @TableField(value = "creatorName", insertStrategy = FieldStrategy.NEVER,select = false,updateStrategy = FieldStrategy.NEVER) private String creatorName; + @Schema(description= "更新者名称,列表展示使用") + @TableField(value = "updaterName", insertStrategy = FieldStrategy.NEVER,select = false,updateStrategy = FieldStrategy.NEVER) + private String updaterName; + @Schema(description= "创建时间") @TableField("createTime") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") diff --git a/data/src/main/java/com/sdm/data/service/IFileUserPermissionService.java b/data/src/main/java/com/sdm/data/service/IFileUserPermissionService.java index 7d0d856d..12a9a5f0 100644 --- a/data/src/main/java/com/sdm/data/service/IFileUserPermissionService.java +++ b/data/src/main/java/com/sdm/data/service/IFileUserPermissionService.java @@ -21,4 +21,12 @@ public interface IFileUserPermissionService extends IService * @return 是否有权限 */ boolean hasFilePermission(Long fileId, Long userId, FilePermissionEnum permission); + + /** + * 获取文件权限位掩码 + * @param fileId 文件ID + * @param userId 用户ID + * @return 权限位掩码 + */ + Integer getMergedPermission(Long fileId, Long userId); } diff --git a/data/src/main/java/com/sdm/data/service/impl/FileUserPermissionServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/FileUserPermissionServiceImpl.java index 5a5b4207..b8a2ac7a 100644 --- a/data/src/main/java/com/sdm/data/service/impl/FileUserPermissionServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/FileUserPermissionServiceImpl.java @@ -83,4 +83,48 @@ public class FileUserPermissionServiceImpl extends ServiceImpl 0) { + // 使用 | (按位或) 合并当前权限与父级权限 + return (byte) (currentPerm | calculateRecursiveMergedPermission(currentFile.getParentId(), userId)); + } + + return currentPerm; + } } diff --git a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java index e5b6b2c8..cd0ef779 100644 --- a/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java +++ b/data/src/main/java/com/sdm/data/service/impl/MinioFileIDataFileServiceImpl.java @@ -57,13 +57,11 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.ObjectUtils; -import org.apache.commons.lang3.tuple.Pair; import org.assertj.core.util.DateUtil; import org.jetbrains.annotations.NotNull; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.http.ResponseEntity; import org.springframework.mock.web.MockMultipartFile; @@ -644,7 +642,20 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { setAnalysisDirectionName(files); setSimulationPoolAndTaskInfo(files); PageInfo page = new PageInfo<>(files); - return PageUtils.getJsonObjectSdmResponse(files, page); + + long total = page.getTotal(); + List dtoList = files.stream().map(entity -> { + FileMetadataInfoResp dto = new FileMetadataInfoResp(); + BeanUtils.copyProperties(entity, dto); + + //计算当前用户对该文件的综合权限位 + // 对于列表查询,如果层级很深,频繁递归会有性能问题。 + dto.setPermissionValue(fileUserPermissionService.getMergedPermission(entity.getId(), ThreadLocalContext.getUserId())); + return dto; + }).collect(Collectors.toList()); + PageInfo page1 = new PageInfo<>(dtoList); + page1.setTotal(total); + return PageUtils.getJsonObjectSdmResponse(dtoList, page1); } @Override @@ -661,6 +672,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { FileMetadataInfoResp fileMetadataInfoResp = new FileMetadataInfoResp(); BeanUtils.copyProperties(fileMetadataInfo, fileMetadataInfoResp); + fileMetadataInfoResp.setPermissionValue(fileUserPermissionService.getMergedPermission(fileMetadataInfo.getId(), ThreadLocalContext.getUserId())); + return SdmResponse.success(fileMetadataInfoResp); } @@ -707,6 +720,10 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { List dtoList = list.stream().map(entity -> { FileMetadataInfoResp dto = new FileMetadataInfoResp(); BeanUtils.copyProperties(entity, dto); + + //计算当前用户对该文件的综合权限位 + // 对于列表查询,如果层级很深,频繁递归会有性能问题。 + dto.setPermissionValue(fileUserPermissionService.getMergedPermission(entity.getId(), ThreadLocalContext.getUserId())); return dto; }).collect(Collectors.toList()); PageInfo page1 = new PageInfo<>(dtoList); @@ -784,7 +801,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { newObjectKey = oldObjectKey.substring(0, oldObjectKey.lastIndexOf("/") + 1) + newName; minioService.renameFile(oldObjectKey, newObjectKey,bucketName); - fileMetadataInfoService.lambdaUpdate().set(FileMetadataInfo::getObjectKey, newObjectKey).set(FileMetadataInfo::getOriginalName, newName).eq(FileMetadataInfo::getId, fileId).update(); + fileMetadataInfoService.lambdaUpdate() + .set(FileMetadataInfo::getObjectKey, newObjectKey) + .set(FileMetadataInfo::getOriginalName, newName) + .set(FileMetadataInfo::getUpdateTime, new Date()) + .set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId()) + .eq(FileMetadataInfo::getId, fileId).update(); return SdmResponse.success("重命名成功"); } catch (Exception e) { log.error("重命名文件失败", e); @@ -944,7 +966,12 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { try { minioService.renameFile(oldDirMinioObjectKey, newDirMinioObjectKey,dirMetadataInfo.getBucketName()); - fileMetadataInfoService.lambdaUpdate().set(FileMetadataInfo::getObjectKey, newDirMinioObjectKey).set(FileMetadataInfo::getOriginalName, req.getNewName()).eq(FileMetadataInfo::getId, dirMetadataInfo.getId()).update(); + fileMetadataInfoService.lambdaUpdate() + .set(FileMetadataInfo::getObjectKey, newDirMinioObjectKey) + .set(FileMetadataInfo::getOriginalName, req.getNewName()) + .set(FileMetadataInfo::getUpdateTime, new Date()) + .set(FileMetadataInfo::getUpdaterId, ThreadLocalContext.getUserId()) + .eq(FileMetadataInfo::getId, dirMetadataInfo.getId()).update(); return SdmResponse.success("重命名目录成功"); } catch (Exception e) { log.error("重命名目录失败", e); @@ -1522,13 +1549,14 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { fileMetadataInfoLambdaQueryChainWrapper.eq(FileMetadataInfo::getParentId, parentDirId); } List list = fileMetadataInfoLambdaQueryChainWrapper.eq(FileMetadataInfo::getTenantId, ThreadLocalContext.getTenantId()).eq(FileMetadataInfo::getDataType, DataTypeEnum.DIRECTORY.getValue()).orderByDesc(FileMetadataInfo::getCreateTime).list(); + setCreatorNames(list); List dtoList = list.stream().map(entity -> { FileMetadataInfoResp dto = new FileMetadataInfoResp(); BeanUtils.copyProperties(entity, dto); - - // todo 后面接入用户系统设置 - dto.setCreatorName(""); + //计算当前用户对该文件的综合权限位 + // 对于列表查询,如果层级很深,频繁递归会有性能问题。 + dto.setPermissionValue(fileUserPermissionService.getMergedPermission(entity.getId(), ThreadLocalContext.getUserId())); return dto; }).collect(Collectors.toList()); @@ -1577,10 +1605,11 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { tempFileMetadataInfo.setRemarks(req.getRemarks()); tempFileMetadataInfo.setSimulationPoolInfoList(req.getSimulationPoolInfoList()); tempFileMetadataInfo.setCreateTime(fileMetadataInfo.getCreateTime()); - tempFileMetadataInfo.setUpdateTime(fileMetadataInfo.getCreateTime()); + tempFileMetadataInfo.setUpdaterId(ThreadLocalContext.getUserId()); tempFileMetadataInfo.setUpdateTime(LocalDateTime.now()); fileMetadataInfo.setTempMetadata(JSONObject.toJSONString(tempFileMetadataInfo)); fileMetadataInfo.setUpdateTime(LocalDateTime.now()); + fileMetadataInfo.setUpdaterId(ThreadLocalContext.getUserId()); //发起审批 FileApproveRequestBuilder updateFileMetaIntoApproveRequestBuilder = FileApproveRequestBuilder.builder() @@ -1615,6 +1644,8 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { fileMetadataInfo.setProjectId(req.getProjectId()); fileMetadataInfo.setAnalysisDirectionId(req.getAnalysisDirectionId()); fileMetadataInfo.setRemarks(req.getRemarks()); + fileMetadataInfo.setUpdateTime(LocalDateTime.now()); + fileMetadataInfo.setUpdaterId(ThreadLocalContext.getUserId()); } fileMetadataInfoService.updateById(fileMetadataInfo); @@ -2172,12 +2203,20 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { try { if (ObjectUtils.isNotEmpty(list)) { // 提取去重的 creatorId - List creatorIds = list.stream() + List creatorIds = new ArrayList<>(list.stream() .map(FileMetadataInfo::getCreatorId) .filter(Objects::nonNull) .distinct() + .toList()); + + List updaterIds = list.stream() + .map(FileMetadataInfo::getUpdaterId) + .filter(Objects::nonNull) + .distinct() .toList(); + creatorIds.addAll(updaterIds); + // 远程查询用户信息 SdmResponse> userListSdmRsp = sysUserFeignClient.listUserByIds( UserQueryReq.builder().userIds(creatorIds).build() @@ -2194,6 +2233,15 @@ public class MinioFileIDataFileServiceImpl implements IDataFileService { cidUser.getRealName() ); fileMetadataInfo.setCreatorName(username); + + Long updaterId = fileMetadataInfo.getUpdaterId(); + CIDUserResp cidUser1 = cidUserMap.get(updaterId); + String username1 = Objects.isNull(cidUser1) ? "" : org.apache.commons.lang3.StringUtils.firstNonBlank( + cidUser1.getNickname(), + cidUser1.getUsername(), + cidUser1.getRealName() + ); + fileMetadataInfo.setUpdaterName(username1); }); } } diff --git a/flowable/src/main/java/com/sdm/flowable/enums/FlowElementTypeEnums.java b/flowable/src/main/java/com/sdm/flowable/enums/FlowElementTypeEnums.java index 0f71d9a0..cc3cf093 100644 --- a/flowable/src/main/java/com/sdm/flowable/enums/FlowElementTypeEnums.java +++ b/flowable/src/main/java/com/sdm/flowable/enums/FlowElementTypeEnums.java @@ -19,7 +19,7 @@ public enum FlowElementTypeEnums { public static FlowElementTypeEnums fromString(String type) { for (FlowElementTypeEnums flowElementType : FlowElementTypeEnums.values()) { - if (flowElementType.type.equals(type)) { + if (flowElementType.type.equalsIgnoreCase(type)) { return flowElementType; } } diff --git a/flowable/src/main/java/com/sdm/flowable/enums/NodeStateEnum.java b/flowable/src/main/java/com/sdm/flowable/enums/NodeStateEnum.java index 7dd9ef31..a3d0a57d 100644 --- a/flowable/src/main/java/com/sdm/flowable/enums/NodeStateEnum.java +++ b/flowable/src/main/java/com/sdm/flowable/enums/NodeStateEnum.java @@ -18,6 +18,13 @@ public enum NodeStateEnum { * 挂起 */ SUSPENDED("suspended"), + + /** + * [新增] 等待用户确认(对应 Manual 模式卡在 _waitUser 节点) + * 前端看到这个状态应显示"执行/继续"按钮 + */ + WAITING_FOR_USER("waiting_for_user"), + /** * 错误 diff --git a/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java b/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java index 1a241b65..7efd4d54 100644 --- a/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java +++ b/flowable/src/main/java/com/sdm/flowable/process/ProcessService.java @@ -286,7 +286,7 @@ public class ProcessService implements Iprocess{ // 4. 计算流程实例整体状态 // 只要 hasDeadLetterJobs 为 false,流程状态就会显示为 running (或 suspended) - processInfo = buildProcessInstanceInfo(processInstanceId, context.isRunning(), context.isSuspended(), context.isHasDeadLetterJobs()); + processInfo = buildProcessInstanceInfo(processInstanceId, context); // 5. 计算每个节点的状态 (核心逻辑) calculateNodeStates(allNodes, context); @@ -303,6 +303,10 @@ public class ProcessService implements Iprocess{ boolean isRunning; boolean isSuspended; boolean hasDeadLetterJobs; + + // 记录最早发生错误的时间(死信作业的创建时间) + Date earliestErrorTime; + // Map Map errorMap = new HashMap<>(); // List @@ -331,6 +335,7 @@ public class ProcessService implements Iprocess{ ProcessStateContext ctx = new ProcessStateContext(); // 1. 获取运行时流程实例对象(判断是否运行中、是否挂起) + // ACT_RU_EXECUTION (运行时执行实例表) ProcessInstance runtimeInstance = runtimeService.createProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult(); @@ -338,18 +343,22 @@ public class ProcessService implements Iprocess{ ctx.setSuspended(ctx.isRunning() && runtimeInstance.isSuspended()); // 2. 准备历史数据 + // 查询 ACT_HI_ACTINST 该流程实例下,所有的历史节点记录 List historicActivities = historyService.createHistoricActivityInstanceQuery() .processInstanceId(processInstanceId) .list(); for (HistoricActivityInstance hist : historicActivities) { if (!ctx.historyMap.containsKey(hist.getActivityId()) || hist.getStartTime().after(ctx.historyMap.get(hist.getActivityId()).getStartTime())) { + // 只保留节点最新的开始时间记录,防止重复覆盖(例如:重试作业) ctx.historyMap.put(hist.getActivityId(), hist); } } // 3. 准备运行时 Active ID 列表 if (ctx.isRunning()) { + // 查 ACT_RU_EXECUTION 运行时活跃节点ID列表 + // historicActivities 也一定会有活跃节点ID,当流程的流转入 serviceTask_id1 的那一瞬间,引擎会立即在 ACT_HI_ACTINST 表插入一条记录 ctx.activeActivityIds.addAll(runtimeService.getActiveActivityIds(processInstanceId)); // A. 查死信 (Error 来源) @@ -362,6 +371,9 @@ public class ProcessService implements Iprocess{ // C. 只有当有 作业(死信或普通) 时,才去查 Execution 映射 if (!deadJobs.isEmpty() || !activeJobs.isEmpty()) { + // 查 Execution 表,获取 ExecutionId 和 ActivityId 映射 + // Flowable 的 Job 表(ACT_RU_JOB 或 ACT_RU_DEADLETTER_JOB)存储的是“干活的任务单”,里面只有 executionId(执行指针 ID),没有 activityId(BPMN 里的节点 ID,如 ServiceTask_1) + // 就算已经进入 ACT_RU_DEADLETTER_JOB 死信队列了 , ACT_RU_EXECUTION 还是会有对应的 ExecutionId 和 ActivityId List executions = runtimeService.createExecutionQuery() .processInstanceId(processInstanceId).list(); Map executionToActivityMap = executions.stream() @@ -371,6 +383,16 @@ public class ProcessService implements Iprocess{ // 处理死信 -> 放入 errorMap if (!deadJobs.isEmpty()) { ctx.setHasDeadLetterJobs(true); + + // 找到最早的死信时间,作为流程“卡住”的时间点 + Date firstError = deadJobs.stream() + .map(Job::getCreateTime) + .filter(Objects::nonNull) + .min(Date::compareTo) + .orElse(null); + ctx.setEarliestErrorTime(firstError); + + for (Job job : deadJobs) { if (job.getExceptionMessage() != null) { String actId = executionToActivityMap.get(job.getExecutionId()); @@ -393,7 +415,11 @@ public class ProcessService implements Iprocess{ } // 构建流程实例信息 - private ProcessInstanceInfo buildProcessInstanceInfo(String processInstanceId, boolean isRunning, boolean isSuspended, boolean hasError) { + private ProcessInstanceInfo buildProcessInstanceInfo(String processInstanceId, ProcessStateContext context) { + boolean isRunning = context.isRunning(); + boolean isSuspended = context.isSuspended(); + boolean hasError = context.isHasDeadLetterJobs(); + HistoricProcessInstance historicInstance = historyService.createHistoricProcessInstanceQuery() .processInstanceId(processInstanceId) .singleResult(); @@ -410,9 +436,22 @@ public class ProcessService implements Iprocess{ // 计算耗时 Long duration = historicInstance.getDurationInMillis(); + // 如果 duration 为空(说明流程没结束),且流程已开始 if (duration == null && historicInstance.getStartTime() != null && isRunning) { - duration = System.currentTimeMillis() - historicInstance.getStartTime().getTime(); + + if (hasError && context.getEarliestErrorTime() != null) { + // 情况 A: 流程报错了,耗时 = 报错时刻 - 开始时刻 + // 这样时间就“定格”了,不会随着你查询时间而增加 + duration = context.getEarliestErrorTime().getTime() - historicInstance.getStartTime().getTime(); + } else { + // 情况 B: 正常运行或挂起,耗时 = 当前时刻 - 开始时刻 + duration = System.currentTimeMillis() - historicInstance.getStartTime().getTime(); + } } + + // 防止出现负数(极端并发情况下) + if (duration != null && duration < 0) duration = 0L; + info.setDurationInMillis(duration); info.setDurationFormatted(duration != null ? formatDuration(duration) : null); @@ -500,7 +539,19 @@ public class ProcessService implements Iprocess{ if (isActive) { // 只要是 Active,就根据流程整体状态来定颜色 - node.setStatus(ctx.isSuspended() ? NodeStateEnum.SUSPENDED.getCode() : NodeStateEnum.ACTIVE.getCode()); + // 如果流程整体被挂起,优先显示挂起 + if (ctx.isSuspended()) { + node.setStatus(NodeStateEnum.SUSPENDED.getCode()); + } else if (ctx.activeActivityIds.contains(waitUserId)) { + // 停留在 _waitUser -> 等待用户操作 (Manual模式) + node.setStatus(NodeStateEnum.WAITING_FOR_USER.getCode()); + } else if (FlowElementTypeEnums.USERTASK.getType().equalsIgnoreCase(node.getType()) && ctx.activeActivityIds.contains(origId)) { + // 这是一个普通的 UserTask,且当前正卡在这里 + node.setStatus(NodeStateEnum.WAITING_FOR_USER.getCode()); + }else { + // 停留在 origId 或 waitId 或 checkId-> 视为通用执行中 + node.setStatus(NodeStateEnum.ACTIVE.getCode()); + } calculateAggregatedTime(node, ctx, waitUserId, origId, checkId); return; }