package jnpf.base.service; import cn.dev33.satoken.stp.StpUtil; import jnpf.base.UserInfo; import jnpf.base.ModuleButtonApi; import jnpf.base.entity.ModuleButtonEntity; import jnpf.base.entity.VisualdevEntity; import jnpf.base.model.ColumnDataModel; import jnpf.base.model.button.ModuleButtonModel; import jnpf.base.model.form.ModuleFormModel; import jnpf.base.model.uploadedit.UploadEditContext; import jnpf.base.util.FlowFormDataUtil; import jnpf.base.util.UploadEditStateMachine; import jnpf.exception.DataException; import jnpf.flowable.WorkFlowApi; import jnpf.flowable.entity.TaskEntity; import jnpf.flowable.model.filepermission.FilePermissionView; import jnpf.flowable.model.filepermission.FilePermissionPolicy.Action; import jnpf.flowable.entity.TemplateNodeEntity; import jnpf.flowable.enums.NodeEnum; import jnpf.flowable.enums.TaskStatusEnum; import jnpf.flowable.model.templatenode.nodejson.NodeModel; import jnpf.onlinedev.model.OnlineInfoModel; import jnpf.permissions.PermissionInterfaceImpl; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** 复用发布表单、原生详情和当前流程字段权限;不按业务名称/QA 节点硬编码。 */ @Service @RequiredArgsConstructor public class UploadEditAccess { public static final String DMS_INCOMPATIBLE = "该审核单不符合当前共享附件协议,无法继续编辑或提交,请重新新建审核单"; private final ObjectProvider visualdevService; private final ObjectProvider formDataUtil; private final WorkFlowApi workFlowApi; private final UploadFilePermissionQuery filePermissions; private final jnpf.onlyoffice.UploadFileCapabilityApi capabilityApi; private final ModuleButtonApi moduleButtonApi; private final UploadEditReadService sharedReads; public void requireEnabled() { jnpf.base.ActionResult> response = capabilityApi.options(); if (response == null || !jnpf.base.ActionResultCode.SUCCESS.getCode().equals(response.getCode()) || response.getData() == null || !Boolean.TRUE.equals(response.getData().get("editEnabled"))) { throw new DataException("上传控件在线编辑尚未启用"); } } public List> baseline(UploadEditContext context, String ownerId) { return checkedBaseline(context, ownerId).getFiles(); } /** 将当前准入结果与真实记录上下文一起交给内部编辑租约,避免再次猜测是否受控。 */ @lombok.Value public static class CheckedBaseline { List> files; boolean controlled; String mode; boolean dmsAudit; FilePermissionView filePermissionView; public CheckedBaseline(List> files, boolean controlled) { this(files, controlled, "edit"); } public CheckedBaseline(List> files, boolean controlled, String mode) { this(files, controlled, mode, false); } public CheckedBaseline(List> files, boolean controlled, String mode, boolean dmsAudit) { this(files, controlled, mode, dmsAudit, null); } public CheckedBaseline(List> files, boolean controlled, String mode, boolean dmsAudit, FilePermissionView permissions) { this.files = files; this.controlled = controlled; this.mode = mode; this.dmsAudit = dmsAudit; this.filePermissionView = permissions; } } public CheckedBaseline checkedBaseline(UploadEditContext context, String ownerId) { return check(context, ownerId, true); } /** 共享稿读取独立于编辑能力;只能用于真实启用文件权限的流程。 */ public CheckedBaseline sharedBaseline(UploadEditContext context, String userId) { return check(context, userId, true, true); } public List sharedBaselines(List contexts, String userId) { if (contexts == null || contexts.isEmpty() || contexts.size() > 200) throw new DataException("附件批量查询数量无效"); UploadEditContext first = contexts.get(0); if (first == null) throw new DataException("缺少附件上下文"); for (UploadEditContext c : contexts) { if (c == null || !Objects.equals(first.getModelId(), c.getModelId()) || !Objects.equals(first.getRecordId(), c.getRecordId()) || !Objects.equals(first.getTaskId(), c.getTaskId()) || !Objects.equals(first.getMenuId(), c.getMenuId())) throw new DataException("附件批量查询必须属于同一表单记录"); } SharedReadScope scope = new SharedReadScope(); return contexts.stream().map(c -> check(c, userId, true, true, scope)).toList(); } private static final class SharedReadScope { VisualdevEntity model; Map form; final Map tasks = new java.util.HashMap<>(); final Map permissions = new java.util.HashMap<>(); } /** 未保存的DMS新增/修订审核统一采用附件副本,其余业务保留原临时编辑行为。 */ public boolean isDmsAuditTemporary(UploadEditContext context) { if (context == null || StringUtils.hasText(context.getRecordId()) || StringUtils.hasText(context.getTaskId())) return false; return isDmsAudit(context); } public boolean isDmsAudit(UploadEditContext context) { if (context == null) return false; return isDmsAttachment(context) && dmsAuditTable(context.getModelId()) != null; } private static boolean isDmsAttachment(UploadEditContext context) { return (!StringUtils.hasText(context.getParentField()) && "wenjian".equals(context.getField())) || ("jilu_mingxi".equals(context.getParentField()) && "jilu_wenjian".equals(context.getField())); } public String dmsAuditTable(String modelId) { VisualdevEntity model = visualdevService.getObject().getReleaseInfo(modelId); if (model == null) return null; Map form = JsonUtil.getJsonToBean(model.getFormData(), Map.class); return dmsAuditTable(form); } private static String dmsAuditTable(Map form) { String table = Objects.toString(form.get("tableName"), ""); return Set.of("dms_wenjian_xinzeng_shenhe", "dms_wenjian_xiuding_shenhe").contains(table) ? table : null; } public void authorizeTemporary(UploadEditContext context, String ownerId) { check(context, ownerId, false); } private CheckedBaseline check(UploadEditContext context, String ownerId, boolean readValue) { return check(context, ownerId, readValue, false); } private CheckedBaseline check(UploadEditContext context, String ownerId, boolean readValue, boolean sharedRead) { return check(context, ownerId, readValue, sharedRead, null); } private CheckedBaseline check(UploadEditContext context, String ownerId, boolean readValue, boolean sharedRead, SharedReadScope scope) { if (!sharedRead) requireEnabled(); if (context == null || !StringUtils.hasText(context.getModelId()) || !StringUtils.hasText(context.getField())) { throw new DataException("缺少原生表单和附件字段上下文"); } VisualdevEntity model = scope != null && scope.model != null ? scope.model : sharedRead ? sharedReads.model(context.getModelId()) : visualdevService.getObject().getReleaseInfo(context.getModelId()); if (model == null) throw new DataException("表单不存在"); Map form = scope != null && scope.form != null ? scope.form : JsonUtil.getJsonToBean(model.getFormData(), Map.class); if (scope != null) { scope.model = model; scope.form = form; } boolean dmsAudit = isDmsAttachment(context) && dmsAuditTable(form) != null; Map field = findField(form.get("fields"), context.getField(), context.getParentField()); Map config = field == null ? Collections.emptyMap() : (Map) field.get("__config__"); if (field == null || config == null || !"uploadFile".equals(config.get("jnpfKey")) || !Boolean.TRUE.equals(field.get("onlineEdit")) || !sharedRead && (Boolean.TRUE.equals(field.get("disabled")) || Boolean.TRUE.equals(config.get("isDisplayOnly")) || Boolean.TRUE.equals(form.get("disabled")))) { throw new DataException("该字段未启用可写的在线编辑功能"); } if (!sharedRead && StringUtils.hasText(context.getParentField())) { Map parent = findField(form.get("fields"), context.getParentField(), null); if (parent == null || Boolean.TRUE.equals(parent.get("disabled"))) throw new DataException("子表不允许修改附件"); } if (!StringUtils.hasText(context.getRecordId())) { if (sharedRead) throw new DataException("共享附件必须属于已保存的流程"); StpUtil.checkPermission(context.getModelId()); ColumnDataModel columns = JsonUtil.getJsonToBean(model.getColumnData(), ColumnDataModel.class); checkButtonPermission(context, columns, "btn_add"); checkFormPermission(context, columns); return new CheckedBaseline(Collections.emptyList(), false, "edit", dmsAudit); } Map data = sharedRead && dmsAudit ? sharedReads.dmsAttachment(model, context, config) : null; if (data == null) data = formDataUtil.getObject().getEditDataInfo(model, context.getRecordId(), OnlineInfoModel.builder().menuId(context.getMenuId()).build()); if (data == null || data.isEmpty()) throw new DataException("原生表单记录不存在"); // 原生详情统一返回真实业务主键;流程任务 ID 只是详情查询的别名。 String recordId = string(data.get("id")); if (!StringUtils.hasText(recordId)) throw new DataException("无法识别原生记录主键"); context.setRecordId(recordId); String taskId = string(data.get("flowTaskId")); TaskEntity task = StringUtils.hasText(taskId) ? scope == null ? workFlowApi.getInfoSubmit(taskId) : scope.tasks.computeIfAbsent(taskId, workFlowApi::getInfoSubmit) : null; FilePermissionView permissions = StringUtils.hasText(taskId) ? scope == null ? currentPermissions(taskId, context, model, sharedRead) : scope.permissions.computeIfAbsent(taskId, id -> currentPermissions(id, context, model, true)) : null; boolean controlled = permissions != null && permissions.isEnabled(); if (!controlled && dmsAudit) throw new DataException(DMS_INCOMPATIBLE); if (sharedRead) { if (!controlled || task == null || !Objects.equals(taskId, task.getId()) || StringUtils.hasText(context.getTaskId()) && !Objects.equals(taskId, context.getTaskId()) || !Objects.equals(ownerId, UserProvider.getLoginUserId()) || !permissions.allows(permissionField(context), Action.VIEW)) throw new DataException("没有流程附件查看权限"); context.setTaskId(taskId); return new CheckedBaseline(UploadEditStateMachine.files(fieldValue(data, context)), true, "edit", dmsAudit, permissions); } String mode = "edit"; if (task != null) { if (!Objects.equals(taskId, task.getId())) throw new DataException("流程实例不匹配"); mode = checkFlow(context, ownerId, task, permissions); } else { // 普通原生记录也可能保存 flowTaskId=主键;只有明确的 legacy 响应可继续原权限。 if (controlled) throw new DataException("无法取得受控流程实例,不能继续编辑"); if (StringUtils.hasText(context.getTaskId())) throw new DataException("流程任务已不存在,不能继续编辑"); StpUtil.checkPermission(context.getModelId()); ColumnDataModel columns = JsonUtil.getJsonToBean(model.getColumnData(), ColumnDataModel.class); checkButtonPermission(context, columns, "btn_edit"); checkFormPermission(context, columns); } return new CheckedBaseline(readValue ? UploadEditStateMachine.files(fieldValue(data, context)) : Collections.emptyList(), controlled, mode, dmsAudit); } private void checkButtonPermission(UploadEditContext context, ColumnDataModel columns, String buttonCode) { if (columns == null || !Boolean.TRUE.equals(columns.getUseBtnPermission())) return; String moduleId = context.getModelId(); if (Boolean.TRUE.equals(columns.getOnlyAssociatedBtnPermission())) { List resources = moduleButtonApi.getListByModuleIdsForPermission( new ModuleButtonModel(Collections.singletonList(moduleId), 0)); if (resources != null && resources.stream().noneMatch(resource -> buttonCode.equals(resource.getEnCode()))) return; } StpUtil.checkPermission(moduleId + "::" + buttonCode); } /** 仅在编辑准入通过后使用;每次签发配置重读发布字段,不能信任请求里的开关。 */ public boolean allowDownload(UploadEditContext context) { VisualdevEntity model = visualdevService.getObject().getReleaseInfo(context.getModelId()); if (model == null) throw new DataException("表单不存在"); Map form = JsonUtil.getJsonToBean(model.getFormData(), Map.class); Map field = findField(form.get("fields"), context.getField(), context.getParentField()); if (field == null || !(field.get("__config__") instanceof Map) || !"uploadFile".equals(((Map) field.get("__config__")).get("jnpfKey"))) { throw new DataException("附件字段不存在"); } Object configured = field.get("allowDownload"); // 旧表单没有此属性时保持开启;损坏配置不能被解释成允许。 boolean configuredDownload = !field.containsKey("allowDownload") || Boolean.TRUE.equals(configured); if (!configuredDownload || !StringUtils.hasText(context.getRecordId())) return configuredDownload; // 工作稿的旧上下文可能未保存taskId;始终以真实记录的flowTaskId确定所属实例。 Map data = formDataUtil.getObject().getEditDataInfo(model, context.getRecordId(), OnlineInfoModel.builder().menuId(context.getMenuId()).build()); if (data == null || data.isEmpty()) throw new DataException("原生表单记录不存在"); String taskId = string(data.get("flowTaskId")); if (!StringUtils.hasText(taskId)) return configuredDownload; FilePermissionView permissions = currentPermissions(taskId, context, model); return permissions.allows(permissionField(context), Action.DOWNLOAD); } /** 插件能力只能从已发布的上传字段配置读取,不能由打开编辑器的请求声明。 */ public boolean qianpiyeAnnotatorEnabled(UploadEditContext context) { VisualdevEntity model = visualdevService.getObject().getReleaseInfo(context.getModelId()); if (model == null) throw new DataException("表单不存在"); Map form = JsonUtil.getJsonToBean(model.getFormData(), Map.class); Map field = findField(form.get("fields"), context.getField(), context.getParentField()); if (field == null || !(field.get("__config__") instanceof Map) || !"uploadFile".equals(((Map) field.get("__config__")).get("jnpfKey"))) { throw new DataException("附件字段不存在"); } return Boolean.TRUE.equals(field.get("onlineEdit")) && Boolean.TRUE.equals(field.get("qianpiyeAnnotatorEnabled")); } public boolean hasEnabledFields(String modelId) { VisualdevEntity model = visualdevService.getObject().getReleaseInfo(modelId); if (model == null) return false; Map form = JsonUtil.getJsonToBean(model.getFormData(), Map.class); return hasEnabledField(form.get("fields")); } private boolean hasEnabledField(Object value) { if (!(value instanceof List)) return false; for (Object item : (List) value) { if (!(item instanceof Map)) continue; Map field = (Map) item; if (!(field.get("__config__") instanceof Map)) continue; Map config = (Map) field.get("__config__"); if ("uploadFile".equals(config.get("jnpfKey")) && Boolean.TRUE.equals(field.get("onlineEdit"))) return true; if (hasEnabledField(config.get("children"))) return true; } return false; } private void checkFormPermission(UploadEditContext context, ColumnDataModel columns) { if (columns != null && Boolean.TRUE.equals(columns.getUseFormPermission())) { Object granted = PermissionInterfaceImpl.getFormMap().get(context.getMenuId()); if (granted == null || JsonUtil.getJsonToList(granted, ModuleFormModel.class).stream() .noneMatch(item -> permissionField(context).equals(item.getEnCode()))) { throw new DataException("没有附件字段编辑权限"); } } } private String checkFlow(UploadEditContext context, String ownerId, TaskEntity task, FilePermissionView permissions) { if (permissions.isEnabled()) { if (!Objects.equals(ownerId, UserProvider.getLoginUserId())) throw new DataException("编辑人员与当前登录人不匹配"); if (!(permissions.allows(permissionField(context), Action.EDIT) || permissions.allows(permissionField(context), Action.REVIEW)) || !permissions.allows(permissionField(context), Action.VIEW)) { throw new DataException("当前流程不允许在线编辑该附件,或文件权限暂不可用"); } context.setTaskId(task.getId()); return permissions.allows(permissionField(context), Action.EDIT) ? "edit" : "review"; } if (task == null || !Objects.equals(ownerId, task.getCreatorUserId()) || !(Objects.equals(TaskStatusEnum.TO_BE_SUBMIT.getCode(), task.getStatus()) || Objects.equals(TaskStatusEnum.BACKED.getCode(), task.getStatus()))) { throw new DataException("流程审批中不能修改附件,仅起草人在提交前或退回后可编辑"); } List nodes = workFlowApi.getNodesByFlowId(task.getFlowId()); TemplateNodeEntity start = nodes == null ? null : nodes.stream() .filter(node -> NodeEnum.START.getType().equals(node.getNodeType()) && context.getModelId().equals(node.getFormId())).findFirst().orElse(null); if (start == null) throw new DataException("附件表单与流程起草节点不匹配"); NodeModel node = JsonUtil.getJsonToBean(start.getNodeJson(), NodeModel.class); if (node.getFormOperates() != null && node.getFormOperates().stream().anyMatch(item -> permissionField(context).equals(item.get("id")) && !Boolean.TRUE.equals(item.get("write")))) throw new DataException("流程配置不允许编辑该附件字段"); return "edit"; } private FilePermissionView currentPermissions(String taskId, UploadEditContext context, VisualdevEntity model) { return currentPermissions(taskId, context, model, false); } private FilePermissionView currentPermissions(String taskId, UploadEditContext context, VisualdevEntity model, boolean reuseModel) { String formId = context.getModelId(); FilePermissionView view = reuseModel ? filePermissions.currentWithPublishedForm(taskId, formId, model.getFormData()) : filePermissions.current(taskId, formId); if (FilePermissionNativeDraft.taskAbsent(view, taskId, formId) && !StringUtils.hasText(context.getTaskId())) { Map persisted = formDataUtil.getObject().getFilePermissionDataInfo(model, context.getRecordId(), OnlineInfoModel.builder().menuId(context.getMenuId()).build()); if (FilePermissionNativeDraft.matches(persisted, context.getRecordId(), taskId)) { return FilePermissionView.legacy(taskId, formId); } } if (view == null || !Objects.equals(taskId, view.getTaskId()) || !Objects.equals(formId, view.getFormId()) || !"ready".equals(view.getStatus())) { throw new DataException("无法取得当前流程文件权限"); } return view; } public static Object fieldValue(Map data, UploadEditContext context) { if (!StringUtils.hasText(context.getParentField())) return data.get(context.getField()); if (!StringUtils.hasText(context.getRowId())) throw new DataException("子表行尚无跟踪 ID"); Object rows = data.get(context.getParentField()); if (rows instanceof List) { for (Object row : (List) rows) { Map map = row instanceof Map ? (Map) row : Collections.emptyMap(); if (context.getRowId().equals(rowId(map))) { return map.get(context.getField()); } } } throw new DataException("附件所属子表行不存在"); } @SuppressWarnings("unchecked") private static Map findField(Object fields, String field, String parent) { if (!(fields instanceof List)) return null; for (Object item : (List) fields) { if (!(item instanceof Map)) continue; Map map = (Map) item; if (!(map.get("__config__") instanceof Map)) continue; Map config = (Map) map.get("__config__"); if (StringUtils.hasText(parent)) { if (parent.equals(map.get("__vModel__"))) return findField(config.get("children"), field, null); } else if (field.equals(map.get("__vModel__"))) { return (Map) map; } if (!"table".equals(config.get("jnpfKey"))) { Map found = findField(config.get("children"), field, parent); if (found != null) return found; } } return null; } public static String rowId(Map row) { for (Map.Entry entry : row.entrySet()) { if ("id".equalsIgnoreCase(String.valueOf(entry.getKey())) || "f_id".equalsIgnoreCase(String.valueOf(entry.getKey()))) { return string(entry.getValue()); } } return ""; } private static String permissionField(UploadEditContext context) { return StringUtils.hasText(context.getParentField()) ? context.getParentField() + "-" + context.getField() : context.getField(); } private static String string(Object value) { return value == null ? "" : String.valueOf(value); } }