package jnpf.elnService.template.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.base.UserInfo; import jnpf.database.util.TenantDataSourceUtil; import jnpf.elnEntity.ElnDocumentFileVo; import jnpf.elnEntity.field.ElnFieldDefEntity; import jnpf.elnEntity.field.ElnFieldEnums; import jnpf.elnEntity.field.ElnFieldForms; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateVersionEntity; import jnpf.elnEntity.template.ElnTemplateVos; import jnpf.elnMapper.ElnTemplateMapper; import jnpf.elnMapper.ElnFieldDefMapper; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnService.template.ElnTemplateDocumentEvidence; import jnpf.elnService.template.ElnTemplateBindingDependencyValidator; import jnpf.elnService.template.ElnTemplateHealthService; import jnpf.elnService.template.ElnPendingBindingCleanupService; import jnpf.elnService.template.ElnTemplateRepeatGroupService; import jnpf.elnService.template.ElnTemplateCheckboxGroupService; import jnpf.elnService.cross.impl.ElnCrossTemplatePublishValidator; import jnpf.elnService.template.docx.DocxControlConfigMatcher; import jnpf.elnService.template.docx.DocxExpectedControlConfig; import jnpf.elnService.template.docx.DocxControlSnapshot; import jnpf.exception.DataException; import jnpf.model.tenant.TenantVO; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; import java.util.stream.Collectors; @Service public class ElnTemplateHealthServiceImpl implements ElnTemplateHealthService { private static final String ACTIVE = "ACTIVE"; private static final String PENDING = "PENDING"; private static final String PUBLISHED = "PUBLISHED"; private static final String PRE_RELEASE = "PRE_RELEASE"; private static final String ENABLED = "enabled"; private final Store store; private final ElnTemplateDocumentEvidence evidence; private final PendingReconciler pendingReconciler; private final Supplier tenantSupplier; private final List dependencyValidators; private final RepeatGroupReader repeatGroupReader; private final CheckboxGroupReader checkboxGroupReader; private final ElnCrossTemplatePublishValidator crossTemplateValidator; @Autowired public ElnTemplateHealthServiceImpl(ElnTemplateMapper templateMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldDefMapper fieldMapper, ElnFieldVersionMapper fieldVersionMapper, ObjectProvider evidenceProvider, List dependencyValidators, ElnCrossTemplatePublishValidator crossTemplateValidator, ElnPendingBindingCleanupService pendingCleanupService, ElnTemplateRepeatGroupService repeatGroupService, ElnTemplateCheckboxGroupService checkboxGroupService) { this(new MyBatisStore(templateMapper, bindingMapper, fieldMapper, fieldVersionMapper), evidence(evidenceProvider), pendingCleanupService::reconcileExpired, ElnTemplateHealthServiceImpl::currentTenantId, dependencyValidators, repeatGroupService::list, checkboxGroupService::list, crossTemplateValidator); } ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence, PendingReconciler pendingReconciler, Supplier tenantSupplier, List dependencyValidators, RepeatGroupReader repeatGroupReader) { this(store, evidence, pendingReconciler, tenantSupplier, dependencyValidators, repeatGroupReader, (tenantId, templateId) -> Collections.emptyList(), null); } ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence, PendingReconciler pendingReconciler, Supplier tenantSupplier, List dependencyValidators, RepeatGroupReader repeatGroupReader, ElnCrossTemplatePublishValidator crossTemplateValidator) { this(store, evidence, pendingReconciler, tenantSupplier, dependencyValidators, repeatGroupReader, (tenantId, templateId) -> Collections.emptyList(), crossTemplateValidator); } ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence, PendingReconciler pendingReconciler, Supplier tenantSupplier, List dependencyValidators, RepeatGroupReader repeatGroupReader, CheckboxGroupReader checkboxGroupReader) { this(store, evidence, pendingReconciler, tenantSupplier, dependencyValidators, repeatGroupReader, checkboxGroupReader, null); } ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence, PendingReconciler pendingReconciler, Supplier tenantSupplier, List dependencyValidators, RepeatGroupReader repeatGroupReader, CheckboxGroupReader checkboxGroupReader, ElnCrossTemplatePublishValidator crossTemplateValidator) { this.store = store; this.evidence = evidence; this.pendingReconciler = pendingReconciler; this.tenantSupplier = tenantSupplier; this.dependencyValidators = dependencyValidators == null ? new ArrayList<>() : new ArrayList<>(dependencyValidators); this.repeatGroupReader = repeatGroupReader; this.checkboxGroupReader = checkboxGroupReader; this.crossTemplateValidator = crossTemplateValidator; } @Override public HealthReport inspect(String templateId) { return inspect(templateId, null); } @Override public HealthReport inspect(String templateId, String attachments) { String tenantId = tenantSupplier.get(); if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(templateId)) { throw new DataException("当前租户或模板 ID 无效"); } tenantId = tenantId.trim(); templateId = templateId.trim(); ElnTemplateVersionEntity template = store.findTemplate(tenantId, templateId); if (template == null) { throw new DataException("模板不存在或不属于当前租户"); } String fileId = uniqueDocxFileId(StringUtils.hasText(attachments) ? attachments : template.getMubanFile()); List controls = evidence.readControls(tenantId, fileId); if (controls == null) { throw new DataException("DOCX 控件证据无效"); } boolean publishedTemplate = PUBLISHED.equals(template.getBizStatus()); List pendingDiagnostics = publishedTemplate ? Collections.emptyList() : pendingReconciler.reconcile(tenantId, templateId, new Date(), controls); List bindings = store.listBindings(tenantId, templateId); HealthReport result = new HealthReport(); result.setTemplateId(templateId); result.setFileId(fileId); List diagnostics = inspectBindings( tenantId, bindings, controls, pendingDiagnostics, repeatGroupReader.list(tenantId, templateId), checkboxGroupReader.list(tenantId, templateId), publishedTemplate); appendRequiredTestResultDiagnostics(diagnostics, bindings); appendCrossTemplateDiagnostics(diagnostics, tenantId, templateId); diagnostics.sort(Comparator.comparing(HealthDiagnostic::getCode) .thenComparing(item -> value(item.getControlTag())) .thenComparing(item -> value(item.getBindingId()))); if (publishedTemplate) { retainLocateActions(diagnostics); } result.setDiagnostics(diagnostics); return result; } private static void appendRequiredTestResultDiagnostics( List diagnostics, List bindings) { for (ElnTemplateVos.Diagnostic source : ElnTestResultTemplatePublishValidator.validateBindings(bindings)) { HealthDiagnostic target = new HealthDiagnostic(); target.setSeverity(source.getSeverity()); target.setCode(source.getCode()); target.setMessage(source.getMessage()); diagnostics.add(target); } } private void appendCrossTemplateDiagnostics(List diagnostics, String tenantId, String templateId) { if (crossTemplateValidator == null) return; for (ElnTemplateVos.Diagnostic source : crossTemplateValidator.validate(tenantId, templateId)) { HealthDiagnostic target = new HealthDiagnostic(); target.setSeverity(source.getSeverity()); target.setCode(source.getCode()); target.setMessage(source.getMessage()); target.setBindingId(source.getBindingId()); target.setControlTag(source.getControlTag()); diagnostics.add(target); } } private static void retainLocateActions(List diagnostics) { for (HealthDiagnostic diagnostic : diagnostics) { diagnostic.setActions(diagnostic.getActions().contains("LOCATE") ? Collections.singletonList("LOCATE") : Collections.emptyList()); } } private List inspectBindings(String tenantId, List bindings, List controls, List pendingDiagnostics, List repeatGroups, List checkboxGroups, boolean publishedTemplate) { List result = new ArrayList<>(); Map> controlsByTag = controlsByTag(controls); Map activeById = new LinkedHashMap<>(); Map activeByTag = new LinkedHashMap<>(); Map liveByTag = new LinkedHashMap<>(); Set liveTags = new LinkedHashSet<>(); for (ElnTemplateBindingEntity binding : safe(bindings)) { if (binding == null) { continue; } if (ACTIVE.equals(binding.getBangdingZhuangtai())) { activeById.put(binding.getId(), binding); activeByTag.putIfAbsent(binding.getKongjianBiaoqian(), binding); liveByTag.putIfAbsent(binding.getKongjianBiaoqian(), binding); liveTags.add(binding.getKongjianBiaoqian()); } else if (PENDING.equals(binding.getBangdingZhuangtai())) { liveByTag.putIfAbsent(binding.getKongjianBiaoqian(), binding); liveTags.add(binding.getKongjianBiaoqian()); } } for (Map.Entry> item : controlsByTag.entrySet()) { List matches = item.getValue(); DocxControlSnapshot first = matches.get(0); for (DocxControlSnapshot control : matches) { if (control.isNestedControl()) { ElnTemplateBindingEntity binding = liveByTag.get(item.getKey()); result.add(diagnostic("ERROR", "NESTED_CONTENT_CONTROL", "内容控件不能嵌套在其他内容控件中", control, binding, binding == null ? null : displayName(tenantId, binding), Arrays.asList("LOCATE", "REPAIR"))); } } if (matches.size() > 1) { ElnTemplateBindingEntity binding = activeByTag.get(item.getKey()); result.add(diagnostic("ERROR", "DUPLICATE_TAG", "同一控件 Tag 在 DOCX 中出现多次", first, binding, binding == null ? null : displayName(tenantId, binding), Arrays.asList("LOCATE"))); } if (!liveTags.contains(item.getKey())) { result.add(diagnostic("ERROR", "ORPHAN_CONTROL", "DOCX 控件没有有效或待确认绑定", first, null, Arrays.asList("LOCATE"))); } } inspectRepeatStructure(result, liveByTag, controlsByTag, repeatGroups); appendCheckboxGroupDiagnostics(result, tenantId, bindings, controlsByTag, checkboxGroups, publishedTemplate); for (ElnTemplateBindingEntity binding : activeById.values()) { ElnFieldDefEntity field = store.findField(tenantId, binding.getZiduanDingyiId()); ElnFieldVersionEntity version = store.findFieldVersion( tenantId, binding.getZiduanBanbenId()); String displayName = version == null ? null : version.getZiduanMingcheng(); if (field == null || !ENABLED.equals(field.getBizEnabled())) { result.add(diagnostic("ERROR", "FIELD_DISABLED", "绑定字段不存在、跨租户或已停用", binding, displayName, Arrays.asList("REPAIR"))); } boolean versionMatches = version != null && Objects.equals(binding.getZiduanDingyiId(), version.getZiduanDingyiId()); if (!versionMatches) { result.add(diagnostic("ERROR", "FIELD_VERSION_MISMATCH", "绑定固定字段版本不存在、跨租户或不属于该字段", binding, displayName, Arrays.asList("REPAIR"))); } else if (!PUBLISHED.equals(version.getBizStatus())) { result.add(diagnostic(PRE_RELEASE.equals(version.getBizStatus()) ? "WARNING" : "ERROR", "FIELD_VERSION_NOT_PUBLISHED", "绑定固定字段版本尚未发布", binding, displayName, Arrays.asList("REPAIR"))); } List matches = controlsByTag.get(binding.getKongjianBiaoqian()); if (matches == null || matches.isEmpty()) { HealthDiagnostic missing = diagnostic("ERROR", "MISSING_CONTROL", "绑定存在但 DOCX 控件已缺失", binding, displayName, Arrays.asList("REPAIR", "CONFIRM_REMOVE")); missing.setDependencies(removalDependencies( tenantId, binding, activeById.values())); result.add(missing); continue; } if (matches.size() != 1) { continue; } DocxControlSnapshot control = matches.get(0); if (!Objects.equals(binding.getKongjianJuese(), control.getControlRole())) { result.add(diagnostic("ERROR", "ROLE_MISMATCH", "控件角色与绑定不一致", control, binding, displayName, Arrays.asList("LOCATE", "REPAIR"))); } if (!Objects.equals(binding.getKongjianLeixing(), control.getControlType())) { result.add(diagnostic("ERROR", "TYPE_MISMATCH", "控件类型与绑定不一致", control, binding, displayName, Arrays.asList("LOCATE", "REPAIR"))); } if (versionMatches && (!DocxControlConfigMatcher.matches( binding.getKongjianLeixing(), config(version), control.getControlConfig()) || (StringUtils.hasText(binding.getKongjianBieming()) && !Objects.equals(binding.getKongjianBieming(), control.getAlias())))) { result.add(diagnostic("ERROR", "CONFIG_MISMATCH", "控件配置与字段版本不一致", control, binding, displayName, Arrays.asList("LOCATE", "REPAIR"))); } if ("REFERENCE".equals(binding.getKongjianJuese())) { ElnTemplateBindingEntity primary = activeById.get(binding.getYinyongShiliId()); if (primary == null || !"PRIMARY".equals(primary.getKongjianJuese()) || !Objects.equals(primary.getZiduanDingyiId(), binding.getZiduanDingyiId()) || !Objects.equals(primary.getZiduanBanbenId(), binding.getZiduanBanbenId())) { result.add(diagnostic("ERROR", "REFERENCE_WITHOUT_PRIMARY", "引用控件没有同模板、同字段版本的有效主控件", control, binding, displayName, Arrays.asList("LOCATE", "REPAIR"))); } } } for (ElnTemplateBindingEntity binding : safe(bindings)) { if (binding != null && PENDING.equals(binding.getBangdingZhuangtai())) { DocxControlSnapshot control = first(controlsByTag.get(binding.getKongjianBiaoqian())); result.add(diagnostic("WARNING", "PENDING_BINDING", "绑定仍待插件确认", control, binding, displayName(tenantId, binding), Arrays.asList("LOCATE"))); } } appendPendingDiagnostics(result, pendingDiagnostics, bindings, controlsByTag, tenantId); return result; } private void appendCheckboxGroupDiagnostics( List diagnostics, String tenantId, List bindings, Map> controlsByTag, List configuredGroups, boolean publishedTemplate) { Map> groups = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : safe(bindings)) { if (binding == null || binding.getCheckboxGroupCode() == null || !(ACTIVE.equals(binding.getBangdingZhuangtai()) || PENDING.equals(binding.getBangdingZhuangtai()))) { continue; } groups.computeIfAbsent(binding.getCheckboxGroupCode(), key -> new ArrayList<>()) .add(binding); } Map rules = new LinkedHashMap<>(); for (ElnTemplateVos.CheckboxGroup rule : safe(configuredGroups)) { if (rule != null && StringUtils.hasText(rule.getGroupCode())) { rules.put(rule.getGroupCode(), rule); } } for (Map.Entry> entry : groups.entrySet()) { String groupCode = entry.getKey(); List members = entry.getValue(); members.sort(Comparator.comparing(item -> value(item.getId()))); if (!groupCode.matches("[A-Za-z0-9_-]{1,80}")) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_CODE_INVALID", "选择组编码格式无效", groupCode, members, controlsByTag, tenantId)); } if (members.size() < 2) { diagnostics.add(checkboxGroupDiagnostic( publishedTemplate ? "ERROR" : "WARNING", "CHECKBOX_GROUP_SINGLE_MEMBER", "选择组只有一个成员,发布前需要补充成员", groupCode, members, controlsByTag, tenantId)); } if (hasInvalidCheckboxGroupMember(tenantId, members)) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_MEMBER_INVALID", "选择组包含非复选框、只读或归属失效的成员", groupCode, members, controlsByTag, tenantId)); } if (!sameCheckboxGroupScope(members)) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_SCOPE_MISMATCH", "同一选择组必须位于同一步骤和同一普通或重复作用域", groupCode, members, controlsByTag, tenantId)); } ElnTemplateVos.CheckboxGroup rule = rules.get(groupCode); if (rule == null) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_RULE_MISSING", "选择组缺少数量规则", groupCode, members, controlsByTag, tenantId)); continue; } Integer minSelected = rule.getMinSelected(); Integer maxSelected = rule.getMaxSelected(); if (!validCheckboxGroupRange(minSelected, maxSelected)) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_RULE_INVALID", "选择组数量规则无效", groupCode, members, controlsByTag, tenantId)); continue; } if (minSelected > members.size()) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_MIN_EXCEEDS_MEMBERS", "选择组最少选择数不能大于成员数", groupCode, members, controlsByTag, tenantId)); } int defaultCount = defaultCheckedCount(tenantId, members); if (defaultCount < minSelected || (maxSelected != null && defaultCount > maxSelected)) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_DEFAULT_COUNT_OUT_OF_RANGE", "选择组默认勾选数不符合数量规则", groupCode, members, controlsByTag, tenantId)); } } for (Map.Entry entry : rules.entrySet()) { if (groups.containsKey(entry.getKey())) continue; diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_WITHOUT_MEMBER", "选择组规则没有活动成员", entry.getKey(), Collections.emptyList(), controlsByTag, tenantId)); ElnTemplateVos.CheckboxGroup rule = entry.getValue(); if (!validCheckboxGroupRange(rule.getMinSelected(), rule.getMaxSelected())) { diagnostics.add(checkboxGroupDiagnostic("ERROR", "CHECKBOX_GROUP_RULE_INVALID", "选择组数量规则无效", entry.getKey(), Collections.emptyList(), controlsByTag, tenantId)); } } } private static boolean validCheckboxGroupRange( Integer minSelected, Integer maxSelected) { return minSelected != null && minSelected >= 0 && (maxSelected == null || (maxSelected >= 1 && minSelected <= maxSelected)); } private boolean hasInvalidCheckboxGroupMember( String tenantId, List members) { for (ElnTemplateBindingEntity binding : members) { ElnFieldDefEntity field = store.findField(tenantId, binding.getZiduanDingyiId()); ElnFieldVersionEntity version = store.findFieldVersion( tenantId, binding.getZiduanBanbenId()); boolean validOwnership = field != null && ENABLED.equals(field.getBizEnabled()) && version != null && Objects.equals(binding.getZiduanDingyiId(), version.getZiduanDingyiId()); if (!validOwnership || !"CHECKBOX".equals(binding.getKongjianLeixing()) || !("PRIMARY".equals(binding.getKongjianJuese()) || "REPEAT".equals(binding.getKongjianJuese())) || readOnly(version)) { return true; } for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { List items = validator.validateCheckboxGroup( tenantId, binding.getMubanId(), binding.getId()); if (items != null && items.stream().anyMatch(item -> item != null && "ERROR".equalsIgnoreCase(item.getSeverity()))) { return true; } } } return false; } private static boolean readOnly(ElnFieldVersionEntity version) { if (version == null || !StringUtils.hasText(version.getBangdingPeizhi())) { return false; } ElnFieldForms.Binding config = JsonUtil.getJsonToBean( version.getBangdingPeizhi(), ElnFieldForms.Binding.class); String source = config == null || config.getRead() == null ? null : config.getRead().getSource(); return StringUtils.hasText(source) && !ElnFieldEnums.ReadSource.NONE.name().equals(source.trim()); } private static boolean sameCheckboxGroupScope(List members) { if (members.isEmpty()) { return true; } ElnTemplateBindingEntity first = members.get(0); for (ElnTemplateBindingEntity member : members) { if (!Objects.equals(first.getKongjianJuese(), member.getKongjianJuese())) { return false; } if ("PRIMARY".equals(first.getKongjianJuese()) && !Objects.equals(first.getStepId(), member.getStepId())) { return false; } if ("REPEAT".equals(first.getKongjianJuese()) && !Objects.equals(first.getRepeatGroupCode(), member.getRepeatGroupCode())) { return false; } } return true; } private int defaultCheckedCount( String tenantId, List members) { int count = 0; for (ElnTemplateBindingEntity member : members) { ElnFieldVersionEntity version = store.findFieldVersion( tenantId, member.getZiduanBanbenId()); Map config = version == null ? null : config(version); if (config != null && Boolean.TRUE.equals(config.get("defaultChecked"))) { count++; } } return count; } private HealthDiagnostic checkboxGroupDiagnostic( String severity, String code, String message, String groupCode, List members, Map> controlsByTag, String tenantId) { ElnTemplateBindingEntity firstBinding = members.isEmpty() ? null : members.get(0); DocxControlSnapshot firstControl = firstBinding == null ? null : first(controlsByTag.get(firstBinding.getKongjianBiaoqian())); HealthDiagnostic result = diagnostic(severity, code, message, firstControl, firstBinding, firstBinding == null ? null : displayName(tenantId, firstBinding), firstControl == null ? Collections.emptyList() : Collections.singletonList("LOCATE")); result.setCheckboxGroupCode(groupCode); result.setBindingIds(members.stream().map(ElnTemplateBindingEntity::getId) .filter(Objects::nonNull).sorted().collect(Collectors.toList())); return result; } private static void inspectRepeatStructure(List result, Map activeByTag, Map> controlsByTag, List repeatGroups) { Map configured = new LinkedHashMap<>(); for (ElnTemplateVos.RepeatGroup group : safe(repeatGroups)) { if (group != null && StringUtils.hasText(group.getGroupCode())) { configured.put(group.getGroupCode(), group); } } Map groupRows = new LinkedHashMap<>(); Map rowOwners = new LinkedHashMap<>(); for (Map.Entry> entry : controlsByTag.entrySet()) { if (!entry.getKey().startsWith("eln.repeat.")) continue; DocxControlSnapshot control = entry.getValue().get(0); ElnTemplateBindingEntity binding = activeByTag.get(entry.getKey()); RepeatTag tag = RepeatTag.parse(entry.getKey()); if (tag == null || binding == null || !"REPEAT".equals(binding.getKongjianJuese()) || !Objects.equals(tag.groupCode, binding.getRepeatGroupCode()) || !Objects.equals(tag.rowId, binding.getRepeatPrototypeRowId()) || !Objects.equals(tag.fieldCode, binding.getZiduanBianma())) { result.add(diagnostic("ERROR", "REPEAT_BINDING_MISMATCH", "重复原型控件与绑定元数据不一致", control, binding, null, Arrays.asList("LOCATE", "REPAIR"))); continue; } if (!"prototype".equals(tag.rowId)) { result.add(diagnostic("ERROR", "REPEAT_PROTOTYPE_ID_INVALID", "模板重复控件必须使用 prototype 原型行标识", control, binding, null, Arrays.asList("LOCATE", "REPAIR"))); } if (!configured.containsKey(tag.groupCode)) { result.add(diagnostic("ERROR", "REPEAT_GROUP_CONFIG_MISSING", "重复组缺少行数约束配置", control, binding, null, Arrays.asList("LOCATE"))); } if (!"word/document.xml".equals(control.getPartName()) || control.getTableIndex() == null || control.getTableRowIndex() == null) { result.add(diagnostic("ERROR", "REPEAT_NOT_IN_TABLE_ROW", "重复原型控件必须位于正文表格的数据行内", control, binding, null, Arrays.asList("LOCATE"))); } if (control.isNestedTable()) { result.add(diagnostic("ERROR", "NESTED_REPEAT_TABLE", "当前阶段不支持嵌套重复表格", control, binding, null, Arrays.asList("LOCATE"))); } if (control.isVerticalMerge()) { result.add(diagnostic("ERROR", "REPEAT_VERTICAL_MERGE", "重复数据区不支持跨行合并", control, binding, null, Arrays.asList("LOCATE"))); } PhysicalRow row = PhysicalRow.of(control); PhysicalRow groupRow = groupRows.putIfAbsent(tag.groupCode, row); if (groupRow != null && !groupRow.equals(row)) { result.add(diagnostic("ERROR", "MULTIPLE_REPEAT_PROTOTYPE_ROWS", "同一重复组的全部原型列必须位于同一条数据行", control, binding, null, Arrays.asList("LOCATE"))); } if (row != null) { String owner = rowOwners.putIfAbsent(row, tag.groupCode); if (owner != null && !owner.equals(tag.groupCode)) { result.add(diagnostic("ERROR", "OVERLAPPING_REPEAT_GROUPS", "不同重复组不能占用同一条物理数据行", control, binding, null, Arrays.asList("LOCATE"))); } } } for (String groupCode : configured.keySet()) { if (!groupRows.containsKey(groupCode)) { result.add(diagnostic("ERROR", "REPEAT_GROUP_WITHOUT_PROTOTYPE", "重复组配置没有对应的 DOCX 原型行", null, null, null, Collections.emptyList())); } } } private List removalDependencies(String tenantId, ElnTemplateBindingEntity binding, Collection activeBindings) { List result = new ArrayList<>(); if ("PRIMARY".equals(binding.getKongjianJuese())) { for (ElnTemplateBindingEntity candidate : activeBindings) { if ("REFERENCE".equals(candidate.getKongjianJuese()) && Objects.equals(binding.getId(), candidate.getYinyongShiliId())) { ElnTemplateVos.Diagnostic dependency = new ElnTemplateVos.Diagnostic(); dependency.setSeverity("ERROR"); dependency.setCode("ACTIVE_REFERENCE_DEPENDENCY"); dependency.setMessage("主控件仍有活动引用,不能删除"); dependency.setBindingId(candidate.getId()); dependency.setControlTag(candidate.getKongjianBiaoqian()); result.add(dependency); } } } Set bindingIds = Collections.singleton(binding.getId()); for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { List diagnostics = validator.validateRemoval( tenantId, binding.getMubanId(), bindingIds); if (diagnostics != null) { diagnostics.stream().filter(Objects::nonNull).forEach(result::add); } } return result; } private void appendPendingDiagnostics(List result, List pendingDiagnostics, List bindings, Map> controlsByTag, String tenantId) { Set allowed = new LinkedHashSet<>(Arrays.asList("PENDING_CONFIRMATION_REQUIRED", "DUPLICATE_TAG", "ROLE_MISMATCH", "TYPE_MISMATCH", "CONFIG_MISMATCH")); Map byId = safe(bindings).stream() .filter(Objects::nonNull).collect(Collectors.toMap(ElnTemplateBindingEntity::getId, item -> item, (first, second) -> first, LinkedHashMap::new)); for (ElnTemplateVos.Diagnostic item : safe(pendingDiagnostics)) { if (item == null || !allowed.contains(item.getCode())) { continue; } ElnTemplateBindingEntity binding = byId.get(item.getBindingId()); if (binding == null || !PENDING.equals(binding.getBangdingZhuangtai())) { continue; } DocxControlSnapshot control = first(controlsByTag.get(binding.getKongjianBiaoqian())); result.add(diagnostic(item.getSeverity(), item.getCode(), item.getMessage(), control, binding, displayName(tenantId, binding), Arrays.asList("LOCATE"))); } } private String displayName(String tenantId, ElnTemplateBindingEntity binding) { ElnFieldVersionEntity version = store.findFieldVersion(tenantId, binding.getZiduanBanbenId()); return version == null ? null : version.getZiduanMingcheng(); } private static Map config(ElnFieldVersionEntity version) { return DocxExpectedControlConfig.from( version.getShujuLeixing(), version.getKongjianPeizhi()); } private static Map> controlsByTag( List controls) { Map> result = new LinkedHashMap<>(); for (DocxControlSnapshot control : safe(controls)) { if (control != null && StringUtils.hasText(control.getTag())) { result.computeIfAbsent(control.getTag(), key -> new ArrayList<>()).add(control); } } return result; } private static HealthDiagnostic diagnostic(String severity, String code, String message, ElnTemplateBindingEntity binding, String displayName, List actions) { return diagnostic(severity, code, message, null, binding, displayName, actions); } private static HealthDiagnostic diagnostic(String severity, String code, String message, DocxControlSnapshot control, ElnTemplateBindingEntity binding, String displayName, List actions) { HealthDiagnostic result = new HealthDiagnostic(); result.setSeverity(severity); result.setCode(code); result.setMessage(message); result.setBindingId(binding == null ? null : binding.getId()); result.setControlTag(control != null ? control.getTag() : binding == null ? null : binding.getKongjianBiaoqian()); result.setFieldDisplayName(displayName); result.setPartName(control == null ? null : control.getPartName()); result.setExpectedVersion(binding == null ? null : binding.getFVersion()); result.setActions(actions == null ? new ArrayList<>() : new ArrayList<>(actions)); return result; } private static HealthDiagnostic diagnostic(String severity, String code, String message, DocxControlSnapshot control, String displayName, List actions) { return diagnostic(severity, code, message, control, null, displayName, actions); } private static DocxControlSnapshot first(List controls) { return controls == null || controls.isEmpty() ? null : controls.get(0); } private static String uniqueDocxFileId(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); List docx = files == null ? new ArrayList<>() : files.stream() .filter(Objects::nonNull) .filter(item -> StringUtils.hasText(item.getFileId())) .filter(item -> "docx".equalsIgnoreCase(item.getFileExtension()) || item.getFileId().toLowerCase().endsWith(".docx")) .map(item -> item.getFileId().trim()) .collect(Collectors.toList()); if (docx.size() != 1) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } return docx.get(0); } catch (DataException e) { throw e; } catch (RuntimeException e) { throw new DataException("模板附件无效"); } } private static ElnTemplateDocumentEvidence evidence( ObjectProvider provider) { ElnTemplateDocumentEvidence result = provider.getIfAvailable(); return result == null ? (tenantId, fileId) -> { throw new DataException("DOCX 控件证据服务不可用"); } : result; } private static String currentTenantId() { return resolveTenantId(TenantDataSourceUtil.getTenantInfo(), UserProvider.getUser()); } static String resolveTenantId(TenantVO routed, UserInfo user) { if (routed != null && StringUtils.hasText(routed.getEnCode())) { return routed.getEnCode().trim(); } return user != null && StringUtils.hasText(user.getTenantId()) ? user.getTenantId().trim() : "0"; } private static String value(String text) { return text == null ? "" : text; } private static List safe(List items) { return items == null ? new ArrayList<>() : items; } private static final class RepeatTag { private final String groupCode; private final String rowId; private final String fieldCode; private RepeatTag(String groupCode, String rowId, String fieldCode) { this.groupCode = groupCode; this.rowId = rowId; this.fieldCode = fieldCode; } private static RepeatTag parse(String tag) { String[] parts = tag == null ? new String[0] : tag.split("\\.", -1); if (parts.length != 5 || !"eln".equals(parts[0]) || !"repeat".equals(parts[1]) || !token(parts[2]) || !token(parts[3]) || !token(parts[4])) { return null; } return new RepeatTag(parts[2], parts[3], parts[4]); } private static boolean token(String value) { return value != null && value.matches("[A-Za-z0-9_-]{1,80}"); } } private static final class PhysicalRow { private final String partName; private final Integer tableIndex; private final Integer rowIndex; private PhysicalRow(String partName, Integer tableIndex, Integer rowIndex) { this.partName = partName; this.tableIndex = tableIndex; this.rowIndex = rowIndex; } private static PhysicalRow of(DocxControlSnapshot control) { return control == null || !StringUtils.hasText(control.getPartName()) || control.getTableIndex() == null || control.getTableRowIndex() == null ? null : new PhysicalRow(control.getPartName(), control.getTableIndex(), control.getTableRowIndex()); } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof PhysicalRow)) return false; PhysicalRow row = (PhysicalRow) other; return Objects.equals(partName, row.partName) && Objects.equals(tableIndex, row.tableIndex) && Objects.equals(rowIndex, row.rowIndex); } @Override public int hashCode() { return Objects.hash(partName, tableIndex, rowIndex); } } public interface Store { ElnTemplateVersionEntity findTemplate(String tenantId, String templateId); List listBindings(String tenantId, String templateId); ElnFieldDefEntity findField(String tenantId, String fieldId); ElnFieldVersionEntity findFieldVersion(String tenantId, String versionId); } public interface PendingReconciler { List reconcile( String tenantId, String templateId, Date now, List controls); } public interface RepeatGroupReader { List list(String tenantId, String templateId); } public interface CheckboxGroupReader { List list(String tenantId, String templateId); } private static final class MyBatisStore implements Store { private final ElnTemplateMapper templateMapper; private final ElnTemplateBindingMapper bindingMapper; private final ElnFieldDefMapper fieldMapper; private final ElnFieldVersionMapper fieldVersionMapper; private MyBatisStore(ElnTemplateMapper templateMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldDefMapper fieldMapper, ElnFieldVersionMapper fieldVersionMapper) { this.templateMapper = templateMapper; this.bindingMapper = bindingMapper; this.fieldMapper = fieldMapper; this.fieldVersionMapper = fieldVersionMapper; } @Override public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) { return templateMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateVersionEntity::getFTenantId, tenantId) .eq(ElnTemplateVersionEntity::getId, templateId) .and(q -> q.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0))); } @Override public List listBindings(String tenantId, String templateId) { return bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getMubanId, templateId) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)) .orderByAsc(ElnTemplateBindingEntity::getId)); } @Override public ElnFieldDefEntity findField(String tenantId, String fieldId) { return fieldMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnFieldDefEntity::getFTenantId, tenantId) .eq(ElnFieldDefEntity::getId, fieldId) .and(q -> q.isNull(ElnFieldDefEntity::getFDeleteMark) .or().eq(ElnFieldDefEntity::getFDeleteMark, 0))); } @Override public ElnFieldVersionEntity findFieldVersion(String tenantId, String versionId) { return fieldVersionMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnFieldVersionEntity::getFTenantId, tenantId) .eq(ElnFieldVersionEntity::getId, versionId) .and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark) .or().eq(ElnFieldVersionEntity::getFDeleteMark, 0))); } } }