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 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 validate(String tenantId, String templateId) { throw new DataException("公式发布校验必须提供 DOCX 附件"); } @Override public List validate(String tenantId, String templateId, String attachments) { List result = new ArrayList<>(); List all = store.listFormulaBindings(tenantId, templateId); List active = new ArrayList<>(); Set activeResultIds = new LinkedHashSet<>(); Set liveResultIds = new LinkedHashSet<>(); Set 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 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 validateBindingOwnership(String tenantId, String templateId, ElnTemplateFormulaBindingEntity formula, List diagnostics) { Set foreignBindingIds = new HashSet<>(); Map parameterMappings = formula.getCanshuYingshe(); for (String bindingId : parameterMappings == null ? Collections.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 foreignBindingIds, List diagnostics) { List slots = store.listSlots(tenantId, version.getId()); Set codes = new HashSet<>(); for (ElnFormulaParameterSlotEntity slot : slots) codes.add(slot.getCanshuBianma()); Map 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 config = JsonUtil.getJsonToBean(version.getBangdingPeizhi(), Map.class); Map read = config == null ? null : (Map) config.get("read"); Map write = config == null ? null : (Map) 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 activeResultIds, Set liveResultIds, List diagnostics) { Map fieldsByTag = new HashMap<>(); Set expectedLockedTags = new HashSet<>(); Set 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> 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 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> 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 files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); if (files == null) throw new DataException("模板附件无效"); List 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 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); } } }