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<String> tenantSupplier;
|
private final List<ElnTemplateBindingDependencyValidator> 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<ElnTemplateDocumentEvidence> evidenceProvider,
|
List<ElnTemplateBindingDependencyValidator> 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<String> tenantSupplier,
|
List<ElnTemplateBindingDependencyValidator> dependencyValidators,
|
RepeatGroupReader repeatGroupReader) {
|
this(store, evidence, pendingReconciler, tenantSupplier, dependencyValidators,
|
repeatGroupReader, (tenantId, templateId) -> Collections.emptyList(), null);
|
}
|
|
ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence,
|
PendingReconciler pendingReconciler, Supplier<String> tenantSupplier,
|
List<ElnTemplateBindingDependencyValidator> 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<String> tenantSupplier,
|
List<ElnTemplateBindingDependencyValidator> dependencyValidators,
|
RepeatGroupReader repeatGroupReader,
|
CheckboxGroupReader checkboxGroupReader) {
|
this(store, evidence, pendingReconciler, tenantSupplier, dependencyValidators,
|
repeatGroupReader, checkboxGroupReader, null);
|
}
|
|
ElnTemplateHealthServiceImpl(Store store, ElnTemplateDocumentEvidence evidence,
|
PendingReconciler pendingReconciler, Supplier<String> tenantSupplier,
|
List<ElnTemplateBindingDependencyValidator> 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<DocxControlSnapshot> controls = evidence.readControls(tenantId, fileId);
|
if (controls == null) {
|
throw new DataException("DOCX 控件证据无效");
|
}
|
boolean publishedTemplate = PUBLISHED.equals(template.getBizStatus());
|
List<ElnTemplateVos.Diagnostic> pendingDiagnostics = publishedTemplate
|
? Collections.emptyList()
|
: pendingReconciler.reconcile(tenantId, templateId, new Date(), controls);
|
List<ElnTemplateBindingEntity> bindings = store.listBindings(tenantId, templateId);
|
|
HealthReport result = new HealthReport();
|
result.setTemplateId(templateId);
|
result.setFileId(fileId);
|
List<HealthDiagnostic> 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<HealthDiagnostic> diagnostics,
|
List<ElnTemplateBindingEntity> 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<HealthDiagnostic> 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<HealthDiagnostic> diagnostics) {
|
for (HealthDiagnostic diagnostic : diagnostics) {
|
diagnostic.setActions(diagnostic.getActions().contains("LOCATE")
|
? Collections.singletonList("LOCATE") : Collections.emptyList());
|
}
|
}
|
|
private List<HealthDiagnostic> inspectBindings(String tenantId,
|
List<ElnTemplateBindingEntity> bindings, List<DocxControlSnapshot> controls,
|
List<ElnTemplateVos.Diagnostic> pendingDiagnostics,
|
List<ElnTemplateVos.RepeatGroup> repeatGroups,
|
List<ElnTemplateVos.CheckboxGroup> checkboxGroups,
|
boolean publishedTemplate) {
|
List<HealthDiagnostic> result = new ArrayList<>();
|
Map<String, List<DocxControlSnapshot>> controlsByTag = controlsByTag(controls);
|
Map<String, ElnTemplateBindingEntity> activeById = new LinkedHashMap<>();
|
Map<String, ElnTemplateBindingEntity> activeByTag = new LinkedHashMap<>();
|
Map<String, ElnTemplateBindingEntity> liveByTag = new LinkedHashMap<>();
|
Set<String> 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<String, List<DocxControlSnapshot>> item : controlsByTag.entrySet()) {
|
List<DocxControlSnapshot> 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<DocxControlSnapshot> 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<HealthDiagnostic> diagnostics,
|
String tenantId,
|
List<ElnTemplateBindingEntity> bindings,
|
Map<String, List<DocxControlSnapshot>> controlsByTag,
|
List<ElnTemplateVos.CheckboxGroup> configuredGroups,
|
boolean publishedTemplate) {
|
Map<String, List<ElnTemplateBindingEntity>> 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<String, ElnTemplateVos.CheckboxGroup> rules = new LinkedHashMap<>();
|
for (ElnTemplateVos.CheckboxGroup rule : safe(configuredGroups)) {
|
if (rule != null && StringUtils.hasText(rule.getGroupCode())) {
|
rules.put(rule.getGroupCode(), rule);
|
}
|
}
|
for (Map.Entry<String, List<ElnTemplateBindingEntity>> entry : groups.entrySet()) {
|
String groupCode = entry.getKey();
|
List<ElnTemplateBindingEntity> 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<String, ElnTemplateVos.CheckboxGroup> 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<ElnTemplateBindingEntity> 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<ElnTemplateVos.Diagnostic> 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<ElnTemplateBindingEntity> 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<ElnTemplateBindingEntity> members) {
|
int count = 0;
|
for (ElnTemplateBindingEntity member : members) {
|
ElnFieldVersionEntity version = store.findFieldVersion(
|
tenantId, member.getZiduanBanbenId());
|
Map<String, Object> 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<ElnTemplateBindingEntity> members,
|
Map<String, List<DocxControlSnapshot>> 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<HealthDiagnostic> result,
|
Map<String, ElnTemplateBindingEntity> activeByTag,
|
Map<String, List<DocxControlSnapshot>> controlsByTag,
|
List<ElnTemplateVos.RepeatGroup> repeatGroups) {
|
Map<String, ElnTemplateVos.RepeatGroup> configured = new LinkedHashMap<>();
|
for (ElnTemplateVos.RepeatGroup group : safe(repeatGroups)) {
|
if (group != null && StringUtils.hasText(group.getGroupCode())) {
|
configured.put(group.getGroupCode(), group);
|
}
|
}
|
Map<String, PhysicalRow> groupRows = new LinkedHashMap<>();
|
Map<PhysicalRow, String> rowOwners = new LinkedHashMap<>();
|
for (Map.Entry<String, List<DocxControlSnapshot>> 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<ElnTemplateVos.Diagnostic> removalDependencies(String tenantId,
|
ElnTemplateBindingEntity binding,
|
Collection<ElnTemplateBindingEntity> activeBindings) {
|
List<ElnTemplateVos.Diagnostic> 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<String> bindingIds = Collections.singleton(binding.getId());
|
for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) {
|
List<ElnTemplateVos.Diagnostic> diagnostics = validator.validateRemoval(
|
tenantId, binding.getMubanId(), bindingIds);
|
if (diagnostics != null) {
|
diagnostics.stream().filter(Objects::nonNull).forEach(result::add);
|
}
|
}
|
return result;
|
}
|
|
private void appendPendingDiagnostics(List<HealthDiagnostic> result,
|
List<ElnTemplateVos.Diagnostic> pendingDiagnostics,
|
List<ElnTemplateBindingEntity> bindings,
|
Map<String, List<DocxControlSnapshot>> controlsByTag, String tenantId) {
|
Set<String> allowed = new LinkedHashSet<>(Arrays.asList("PENDING_CONFIRMATION_REQUIRED",
|
"DUPLICATE_TAG", "ROLE_MISMATCH", "TYPE_MISMATCH", "CONFIG_MISMATCH"));
|
Map<String, ElnTemplateBindingEntity> 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<String, Object> config(ElnFieldVersionEntity version) {
|
return DocxExpectedControlConfig.from(
|
version.getShujuLeixing(), version.getKongjianPeizhi());
|
}
|
|
private static Map<String, List<DocxControlSnapshot>> controlsByTag(
|
List<DocxControlSnapshot> controls) {
|
Map<String, List<DocxControlSnapshot>> 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<String> 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<String> 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<String> actions) {
|
return diagnostic(severity, code, message, control, null, displayName, actions);
|
}
|
|
private static DocxControlSnapshot first(List<DocxControlSnapshot> controls) {
|
return controls == null || controls.isEmpty() ? null : controls.get(0);
|
}
|
|
private static String uniqueDocxFileId(String attachments) {
|
try {
|
List<ElnDocumentFileVo> files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class);
|
List<String> 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<ElnTemplateDocumentEvidence> 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 <T> List<T> safe(List<T> 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<ElnTemplateBindingEntity> listBindings(String tenantId, String templateId);
|
ElnFieldDefEntity findField(String tenantId, String fieldId);
|
ElnFieldVersionEntity findFieldVersion(String tenantId, String versionId);
|
}
|
|
public interface PendingReconciler {
|
List<jnpf.elnEntity.template.ElnTemplateVos.Diagnostic> reconcile(
|
String tenantId, String templateId, Date now,
|
List<DocxControlSnapshot> controls);
|
}
|
|
public interface RepeatGroupReader {
|
List<ElnTemplateVos.RepeatGroup> list(String tenantId, String templateId);
|
}
|
|
public interface CheckboxGroupReader {
|
List<ElnTemplateVos.CheckboxGroup> 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.<ElnTemplateVersionEntity>lambdaQuery()
|
.eq(ElnTemplateVersionEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateVersionEntity::getId, templateId)
|
.and(q -> q.isNull(ElnTemplateVersionEntity::getFDeleteMark)
|
.or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0)));
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listBindings(String tenantId, String templateId) {
|
return bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>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.<ElnFieldDefEntity>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.<ElnFieldVersionEntity>lambdaQuery()
|
.eq(ElnFieldVersionEntity::getFTenantId, tenantId)
|
.eq(ElnFieldVersionEntity::getId, versionId)
|
.and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark)
|
.or().eq(ElnFieldVersionEntity::getFDeleteMark, 0)));
|
}
|
}
|
}
|