刘光辉
4 小时以前 cf11e63b659afcc9792ab3e395f1b45d844431a8
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
import { cloneDeep } from 'lodash-es';
 
export const fileActions = ['VIEW', 'DOWNLOAD', 'EDIT', 'REVIEW', 'ADD', 'REPLACE', 'REMOVE'] as const;
export type FileAction = (typeof fileActions)[number];
export type FileEffect = 'ALLOW' | 'DENY' | 'INHERIT';
export type FileActions = Partial<Record<FileAction, FileEffect>>;
 
export interface FilePermissionRule {
  id: string;
  formId: string;
  field: string;
  subject: 'CREATOR' | 'CURRENT_HANDLERS' | 'NODE_PARTICIPANTS' | 'NON_CURRENT_HANDLERS' | 'PARTICIPANTS';
  subjectNodeId?: string;
  trigger: 'ACTIVE' | 'CANCELLED' | 'COMPLETED' | 'ENTERED' | 'TERMINATED';
  triggerNodeId?: string;
  onReturn?: 'KEEP' | 'RELEASE';
  actions: FileActions;
}
 
export interface FilePermissionPolicy {
  version: 1;
  rules: FilePermissionRule[];
}
 
/** 仅同一表单同一附件字段保留;不要把旧授权复制到另一个表单的同名字段。 */
export function retainFilePermissions(previous: any, field: any, isSameForm: boolean): { filePermissions?: FileActions } {
  if (!isSameForm || field.__config__?.jnpfKey !== 'uploadFile' || previous?.jnpfKey !== 'uploadFile' || !Object.hasOwn(previous, 'filePermissions')) return {};
  // 保留未知值交由校验报告,不能静默删除损坏的 DENY 配置。
  return { filePermissions: cloneDeep(previous.filePermissions) };
}
 
/** 校验对象来自该流程版本的表单字段和节点,不能以显示名称匹配。 */
export function validateFilePermissionPolicy(policy: FilePermissionPolicy, fieldsByForm: Record<string, Set<string>>, nodeIds: Set<string>): string[] {
  if (!policy || policy.version !== 1 || !Array.isArray(policy.rules)) return ['文件权限配置版本或结构无效'];
  const errors: string[] = [];
  const ids = new Set<string>();
  for (const rule of policy.rules) {
    if (!rule || typeof rule.id !== 'string' || !rule.id.trim()) {
      errors.push('文件规则缺少 ID');
      continue;
    }
    if (ids.has(rule.id)) errors.push(`文件规则 ID 重复:${rule.id}`);
    ids.add(rule.id);
    if (!Object.hasOwn(fieldsByForm, rule.formId) || !fieldsByForm[rule.formId]?.has(rule.field)) errors.push(`附件字段不存在:${rule.id}`);
    if (!['CREATOR', 'CURRENT_HANDLERS', 'NODE_PARTICIPANTS', 'NON_CURRENT_HANDLERS', 'PARTICIPANTS'].includes(rule.subject))
      errors.push(`作用人员无效:${rule.id}`);
    if (!['ACTIVE', 'CANCELLED', 'COMPLETED', 'ENTERED', 'TERMINATED'].includes(rule.trigger)) errors.push(`生效条件无效:${rule.id}`);
    if (rule.subject === 'NODE_PARTICIPANTS' && !nodeIds.has(rule.subjectNodeId || '')) errors.push(`参与人节点不存在:${rule.id}`);
    if (['ACTIVE', 'ENTERED'].includes(rule.trigger) && !nodeIds.has(rule.triggerNodeId || '')) errors.push(`触发节点不存在:${rule.id}`);
    if (rule.trigger === 'ENTERED' && !['KEEP', 'RELEASE'].includes(rule.onReturn || '')) errors.push(`请选择退回后保留或解除规则:${rule.id}`);
    if (!rule.actions || Array.isArray(rule.actions) || typeof rule.actions !== 'object' || !Object.keys(rule.actions).length) {
      errors.push(`文件动作为空或无效:${rule.id}`);
    } else {
      for (const [action, effect] of Object.entries(rule.actions)) {
        if (!fileActions.includes(action as FileAction) || !['ALLOW', 'DENY', 'INHERIT'].includes(effect)) errors.push(`文件动作无效:${rule.id}`);
      }
    }
  }
  return errors;
}
 
export interface FilePermissionDesignerContext {
  attachments: { field: string; formId: string; label: string; value: string }[];
  nodes: { label: string; value: string }[];
  fieldsByForm: Record<string, Set<string>>;
  nodeIds: Set<string>;
}
 
/** 只使用当前流程实际存在的人工节点与原生表单字段缓存,不采纳已删除节点/其他表单的缓存。 */
export function filePermissionDesignerContext(data: Record<string, any>, liveNodeIds: Set<string>): FilePermissionDesignerContext {
  const nodes = Object.entries(data).filter(([id, node]) => liveNodeIds.has(id) && ['approver', 'processing', 'start'].includes(node?.type));
  const startFormId = nodes.find(([, node]) => node.type === 'start')?.[1]?.formId;
  const formNames = new Map<string, string>();
  for (const [, node] of nodes) {
    const formId = node.formId || startFormId;
    if (formId) formNames.set(formId, node.formName || formNames.get(formId) || formId);
  }
  const attachments: FilePermissionDesignerContext['attachments'] = [];
  const fieldsByForm: Record<string, Set<string>> = Object.create(null);
  for (const [formId, name] of formNames) {
    fieldsByForm[formId] = new Set();
    const fields = data.global?.allFormMap?.[`form_${formId}`];
    if (!Array.isArray(fields)) continue;
    for (const field of fields) {
      if (field?.__config__?.jnpfKey !== 'uploadFile' || typeof field.id !== 'string' || !field.id || fieldsByForm[formId]!.has(field.id)) continue;
      fieldsByForm[formId]!.add(field.id);
      attachments.push({ value: JSON.stringify([formId, field.id]), formId, field: field.id, label: `${name} / ${field.fullName || field.id}` });
    }
  }
  return {
    attachments,
    fieldsByForm,
    nodes: nodes.map(([id, node]) => ({ value: id, label: node.nodeName || id })),
    nodeIds: new Set(nodes.map(([id]) => id)),
  };
}
 
/** 编辑器可展示的结构;未知或损坏配置保留原值,不能打开后自动覆盖为无规则。 */
export function isEditableFilePermissionPolicy(value: unknown): value is FilePermissionPolicy {
  const policy = value as FilePermissionPolicy;
  return (
    !!policy &&
    policy.version === 1 &&
    Array.isArray(policy.rules) &&
    policy.rules.every(
      (rule) =>
        !!rule &&
        typeof rule === 'object' &&
        !Array.isArray(rule) &&
        typeof rule.id === 'string' &&
        !!rule.actions &&
        typeof rule.actions === 'object' &&
        !Array.isArray(rule.actions),
    )
  );
}
 
export function validateDesignerFilePermissions(data: Record<string, any>, liveNodeIds: Set<string>, flowType: number): string[] {
  const policies = Object.entries(data).filter(([id, node]) => (id === 'global' || liveNodeIds.has(id)) && node?.filePermissionPolicy != null);
  if (!policies.length) return [];
  if (![0, 1].includes(flowType)) return ['当前流程类型暂不支持文件权限规则'];
  const context = filePermissionDesignerContext(data, liveNodeIds);
  const ids = new Set<string>();
  const errors: string[] = [];
  for (const [, node] of policies) {
    const policy = node.filePermissionPolicy;
    errors.push(...validateFilePermissionPolicy(policy, context.fieldsByForm, context.nodeIds));
    if (!isEditableFilePermissionPolicy(policy)) continue;
    for (const rule of policy.rules) {
      if (rule.id.startsWith('@field/')) errors.push('文件规则使用了保留标识');
      if (ids.has(rule.id)) errors.push(`文件规则 ID 重复:${rule.id}`);
      ids.add(rule.id);
    }
  }
  return errors;
}