刘光辉
7 小时以前 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
package jnpf.elnService.formula.impl;
 
import jnpf.elnEntity.ElnDocumentFileVo;
import jnpf.elnEntity.field.ElnFieldVersionEntity;
import jnpf.elnEntity.formula.ElnFormulaDefEntity;
import jnpf.elnEntity.formula.ElnFormulaParameterSlotEntity;
import jnpf.elnEntity.formula.ElnFormulaVersionEntity;
import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity;
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
import jnpf.elnEntity.template.ElnTemplateVos;
import jnpf.elnMapper.ElnFieldVersionMapper;
import jnpf.elnMapper.ElnFormulaDefMapper;
import jnpf.elnMapper.ElnFormulaParameterSlotMapper;
import jnpf.elnMapper.ElnFormulaVersionMapper;
import jnpf.elnMapper.ElnTemplateBindingMapper;
import jnpf.elnMapper.ElnTemplateFormulaBindingMapper;
import jnpf.elnService.formula.FormulaDependencyGraph;
import jnpf.elnService.template.ElnTemplateDocumentEvidence;
import jnpf.elnService.template.ElnTemplatePublishValidator;
import jnpf.elnService.template.docx.DocxControlSnapshot;
import jnpf.exception.DataException;
import jnpf.util.JsonUtil;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
 
@Service
public class FormulaTemplatePublishValidator implements ElnTemplatePublishValidator {
    private final Store store;
    private final ElnTemplateDocumentEvidence evidence;
    private final FormulaDependencyGraph graph;
 
    @Autowired
    public FormulaTemplatePublishValidator(ElnTemplateFormulaBindingMapper formulas,
            ElnTemplateBindingMapper fields, ElnFieldVersionMapper fieldVersions,
            ElnFormulaVersionMapper formulaVersions, ElnFormulaDefMapper definitions,
            ElnFormulaParameterSlotMapper slots,
            ObjectProvider<ElnTemplateDocumentEvidence> evidenceProvider) {
        this(new PublishMyBatisStore(formulas, fields,
                fieldVersions, formulaVersions, definitions, slots),
                requiredEvidence(evidenceProvider), new FormulaDependencyGraph());
    }
 
    public FormulaTemplatePublishValidator(Store store, ElnTemplateDocumentEvidence evidence,
            FormulaDependencyGraph graph) {
        this.store=store;this.evidence=evidence;this.graph=graph;
    }
 
    @Override public List<ElnTemplateVos.Diagnostic> validate(String tenantId, String templateId) {
        throw new DataException("公式发布校验必须提供 DOCX 附件");
    }
 
    @Override
    public List<ElnTemplateVos.Diagnostic> validate(String tenantId, String templateId,
            String attachments) {
        List<ElnTemplateVos.Diagnostic> result = new ArrayList<>();
        List<ElnTemplateFormulaBindingEntity> all = store.listFormulaBindings(tenantId, templateId);
        List<ElnTemplateFormulaBindingEntity> active = new ArrayList<>();
        Set<String> activeResultIds = new LinkedHashSet<>();
        Set<String> liveResultIds = new LinkedHashSet<>();
        Set<String> writtenResults = new HashSet<>();
        for (ElnTemplateFormulaBindingEntity formula : all) {
            if (!"ACTIVE".equals(formula.getBangdingZhuangtai())) {
                if ("PENDING".equals(formula.getBangdingZhuangtai())) {
                    liveResultIds.add(formula.getJieguoBangdingId());
                    result.add(diagnostic("PENDING_FORMULA_BINDING",
                            "公式映射仍待插件确认", formula.getId()));
                }
                continue;
            }
            active.add(formula);
            activeResultIds.add(formula.getJieguoBangdingId());
            liveResultIds.add(formula.getJieguoBangdingId());
            if (!writtenResults.add(formula.getJieguoBangdingId())) result.add(diagnostic(
                    "FORMULA_RESULT_NOT_UNIQUE", "结果字段被多个公式写入", formula.getJieguoBangdingId()));
            Set<String> foreignBindingIds = validateBindingOwnership(
                    tenantId, templateId, formula, result);
            ElnFormulaDefEntity definition = store.findFormulaDefinition(
                    tenantId, formula.getGongshiDingyiId());
            ElnFormulaVersionEntity version = store.findFormulaVersion(
                    tenantId, formula.getGongshiBanbenId());
            if (definition == null || !"enabled".equals(definition.getBizEnabled())) result.add(
                    diagnostic("FORMULA_DISABLED", "模板引用的公式已停用", formula.getId()));
            if (version == null || !"PUBLISHED".equals(version.getBizStatus())) result.add(
                    diagnostic("FORMULA_VERSION_NOT_PUBLISHED", "模板公式版本尚未发布", formula.getId()));
            if (definition != null && version != null
                    && (!Objects.equals(formula.getGongshiDingyiId(), definition.getId())
                    || !Objects.equals(formula.getGongshiDingyiId(), version.getGongshiDingyiId())))
                result.add(diagnostic("FORMULA_DEFINITION_VERSION_MISMATCH",
                        "公式定义与版本归属不一致", formula.getId()));
            if (version != null) validateMapping(
                    tenantId, formula, version, foreignBindingIds, result);
        }
        FormulaDependencyGraph.Result graphResult = graph.validate(active);
        if (!graphResult.isValid()) result.add(diagnostic("FORMULA_DEPENDENCY_CYCLE",
                "公式依赖存在循环: " + graphResult.getCyclePath(), null));
        validateLocks(tenantId, templateId, attachments, activeResultIds, liveResultIds, result);
        return result;
    }
 
    private Set<String> validateBindingOwnership(String tenantId, String templateId,
            ElnTemplateFormulaBindingEntity formula,
            List<ElnTemplateVos.Diagnostic> diagnostics) {
        Set<String> foreignBindingIds = new HashSet<>();
        Map<String, String> parameterMappings = formula.getCanshuYingshe();
        for (String bindingId : parameterMappings == null
                ? Collections.<String>emptyList() : parameterMappings.values()) {
            ElnTemplateBindingEntity field = store.findFieldBinding(tenantId, bindingId);
            if (field != null && !templateId.equals(field.getMubanId())
                    && foreignBindingIds.add(field.getId()))
                diagnostics.add(diagnostic("FORMULA_BINDING_TEMPLATE_MISMATCH",
                        "公式参数字段不属于当前模板", field.getId()));
        }
        ElnTemplateBindingEntity output = store.findFieldBinding(
                tenantId, formula.getJieguoBangdingId());
        if (output != null && !templateId.equals(output.getMubanId())
                && foreignBindingIds.add(output.getId()))
            diagnostics.add(diagnostic("FORMULA_BINDING_TEMPLATE_MISMATCH",
                    "公式结果字段不属于当前模板", output.getId()));
        return foreignBindingIds;
    }
 
    private void validateMapping(String tenantId,
            ElnTemplateFormulaBindingEntity formula, ElnFormulaVersionEntity version,
            Set<String> foreignBindingIds,
            List<ElnTemplateVos.Diagnostic> diagnostics) {
        List<ElnFormulaParameterSlotEntity> slots = store.listSlots(tenantId, version.getId());
        Set<String> codes = new HashSet<>();
        for (ElnFormulaParameterSlotEntity slot : slots) codes.add(slot.getCanshuBianma());
        Map<String, String> parameterMappings = formula.getCanshuYingshe() == null
                ? Collections.emptyMap() : formula.getCanshuYingshe();
        if (!codes.equals(parameterMappings.keySet())) diagnostics.add(diagnostic(
                "FORMULA_PARAMETER_MAPPING_INCOMPLETE", "公式参数映射不完整", formula.getId()));
        for (ElnFormulaParameterSlotEntity slot : slots) {
            ElnTemplateBindingEntity field = store.findFieldBinding(
                    tenantId, parameterMappings.get(slot.getCanshuBianma()));
            if (field != null && foreignBindingIds.contains(field.getId())) continue;
            ElnFieldVersionEntity fieldVersion = field == null ? null
                    : store.findFieldVersion(tenantId, field.getZiduanBanbenId());
            if (!activePrimary(field) || fieldVersion == null
                    || !FormulaFieldBindingDependencyValidator.compatible(
                            slot.getShujuLeixing(), fieldVersion.getShujuLeixing()))
                diagnostics.add(diagnostic("FORMULA_FIELD_TYPE_MISMATCH",
                        "公式参数字段无效或类型不兼容", field == null ? null : field.getId()));
        }
        ElnTemplateBindingEntity output = store.findFieldBinding(tenantId, formula.getJieguoBangdingId());
        if (output != null && foreignBindingIds.contains(output.getId())) return;
        ElnFieldVersionEntity outputVersion = output == null ? null
                : store.findFieldVersion(tenantId, output.getZiduanBanbenId());
        if (!activePrimary(output) || outputVersion == null
                || !FormulaFieldBindingDependencyValidator.compatible(
                        version.getJieguoLeixing(), outputVersion.getShujuLeixing()))
            diagnostics.add(diagnostic("FORMULA_FIELD_TYPE_MISMATCH",
                    "公式结果字段无效或类型不兼容", formula.getJieguoBangdingId()));
        if (outputVersion != null && !validFormulaResultRules(outputVersion))
            diagnostics.add(diagnostic("FORMULA_RESULT_SOURCE_INVALID",
                    "公式结果字段必须为 NONE/ELN_RECORD/RECORD_PRIVATE",
                    formula.getJieguoBangdingId()));
    }
 
    @SuppressWarnings("unchecked")
    private static boolean validFormulaResultRules(ElnFieldVersionEntity version) {
        if (!"RECORD_PRIVATE".equals(version.getPiciMoshi())
                || version.getBangdingPeizhi() == null) return false;
        Map<String, Object> config = JsonUtil.getJsonToBean(version.getBangdingPeizhi(), Map.class);
        Map<String, Object> read = config == null ? null : (Map<String, Object>) config.get("read");
        Map<String, Object> write = config == null ? null : (Map<String, Object>) config.get("write");
        return read != null && write != null && "NONE".equals(read.get("source"))
                && "ELN_RECORD".equals(write.get("target"));
    }
 
    private void validateLocks(String tenantId, String templateId, String attachments,
            Set<String> activeResultIds, Set<String> liveResultIds,
            List<ElnTemplateVos.Diagnostic> diagnostics) {
        Map<String, ElnTemplateBindingEntity> fieldsByTag = new HashMap<>();
        Set<String> expectedLockedTags = new HashSet<>();
        Set<String> allowedLockedTags = new HashSet<>();
        for (ElnTemplateBindingEntity field : store.listFieldBindings(tenantId, templateId)) {
            if (!"ACTIVE".equals(field.getBangdingZhuangtai())
                    && !"PENDING".equals(field.getBangdingZhuangtai())) continue;
            fieldsByTag.put(field.getKongjianBiaoqian(), field);
            if (activeResultIds.contains(field.getId())
                    || activeResultIds.contains(field.getYinyongShiliId()))
                expectedLockedTags.add(field.getKongjianBiaoqian());
            if (liveResultIds.contains(field.getId())
                    || liveResultIds.contains(field.getYinyongShiliId()))
                allowedLockedTags.add(field.getKongjianBiaoqian());
        }
        Map<String, List<DocxControlSnapshot>> controls = new HashMap<>();
        for (DocxControlSnapshot control : evidence.readControls(tenantId, docxFileId(attachments)))
            controls.computeIfAbsent(control.getTag(), key -> new ArrayList<>()).add(control);
        for (String tag : expectedLockedTags) {
            List<DocxControlSnapshot> matches = controls.get(tag);
            if (matches == null || matches.size() != 1
                    || !"sdtContentLocked".equals(matches.get(0).getLock()))
                diagnostics.add(diagnostic("FORMULA_RESULT_LOCK_MISMATCH",
                        "公式结果主控件和引用必须只读", fieldsByTag.get(tag).getId()));
        }
        for (Map.Entry<String, List<DocxControlSnapshot>> item : controls.entrySet()) {
            if (fieldsByTag.containsKey(item.getKey())
                    && !allowedLockedTags.contains(item.getKey())
                    && item.getValue().stream().anyMatch(c -> "sdtContentLocked".equals(c.getLock())))
                diagnostics.add(diagnostic("FORMULA_RESULT_LOCK_MISMATCH",
                        "无公式映射字段残留公式只读锁", fieldsByTag.containsKey(item.getKey())
                                ? fieldsByTag.get(item.getKey()).getId() : null));
        }
    }
 
    private static boolean activePrimary(ElnTemplateBindingEntity value) {
        return value != null && "ACTIVE".equals(value.getBangdingZhuangtai())
                && "PRIMARY".equals(value.getKongjianJuese());
    }
    private static String docxFileId(String attachments) {
        List<ElnDocumentFileVo> files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class);
        if (files == null) throw new DataException("模板附件无效");
        List<String> docx = new ArrayList<>();
        for (ElnDocumentFileVo file : files) if (file != null && file.getFileId() != null
                && ("docx".equalsIgnoreCase(file.getFileExtension())
                    || file.getFileId().toLowerCase().endsWith(".docx"))) docx.add(file.getFileId());
        if (docx.size() != 1) throw new DataException("模板必须且只能包含一个 DOCX 附件");
        return docx.get(0);
    }
    private static ElnTemplateVos.Diagnostic diagnostic(String code, String message, String id) {
        ElnTemplateVos.Diagnostic result = new ElnTemplateVos.Diagnostic();
        result.setSeverity("ERROR");result.setCode(code);result.setMessage(message);result.setBindingId(id);
        return result;
    }
    private static ElnTemplateDocumentEvidence requiredEvidence(
            ObjectProvider<ElnTemplateDocumentEvidence> provider) {
        ElnTemplateDocumentEvidence result = provider.getIfAvailable();
        return result == null ? (tenant, file) -> { throw new DataException("DOCX 控件证据服务不可用"); } : result;
    }
 
    public interface Store extends FormulaFieldBindingDependencyValidator.Store { }
 
    private static final class PublishMyBatisStore
            extends FormulaFieldBindingDependencyValidator.MyBatisStore implements Store {
        private PublishMyBatisStore(ElnTemplateFormulaBindingMapper formulas,
                ElnTemplateBindingMapper fields, ElnFieldVersionMapper fieldVersions,
                ElnFormulaVersionMapper formulaVersions, ElnFormulaDefMapper definitions,
                ElnFormulaParameterSlotMapper slots) {
            super(formulas, fields, fieldVersions, formulaVersions, definitions, slots);
        }
    }
}