刘光辉
8 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
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); }
}