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<TemplateNodeEntity> 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<TemplateNodeEntity> nodes,
|
Map<String, Object> 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<String, TemplateNodeEntity> 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<String> entered = new HashSet<>(snapshot.getEnteredNodes());
|
Set<String> readableNodes = new HashSet<>();
|
Set<String> writableNodes = new HashSet<>();
|
Set<String> 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<String, NodeModel> readable = nodeModels(readableNodes, byCode, formId, start.getFormId());
|
Map<String, NodeModel> 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<String, NodeModel> nodeModels(Set<String> codes, Map<String, TemplateNodeEntity> nodes, String formId, String startForm) {
|
Map<String, NodeModel> 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<NodeModel> readable,
|
Collection<NodeModel> 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<Action, Boolean> 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<Action, Boolean> 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<String, Object> 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); }
|
}
|
}
|