package jnpf.flowable.service; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.base.entity.VisualdevEntity; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.mapper.FilePermissionRuntimeMapper; import jnpf.flowable.mapper.TemplateNodeMapper; import jnpf.flowable.model.filepermission.FilePermissionNativeState; import jnpf.flowable.model.filepermission.FilePermissionNativeState.Member; import jnpf.flowable.model.filepermission.FilePermissionPolicy.*; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.flowable.util.ServiceUtil; import jnpf.flowable.util.filepermission.FilePermissionRuntime; import jnpf.flowable.util.filepermission.FilePermissionSnapshot; import jnpf.util.JsonUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.nio.charset.StandardCharsets; import java.util.*; /** * 以业务库已提交状态为生效点,每次动作重新读取;不把控制器末尾的事件当作授权源。 * 不缓存允许结果、不改原生流转事务。revision仅用于详情刷新,不能用作授权票据。 */ @Service @RequiredArgsConstructor public class NativeFilePermissionRuntimeSource implements FilePermissionRuntimeSource { private final FilePermissionRuntimeMapper mapper; private final TemplateNodeMapper nodes; private final ServiceUtil forms; @Override public FilePermissionRuntime.Facts read(String tenantId, String taskId, String formId, String userId, FilePermissionSnapshot snapshot) { return readWithPublishedForm(tenantId, taskId, formId, userId, snapshot, null); } @Override public FilePermissionRuntime.Facts readWithPublishedForm(String tenantId, String taskId, String formId, String userId, FilePermissionSnapshot snapshot, String formData) { FilePermissionNativeState state = mapper.find(taskId); if (state == null) throw new IllegalArgumentException("流程不存在"); List nodeList = nodes.getList(state.getFlowId()); if (formData == null) { VisualdevEntity form = forms.getReleaseInfo(formId); if (form == null) throw new IllegalArgumentException("发布表单不存在"); formData = form.getFormData(); } FilePermissionRuntime.Facts facts = project(tenantId, formId, userId, snapshot, state, nodeList, JsonUtil.getJsonToBean(formData, Map.class)); // 配置读取可能跨服务;若期间实例已推进,不返回旧人员/新配置混合的允许结果。 if (!fingerprint(state).equals(fingerprint(mapper.find(taskId)))) facts.transitioning = true; return facts; } /** 使用原生任务/经办/记录模型做投影,测试可直接覆盖多节点和历史节点情形。 */ public static FilePermissionRuntime.Facts project(String tenantId, String formId, String userId, FilePermissionSnapshot snapshot, FilePermissionNativeState state, List nodes, Map form) { FilePermissionRuntime.Facts f = new FilePermissionRuntime.Facts(); f.tenantId = tenantId; f.taskId = state.getTaskId(); f.formId = formId; f.userId = userId; f.creatorId = state.getCreatorId(); f.revision = fingerprint(state); // 自由流程会重新生成编码,映射尚未验证时拒绝,不能套用其他节点规则。 if (Integer.valueOf(3).equals(state.getFlowType()) || nodes == null || form == null || state.getStatus() == null) return f; int status = state.getStatus(); if (!Arrays.asList(0, 1, 2, 3, 4, 7, 8, 9).contains(status)) return f; if (status == 2 || status == 3) f.terminal = FilePermissionRuntime.Terminal.COMPLETED; if (status == 7) f.terminal = FilePermissionRuntime.Terminal.CANCELLED; if (status == 4) f.terminal = FilePermissionRuntime.Terminal.TERMINATED; Map byCode = new HashMap<>(); TemplateNodeEntity start = null; for (TemplateNodeEntity node : nodes) { byCode.put(node.getNodeCode(), node); if ("start".equals(node.getNodeType())) start = node; } if (start == null) return f; // 节点引用已改变时,不以最新编码猜测历史快照的指向。 for (Rule rule : snapshot.getRules()) { if (rule.triggerNodeId != null && !byCode.containsKey(rule.triggerNodeId) || rule.subjectNodeId != null && !byCode.containsKey(rule.subjectNodeId)) return f; } if (status == 0 || status == 8 || status == 9) f.activeNodes.add(start.getNodeCode()); if (status == 1 && state.getCurrentNodeCode() != null) { for (String node : state.getCurrentNodeCode().split(",")) if (!node.trim().isEmpty()) f.activeNodes.add(node.trim()); } Set entered = new HashSet<>(snapshot.getEnteredNodes()); Set readableNodes = new HashSet<>(); Set writableNodes = new HashSet<>(); Set assignedNodes = new HashSet<>(); if (Objects.equals(userId, f.creatorId)) { readableNodes.add(start.getNodeCode()); if (status == 0 || status == 8 || status == 9) writableNodes.add(start.getNodeCode()); } for (Member member : state.getMembers()) { boolean operator = "operator".equals(member.getKind()); boolean record = "record".equals(member.getKind()); boolean validOperator = operator && member.getStatus() != null && member.getStatus() >= 0; if (validOperator && member.getUserId() != null) assignedNodes.add(member.getNodeCode()); // 失效经办/尚未激活的未来经办不能用于授予参与人权限;真实操作记录保留参与事实。 if (validOperator || record || "circulate".equals(member.getKind())) { if (member.getUserId() != null) f.participants.computeIfAbsent(member.getNodeCode(), key -> new HashSet<>()).add(member.getUserId()); if (Objects.equals(userId, member.getUserId())) readableNodes.add(member.getNodeCode()); } if (validOperator || record) entered.add(member.getNodeCode()); if (validOperator && status == 1 && Integer.valueOf(0).equals(member.getCompletion()) && member.getHandleStatus() == null && member.getStartHandleTime() != null && member.getUserId() != null && f.activeNodes.contains(member.getNodeCode())) { f.currentHandlers.computeIfAbsent(member.getNodeCode(), key -> new HashSet<>()).add(member.getUserId()); if (Objects.equals(userId, member.getUserId())) writableNodes.add(member.getNodeCode()); } } entered.addAll(f.activeNodes); for (Rule rule : snapshot.getRules()) { if (rule.trigger != Trigger.ENTERED) continue; if (rule.onReturn == OnReturn.RELEASE) { if (snapshot.getActiveReleaseRules().contains(rule.id)) f.activeEnteredRules.add(rule.id); } else if (entered.contains(rule.triggerNodeId)) { f.activeEnteredRules.add(rule.id); } } // 当前节点与经办尚未一致时不放行;不把未落库办理人当成空角色。 if (status == 1) { if (f.activeNodes.isEmpty()) return f; for (String code : f.activeNodes) { TemplateNodeEntity node = byCode.get(code); if (node == null) return f; if (Arrays.asList("approver", "processing").contains(node.getNodeType()) && !assignedNodes.contains(code)) return f; } } Map readable = nodeModels(readableNodes, byCode, formId, start.getFormId()); Map writable = nodeModels(writableNodes, byCode, formId, start.getFormId()); if (readable.isEmpty()) return f; collect(form.get("fields"), null, Boolean.TRUE.equals(form.get("disabled")), readable.values(), writable.values(), f); f.verified = true; return f; } private static Map nodeModels(Set codes, Map nodes, String formId, String startForm) { Map result = new HashMap<>(); for (String code : codes) { TemplateNodeEntity node = nodes.get(code); if (node == null) continue; String id = node.getFormId() == null || node.getFormId().isEmpty() ? startForm : node.getFormId(); if (formId.equals(id)) result.put(code, JsonUtil.getJsonToBean(node.getNodeJson(), NodeModel.class)); } return result; } private static void collect(Object value, String parent, boolean disabled, Collection readable, Collection writable, FilePermissionRuntime.Facts facts) { if (!(value instanceof List)) throw new IllegalArgumentException("表单字段结构无效"); for (Object item : (List) value) { if (!(item instanceof Map)) throw new IllegalArgumentException("表单字段结构无效"); Map field = (Map) item; if (!(field.get("__config__") instanceof Map)) throw new IllegalArgumentException("表单字段结构无效"); Map config = (Map) field.get("__config__"); String key = Objects.toString(field.get("__vModel__"), ""); String id = parent == null ? key : parent + "-" + key; boolean locked = disabled || Boolean.TRUE.equals(field.get("disabled")) || Boolean.TRUE.equals(config.get("isDisplayOnly")); if ("uploadFile".equals(config.get("jnpfKey"))) { boolean read = readable.stream().anyMatch(node -> permit(node, id, "read") && (parent == null || permit(node, parent, "read"))); boolean write = read && !locked && writable.stream().anyMatch(node -> permit(node, id, "read") && permit(node, id, "write") && (parent == null || permit(node, parent, "read") && permit(node, parent, "write"))); Map bounds = new EnumMap<>(Action.class); bounds.put(Action.VIEW, read); bounds.put(Action.DOWNLOAD, read && (!field.containsKey("allowDownload") || Boolean.TRUE.equals(field.get("allowDownload")))); // 显式文件EDIT独立于节点字段write;INHERIT仍遵守原生可写条件。 bounds.put(Action.EDIT, read && !locked && facts.terminal == FilePermissionRuntime.Terminal.NONE && Boolean.TRUE.equals(field.get("onlineEdit"))); bounds.put(Action.REVIEW, Boolean.TRUE.equals(bounds.get(Action.EDIT)) && facts.currentHandlers.entrySet().stream().anyMatch(e -> facts.activeNodes.contains(e.getKey()) && e.getValue().contains(facts.userId))); bounds.put(Action.ADD, write); bounds.put(Action.REPLACE, write); bounds.put(Action.REMOVE, write); facts.nativeUpperBounds.put(id, bounds); Map inherited = new EnumMap<>(bounds); inherited.put(Action.EDIT, write && Boolean.TRUE.equals(field.get("onlineEdit"))); inherited.put(Action.REVIEW, false); facts.inherited.put(id, inherited); } if (config.get("children") instanceof List) { collect(config.get("children"), "table".equals(config.get("jnpfKey")) ? key : parent, locked, readable, writable, facts); } } } private static boolean permit(NodeModel node, String field, String action) { if (node == null) return false; if (node.getFormOperates() == null || node.getFormOperates().isEmpty()) return true; for (Map item : node.getFormOperates()) if (field.equals(item.get("id"))) return Boolean.TRUE.equals(item.get(action)); return true; // 与原生字段无单独配置时的默认行为一致。 } private static String fingerprint(FilePermissionNativeState value) { if (value == null) return "missing"; try { return DigestUtils.md5DigestAsHex(new ObjectMapper().writeValueAsString(value).getBytes(StandardCharsets.UTF_8)); } catch (java.io.IOException e) { throw new IllegalArgumentException("运行态读取失败", e); } } }