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);
|
}
|
}
|
}
|