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> visualdevService;
|
private final ObjectProvider<FlowFormDataUtil> 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<Map<String, Boolean>> 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<Map<String, Object>> baseline(UploadEditContext context, String ownerId) {
|
return checkedBaseline(context, ownerId).getFiles();
|
}
|
/** 将当前准入结果与真实记录上下文一起交给内部编辑租约,避免再次猜测是否受控。 */
|
@lombok.Value
|
public static class CheckedBaseline {
|
List<Map<String, Object>> files;
|
boolean controlled;
|
String mode;
|
boolean dmsAudit;
|
FilePermissionView filePermissionView;
|
public CheckedBaseline(List<Map<String, Object>> files, boolean controlled) { this(files, controlled, "edit"); }
|
public CheckedBaseline(List<Map<String, Object>> files, boolean controlled, String mode) {
|
this(files, controlled, mode, false);
|
}
|
public CheckedBaseline(List<Map<String, Object>> files, boolean controlled, String mode, boolean dmsAudit) {
|
this(files, controlled, mode, dmsAudit, null);
|
}
|
public CheckedBaseline(List<Map<String, Object>> 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<CheckedBaseline> sharedBaselines(List<UploadEditContext> 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<String, Object> form;
|
final Map<String, TaskEntity> tasks = new java.util.HashMap<>();
|
final Map<String, FilePermissionView> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<ModuleButtonEntity> 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<String, Object> form = JsonUtil.getJsonToBean(model.getFormData(), Map.class);
|
Map<String, Object> 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<String, Object> 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<String, Object> form = JsonUtil.getJsonToBean(model.getFormData(), Map.class);
|
Map<String, Object> 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<TemplateNodeEntity> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object>) map;
|
}
|
if (!"table".equals(config.get("jnpfKey"))) {
|
Map<String, Object> 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); }
|
}
|