package jnpf.elnService.impl; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.database.util.TenantDataSourceUtil; import jnpf.eln.RecordFreezeCheckBatch; import jnpf.eln.RecordFreezeCheckRequest; import jnpf.eln.RecordFreezeCheckResult; import jnpf.eln.RecordFreezeIssue; import jnpf.eln.RecordFreezeReleaseRequest; import jnpf.elnEntity.field.ElnFieldForms; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordFormulaCalculationResult; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRepeatValueEntity; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordSaveOperationEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnRecordFieldValueMapper; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRepeatValueMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnMapper.ElnRecordSaveOperationMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnMapper.ElnTemplateFormulaBindingMapper; import jnpf.elnService.ElnRecordFormulaCalculationService; import jnpf.elnService.ElnRecordFreezeCheckService; import jnpf.elnService.ElnCrossTemplateSnapshotResolver; import jnpf.elnService.ElnRecordSourceSyncOperationCoordinator; import jnpf.elnService.field.ElnTestResultFields; import jnpf.elnService.template.ElnTemplateDocumentEvidence; import jnpf.elnService.template.docx.DocxControlConfigMatcher; import jnpf.elnService.template.docx.DocxExpectedControlConfig; import jnpf.elnService.template.docx.DocxControlSnapshot; import jnpf.exception.DataException; import jnpf.onlyoffice.RecordRevisionFenceApi; import jnpf.util.JsonUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.format.DateTimeParseException; import java.util.Date; import java.util.Set; /** * LIMS 结果提交前的服务端冻结准备。计算日志和计算结果在本地事务中落库, * 但绝不改变 ELN 修订状态,也绝不反调 LIMS。 */ @Service public class ElnRecordFreezeCheckServiceImpl implements ElnRecordFreezeCheckService { private static final String ACTIVE = "ACTIVE"; private static final String PRIMARY = "PRIMARY"; private static final String REFERENCE = "REFERENCE"; private static final String DRAFT = "DRAFT"; private final ElnRecordMapper recordMapper; private final ElnRecordRevisionMapper revisionMapper; private final ElnRecordFieldValueMapper valueMapper; private final ElnRecordRepeatValueMapper repeatValueMapper; private final ElnRecordSaveOperationMapper operationMapper; private final ElnTemplateBindingMapper bindingMapper; private final ElnTemplateFormulaBindingMapper formulaBindingMapper; private final ElnFieldVersionMapper fieldVersionMapper; private final ElnRecordFormulaCalculationService formulaCalculationService; private final ElnTemplateDocumentEvidence documentEvidence; private final RecordRevisionFenceApi revisionFenceApi; private final TenantRoute tenantRoute; private final ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver; private final ElnRecordSourceSyncOperationCoordinator sourceSyncOperations; @Autowired public ElnRecordFreezeCheckServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, RecordRevisionFenceApi revisionFenceApi, ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver, ElnRecordSourceSyncOperationCoordinator sourceSyncOperations) { this(recordMapper, revisionMapper, valueMapper, repeatValueMapper, operationMapper, bindingMapper, formulaBindingMapper, fieldVersionMapper, formulaCalculationService, documentEvidence, revisionFenceApi, new TenantRoute() { @Override public void switchTo(String tenantId) { TenantDataSourceUtil.switchTenant(tenantId); } @Override public void clear() { TenantDataSourceUtil.clearLocalTenantInfo(); } }, crossTemplateSnapshotResolver, sourceSyncOperations); } ElnRecordFreezeCheckServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, RecordRevisionFenceApi revisionFenceApi, TenantRoute tenantRoute) { this(recordMapper, revisionMapper, valueMapper, null, operationMapper, bindingMapper, formulaBindingMapper, fieldVersionMapper, formulaCalculationService, documentEvidence, revisionFenceApi, tenantRoute, ElnCrossTemplateSnapshotResolver.none(), ElnRecordSourceSyncOperationCoordinator.none()); } ElnRecordFreezeCheckServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, RecordRevisionFenceApi revisionFenceApi, TenantRoute tenantRoute) { this(recordMapper, revisionMapper, valueMapper, repeatValueMapper, operationMapper, bindingMapper, formulaBindingMapper, fieldVersionMapper, formulaCalculationService, documentEvidence, revisionFenceApi, tenantRoute, ElnCrossTemplateSnapshotResolver.none(), ElnRecordSourceSyncOperationCoordinator.none()); } ElnRecordFreezeCheckServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, RecordRevisionFenceApi revisionFenceApi, TenantRoute tenantRoute, ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver) { this(recordMapper, revisionMapper, valueMapper, repeatValueMapper, operationMapper, bindingMapper, formulaBindingMapper, fieldVersionMapper, formulaCalculationService, documentEvidence, revisionFenceApi, tenantRoute, crossTemplateSnapshotResolver, ElnRecordSourceSyncOperationCoordinator.none()); } ElnRecordFreezeCheckServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnRecordFieldValueMapper valueMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, RecordRevisionFenceApi revisionFenceApi, TenantRoute tenantRoute, ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver, ElnRecordSourceSyncOperationCoordinator sourceSyncOperations) { this.recordMapper = recordMapper; this.revisionMapper = revisionMapper; this.valueMapper = valueMapper; this.repeatValueMapper = repeatValueMapper; this.operationMapper = operationMapper; this.bindingMapper = bindingMapper; this.formulaBindingMapper = formulaBindingMapper; this.fieldVersionMapper = fieldVersionMapper; this.formulaCalculationService = formulaCalculationService; this.documentEvidence = documentEvidence; this.revisionFenceApi = revisionFenceApi; this.tenantRoute = tenantRoute; this.crossTemplateSnapshotResolver = crossTemplateSnapshotResolver; this.sourceSyncOperations = sourceSyncOperations; } @Override @Transactional(rollbackFor = Exception.class) public RecordFreezeCheckBatch prepare(RecordFreezeCheckRequest request) { requireRequest(request); return inTenant(request.getTenantId(), () -> prepareInTenant(request)); } private RecordFreezeCheckBatch prepareInTenant(RecordFreezeCheckRequest request) { RecordFreezeCheckBatch batch = new RecordFreezeCheckBatch(); List ordered = new ArrayList<>(uniqueIds(request.getTestWorkItemIds())); Collections.sort(ordered); for (String testWorkItemId : ordered) { batch.getItems().add(prepareOne(request, testWorkItemId)); } return batch; } private RecordFreezeCheckResult prepareOne(RecordFreezeCheckRequest request, String testWorkItemId) { RecordFreezeCheckResult result = result(testWorkItemId); ElnRecordEntity observed = recordMapper.selectLiveByTestWorkItem(request.getTenantId(), testWorkItemId); if (observed == null) { result.setStatus(RecordFreezeCheckResult.NOT_APPLICABLE); return result; } String token = null; ElnRecordEntity record = null; ElnRecordRevisionEntity revision = null; try { record = recordMapper.selectLiveForUpdate(request.getTenantId(), observed.getId()); if (record == null || !testWorkItemId.equals(record.getJianyanXiangId()) || !StringUtils.hasText(record.getDangqianXiudingId())) { add(result, "RECORD", "RECORD_CURRENT_INVALID", "电子记录不存在或当前修订无效"); return blocked(result); } revision = currentRevision(request.getTenantId(), record); if (revision == null || !DRAFT.equals(revision.getBizStatus()) || !StringUtils.hasText(revision.getJiluWendang())) { add(result, "PERMISSION", "REVISION_NOT_FREEZABLE", "电子记录当前修订不可冻结"); return blocked(result); } token = acquire(request.getTenantId(), record.getId(), revision.getJiluWendang(), request.getOperationCode() + ":" + testWorkItemId); checkPreparedOperations(request.getTenantId(), record.getId(), revision.getId(), result); Map bindings = activeBindings(request.getTenantId(), record.getMubanId(), result); List values = values(request.getTenantId(), record.getId(), revision.getId(), result); validateCrossTemplateSnapshots(request.getTenantId(), testWorkItemId, values, result); Map valuesByBinding = valuesByBinding(values, result); List formulas = activeFormulas(request.getTenantId(), record.getMubanId()); validateFormulaResultBindings(formulas, bindings, valuesByBinding, result); Map controls = controls(request.getTenantId(), revision.getJiluWendang(), result); validateBindingsAndDocument(request.getTenantId(), bindings, valuesByBinding, controls, result, false); // 不因先前诊断跳过权威全量复算,日志与结果必须反映本次提交前状态。 ElnRecordFormulaCalculationResult recalculated = new ElnRecordFormulaCalculationResult(); try { recalculated = formulaCalculationService.recalculateAllWithRepeats( request.getTenantId(), record, revision, values, request.getOperationCode() + ":" + testWorkItemId, request.getUserId()); if (recalculated == null) { recalculated = new ElnRecordFormulaCalculationResult(); recalculated.setFieldValues(formulaCalculationService.recalculateAll( request.getTenantId(), record, revision, values, request.getOperationCode() + ":" + testWorkItemId, request.getUserId())); } for (ElnRecordFieldValueEntity value : recalculated.getFieldValues()) { if (valueMapper.updateById(value) != 1) { add(result, "FORMULA", "FORMULA_RESULT_WRITE_CONFLICT", "公式结果保存冲突"); } } for (ElnRecordRepeatValueEntity value : recalculated.getRepeatValues()) { if (repeatValueMapper == null || repeatValueMapper.updateById(value) != 1) { add(result, "FORMULA", "REPEAT_FORMULA_RESULT_WRITE_CONFLICT", "重复公式结果保存冲突"); } } } catch (RuntimeException e) { add(result, "FORMULA", "FORMULA_RECALCULATION_FAILED", message(e, "公式权威复算失败")); } // 公式结果已更新后再次核对,防止旧 DB/DOCX 一致而本次复算改变结果值后仍放行。 validateBindingsAndDocument(request.getTenantId(), bindings, valuesByBinding, controls, result, true); validateFormulaStates(request.getTenantId(), formulas, valuesByBinding, bindings, result); validateRepeatFormulaStates(request.getTenantId(), formulas, recalculated, bindings, controls, result); TestSubmissionValues submissionValues = submissionValues(bindings, valuesByBinding, result); // 版本 CAS 将本次复算及 fence 前的版本变化显式串行化。 if (record.getFVersion() == null || recordMapper.advanceVersion(request.getTenantId(), record.getId(), record.getFVersion(), request.getUserId()) != 1) { add(result, "CONCURRENCY", "RECORD_VERSION_CONFLICT", "电子记录已被并发修改,请重新提交"); } if (hasBlockingIssue(result)) return blocked(result); result.setStatus(RecordFreezeCheckResult.READY); result.setReleaseToken(token); result.setTestResult(submissionValues.result); result.setTestConclusion(submissionValues.conclusion); return result; } catch (RuntimeException e) { add(result, "RECORD", "RECORD_FREEZE_FAILED", message(e, "电子记录冻结校验失败")); return blocked(result); } finally { if (!RecordFreezeCheckResult.READY.equals(result.getStatus()) && token != null && record != null && revision != null) { releaseQuietly(request.getTenantId(), record.getId(), revision.getJiluWendang(), token); } } } @Override public boolean release(RecordFreezeReleaseRequest request) { if (request == null || !StringUtils.hasText(request.getTenantId()) || !StringUtils.hasText(request.getOperationCode()) || request.getTestWorkItemIds() == null) return false; return inTenant(request.getTenantId(), () -> { boolean allReleased = true; for (String testWorkItemId : uniqueIds(request.getTestWorkItemIds())) { ElnRecordEntity record = recordMapper.selectLiveByTestWorkItem(request.getTenantId(), testWorkItemId); if (record == null) continue; ElnRecordRevisionEntity revision = record == null ? null : currentRevision(request.getTenantId(), record); if (record == null || revision == null || !StringUtils.hasText(revision.getJiluWendang())) { allReleased = false; continue; } try { requireSuccess(revisionFenceApi.release(request.getTenantId(), record.getId(), revision.getJiluWendang(), "annex", "freeze:" + request.getOperationCode() + ":" + testWorkItemId), "电子记录冻结释放失败"); } catch (RuntimeException e) { allReleased = false; } } return allReleased; }); } private ElnRecordRevisionEntity currentRevision(String tenantId, ElnRecordEntity record) { return revisionMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordRevisionEntity::getFTenantId, tenantId).eq(ElnRecordRevisionEntity::getId, record.getDangqianXiudingId()) .eq(ElnRecordRevisionEntity::getDianziJiluId, record.getId()) .and(q -> q.isNull(ElnRecordRevisionEntity::getFDeleteMark).or().eq(ElnRecordRevisionEntity::getFDeleteMark, 0)).last("limit 1")); } private void checkPreparedOperations(String tenantId, String recordId, String revisionId, RecordFreezeCheckResult result) { Long count = operationMapper.selectCount(Wrappers.lambdaQuery() .eq(ElnRecordSaveOperationEntity::getFTenantId, tenantId).eq(ElnRecordSaveOperationEntity::getDianziJiluId, recordId) .eq(ElnRecordSaveOperationEntity::getXiudingId, revisionId).eq(ElnRecordSaveOperationEntity::getOperationStatus, "PREPARED") .and(q -> q.isNull(ElnRecordSaveOperationEntity::getFDeleteMark).or().eq(ElnRecordSaveOperationEntity::getFDeleteMark, 0))); if (count != null && count > 0) { add(result, "CONCURRENCY", "SAVE_OPERATION_PENDING", "电子记录仍有进行中的保存操作"); } if (sourceSyncOperations.hasPendingDocumentConfirmation(tenantId, recordId)) { add(result, "CONCURRENCY", "SOURCE_SYNC_DOCUMENT_PENDING", "电子记录来源同步仍待文档确认"); } } private Map activeBindings(String tenantId, String templateId, RecordFreezeCheckResult result) { Map map = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId).eq(ElnTemplateBindingEntity::getMubanId, templateId) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, ACTIVE) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark).or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)))) { if (map.put(binding.getId(), binding) != null) { add(result, "DOCUMENT", "ACTIVE_BINDING_DUPLICATED", "活动字段绑定重复"); } } if (map.isEmpty()) add(result, "DOCUMENT", "ACTIVE_BINDING_MISSING", "电子记录没有活动字段绑定"); return map; } private List activeFormulas(String tenantId, String templateId) { return formulaBindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId) .eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, ACTIVE) .and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark) .or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0)) .orderByAsc(ElnTemplateFormulaBindingEntity::getTuopuXuhao) .orderByAsc(ElnTemplateFormulaBindingEntity::getId)); } private List values(String tenantId, String recordId, String revisionId, RecordFreezeCheckResult result) { List values = valueMapper.selectList(Wrappers.lambdaQuery() .eq(ElnRecordFieldValueEntity::getFTenantId, tenantId).eq(ElnRecordFieldValueEntity::getDianziJiluId, recordId) .eq(ElnRecordFieldValueEntity::getXiudingId, revisionId) .and(q -> q.isNull(ElnRecordFieldValueEntity::getFDeleteMark).or().eq(ElnRecordFieldValueEntity::getFDeleteMark, 0))); if (values.isEmpty()) add(result, "REQUIRED", "RECORD_VALUE_MISSING", "电子记录没有字段值"); return values; } private Map valuesByBinding(List values, RecordFreezeCheckResult result) { Map map = new HashMap<>(); for (ElnRecordFieldValueEntity value : values) { if (map.put(value.getMubanBangdingId(), value) != null) { add(result, "REQUIRED", "RECORD_VALUE_DUPLICATED", "电子记录存在重复主字段值"); } } return map; } private void validateCrossTemplateSnapshots(String tenantId, String testWorkItemId, List values, RecordFreezeCheckResult result) { List issues = crossTemplateSnapshotResolver.validatePersistedSnapshots(tenantId, testWorkItemId, values); if (issues == null) return; for (ElnCrossTemplateSnapshotResolver.SnapshotIssue issue : issues) { if (issue == null) continue; add(result, "CROSS_TEMPLATE", issue.getCode(), issue.getMessage(), true, issue.getTargetBindingId(), issue.getTargetFieldCode(), issue.getSourceTemplateCode()); } } private void validateFormulaResultBindings(List formulas, Map bindings, Map values, RecordFreezeCheckResult result) { Set resultBindingIds = new LinkedHashSet<>(); for (ElnTemplateFormulaBindingEntity formula : formulas) { String resultBindingId = formula == null ? null : formula.getJieguoBangdingId(); ElnTemplateBindingEntity binding = bindings.get(resultBindingId); ElnRecordFieldValueEntity value = values.get(resultBindingId); boolean primaryValid = binding != null && PRIMARY.equals(binding.getKongjianJuese()) && value != null && Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId()); boolean repeatValid = binding != null && "REPEAT".equals(binding.getKongjianJuese()) && StringUtils.hasText(binding.getRepeatGroupCode()); if (!StringUtils.hasText(resultBindingId) || !(primaryValid || repeatValid)) { add(result, "FORMULA", "FORMULA_RESULT_BINDING_INVALID", "公式结果字段固定绑定无效: " + formulaLabel(formula)); } if (StringUtils.hasText(resultBindingId) && !resultBindingIds.add(resultBindingId)) { add(result, "FORMULA", "FORMULA_RESULT_BINDING_DUPLICATED", "公式结果字段被多个活动公式写入: " + resultBindingId); } } } private Map controls(String tenantId, String fileId, RecordFreezeCheckResult result) { Map map = new HashMap<>(); try { for (DocxControlSnapshot control : documentEvidence.readControls(tenantId, fileId)) { if (control == null || !StringUtils.hasText(control.getTag()) || map.put(control.getTag(), control) != null) { add(result, "DOCUMENT", "DOCX_CONTROL_TAG_INVALID", "DOCX 控件标签无效或重复"); } } } catch (RuntimeException e) { add(result, "DOCUMENT", "DOCX_EVIDENCE_READ_FAILED", message(e, "DOCX 控件证据读取失败")); } return map; } private void validateBindingsAndDocument(String tenantId, Map bindings, Map values, Map controls, RecordFreezeCheckResult result, boolean compareValues) { Set primaryBindingIds = new LinkedHashSet<>(); for (ElnTemplateBindingEntity binding : bindings.values()) { DocxControlSnapshot control = controls.get(binding.getKongjianBiaoqian()); if (control == null || !Objects.equals(binding.getKongjianJuese(), control.getControlRole()) || !Objects.equals(binding.getKongjianLeixing(), control.getControlType())) { add(result, "DOCUMENT", "DOCX_BINDING_MISMATCH", "DOCX 与活动字段绑定不一致: " + binding.getZiduanBianma()); } if (!PRIMARY.equals(binding.getKongjianJuese())) continue; primaryBindingIds.add(binding.getId()); ElnRecordFieldValueEntity value = values.get(binding.getId()); if (value == null) { ElnFieldVersionEntity missingValueVersion = fieldVersionMapper.selectById(binding.getZiduanBanbenId()); if (missingValueVersion != null && tenantId.equals(missingValueVersion.getFTenantId())) { validateConfiguredSourceInitialization( missingValueVersion, binding, null, result); } add(result, "REQUIRED", "RECORD_FIELD_VERSION_MISMATCH", "记录字段与固定字段版本不一致: " + binding.getZiduanBianma()); continue; } if (!Objects.equals(value.getZiduanBanbenId(), binding.getZiduanBanbenId()) || !Objects.equals(value.getZiduanBianma(), binding.getZiduanBianma())) { add(result, "REQUIRED", "RECORD_FIELD_VERSION_MISMATCH", "记录字段与固定字段版本不一致: " + binding.getZiduanBianma()); continue; } ElnFieldVersionEntity version = fieldVersionMapper.selectById(binding.getZiduanBanbenId()); if (version == null || !tenantId.equals(version.getFTenantId())) { add(result, "DOCUMENT", "FIELD_VERSION_MISSING", "固定字段版本不存在: " + binding.getZiduanBianma()); } else { if (control != null && !DocxControlConfigMatcher.matches(control.getControlType(), DocxExpectedControlConfig.from(version.getShujuLeixing(), version.getKongjianPeizhi()), control.getControlConfig())) { add(result, "DOCUMENT", "DOCX_CONTROL_CONFIG_MISMATCH", "DOCX 控件配置与固定字段版本不一致: " + binding.getZiduanBianma()); } validateConfiguredSourceInitialization(version, binding, value, result); String storedValue = storedValue(value, version); if (Integer.valueOf(1).equals(version.getShifouBitian()) && !StringUtils.hasText(storedValue)) { add(result, "REQUIRED", "REQUIRED_FIELD_MISSING", "必填字段未填写: " + binding.getZiduanBianma()); } if (compareValues && control != null && !Objects.equals(normalizeStored(storedValue, version), normalizeDocx(control.getCurrentValue(), version))) { add(result, "DOCUMENT", "DOCX_VALUE_MISMATCH", "结构化主字段与 DOCX 值不一致: " + binding.getZiduanBianma()); } } } for (String bindingId : values.keySet()) { if (!primaryBindingIds.contains(bindingId)) { add(result, "REQUIRED", "RECORD_FIELD_VALUE_INVALID", "电子记录存在失效或非主字段值"); } } for (ElnTemplateBindingEntity reference : bindings.values()) { if (!REFERENCE.equals(reference.getKongjianJuese())) continue; ElnTemplateBindingEntity primary = bindings.get(reference.getYinyongShiliId()); DocxControlSnapshot primaryControl = primary == null ? null : controls.get(primary.getKongjianBiaoqian()); DocxControlSnapshot referenceControl = controls.get(reference.getKongjianBiaoqian()); if (primary == null || !PRIMARY.equals(primary.getKongjianJuese()) || !Objects.equals(primary.getZiduanDingyiId(), reference.getZiduanDingyiId()) || !Objects.equals(primary.getZiduanBianma(), reference.getZiduanBianma()) || !Objects.equals(primary.getZiduanBanbenId(), reference.getZiduanBanbenId()) || primaryControl == null || referenceControl == null || !Objects.equals(primaryControl.getCurrentValue(), referenceControl.getCurrentValue())) { add(result, "REFERENCE", "DOCX_REFERENCE_MISMATCH", "DOCX 引用字段与主字段不一致: " + reference.getZiduanBianma()); } } } private static void validateConfiguredSourceInitialization( ElnFieldVersionEntity version, ElnTemplateBindingEntity binding, ElnRecordFieldValueEntity value, RecordFreezeCheckResult result) { ElnFieldForms.Binding config; try { config = StringUtils.hasText(version.getBangdingPeizhi()) ? JsonUtil.getJsonToBean(version.getBangdingPeizhi(), ElnFieldForms.Binding.class) : null; } catch (RuntimeException invalid) { return; } if (config == null || config.getRead() == null || !"LIMS_TASK".equals(config.getRead().getSource()) || config.getRead().getPropertyRevision() == null) { return; } String outcome = value == null ? null : value.getYuanQuzhiZhuangtai(); boolean completedOutcome = "SUCCESS".equals(outcome) || "MISSING".equals(outcome) || "NOT_APPLICABLE".equals(outcome) || "CLEARED".equals(outcome); boolean initialized = value != null && "LIMS_TASK".equals(value.getZhiLaiyuan()) && Objects.equals(config.getRead().getProperty(), value.getYuanShuxing()) && Objects.equals(config.getRead().getPropertyRevision(), value.getYuanShuxingBanben()) && completedOutcome && StringUtils.hasText(value.getYuanXueyuan()) && value.getYuanHuoquShijian() != null; if (!initialized) { add(result, "SOURCE", "SOURCE_INITIALIZATION_INCOMPLETE", "配置来源字段尚未完成初始化: " + binding.getZiduanBianma(), true, binding.getId(), binding.getZiduanBianma(), null); } } private void validateFormulaStates(String tenantId, List formulas, Map values, Map bindings, RecordFreezeCheckResult result) { for (ElnTemplateFormulaBindingEntity formula : formulas) { ElnRecordFieldValueEntity value = values.get(formula.getJieguoBangdingId()); if (value == null) continue; ElnTemplateBindingEntity binding = bindings.get(formula.getJieguoBangdingId()); if (!StringUtils.hasText(value.getJisuanZhuangtai())) { add(result, "FORMULA", "FORMULA_STATUS_MISSING", "公式未产生权威计算状态: " + formulaLabel(formula)); continue; } boolean required = binding != null && required(binding.getZiduanBanbenId(), tenantId); if (!"SUCCESS".equals(value.getJisuanZhuangtai())) { boolean blocking = required || !"MISSING_INPUT".equals(value.getJisuanZhuangtai()); add(result, "FORMULA", required ? "REQUIRED_FORMULA_NOT_SUCCESS" : "FORMULA_NOT_SUCCESS", (required ? "必填公式未成功: " : "公式未成功: ") + value.getZiduanBianma() + "(" + value.getJisuanZhuangtai() + ")", blocking); } } } private void validateRepeatFormulaStates(String tenantId, List formulas, ElnRecordFormulaCalculationResult calculated, Map bindings, Map controls, RecordFreezeCheckResult result) { Map repeatResults = new LinkedHashMap<>(); for (ElnTemplateFormulaBindingEntity formula : formulas) { ElnTemplateBindingEntity binding = bindings.get(formula.getJieguoBangdingId()); if (binding != null && "REPEAT".equals(binding.getKongjianJuese())) { repeatResults.put(binding.getId(), binding); } } if (repeatResults.isEmpty()) return; List rows = calculated.getRepeatRows() == null ? Collections.emptyList() : calculated.getRepeatRows(); List values = calculated.getRepeatValues() == null ? Collections.emptyList() : calculated.getRepeatValues(); Map valuesByCell = new LinkedHashMap<>(); for (ElnRecordRepeatValueEntity value : values) { if (value == null || !repeatResults.containsKey(value.getMubanBangdingId())) continue; String key = repeatCellKey(value.getRepeatGroupCode(), value.getRepeatRowId(), value.getMubanBangdingId()); if (valuesByCell.put(key, value) != null) { add(result, "FORMULA", "REPEAT_FORMULA_RESULT_DUPLICATED", "重复行公式结果单元格重复"); } } Set rowKeys = new LinkedHashSet<>(); for (ElnRecordRepeatRowEntity row : rows) { if (row == null || !ACTIVE.equals(row.getOperationStatus()) || Integer.valueOf(1).equals(row.getFDeleteMark()) || !StringUtils.hasText(row.getRepeatGroupCode()) || !StringUtils.hasText(row.getRowId()) || !rowKeys.add(repeatCellKey( row.getRepeatGroupCode(), row.getRowId(), ""))) { add(result, "FORMULA", "REPEAT_FORMULA_ROW_INVALID", "重复行公式活动行身份无效"); continue; } for (ElnTemplateBindingEntity binding : repeatResults.values()) { if (!Objects.equals(row.getRepeatGroupCode(), binding.getRepeatGroupCode())) continue; validateRepeatFormulaCell(tenantId, row, binding, valuesByCell.remove(repeatCellKey(row.getRepeatGroupCode(), row.getRowId(), binding.getId())), controls, result); } } if (!valuesByCell.isEmpty()) { add(result, "FORMULA", "REPEAT_FORMULA_RESULT_ORPHANED", "重复行公式结果不属于本次活动行"); } } private void validateRepeatFormulaCell(String tenantId, ElnRecordRepeatRowEntity row, ElnTemplateBindingEntity binding, ElnRecordRepeatValueEntity value, Map controls, RecordFreezeCheckResult result) { String label = binding.getZiduanBianma() + "[" + row.getRowId() + "]"; ElnFieldVersionEntity version = fieldVersionMapper.selectById(binding.getZiduanBanbenId()); boolean required = version != null && tenantId.equals(version.getFTenantId()) && Integer.valueOf(1).equals(version.getShifouBitian()); if (value == null) { add(result, "FORMULA", "REPEAT_FORMULA_RESULT_MISSING", "重复行公式结果单元格缺失: " + label); } else if (!Objects.equals(row.getRepeatGroupCode(), value.getRepeatGroupCode()) || !Objects.equals(row.getRowId(), value.getRepeatRowId()) || !Objects.equals(binding.getId(), value.getMubanBangdingId()) || !Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId())) { add(result, "FORMULA", "REPEAT_FORMULA_RESULT_INVALID", "重复行公式结果固定绑定无效: " + label); } else if (!StringUtils.hasText(value.getJisuanZhuangtai())) { add(result, "FORMULA", "REPEAT_FORMULA_STATUS_MISSING", "重复行公式未产生权威计算状态: " + label); } else if (!"SUCCESS".equals(value.getJisuanZhuangtai())) { boolean blocking = required || !"MISSING_INPUT".equals(value.getJisuanZhuangtai()); add(result, "FORMULA", required ? "REQUIRED_REPEAT_FORMULA_NOT_SUCCESS" : "REPEAT_FORMULA_NOT_SUCCESS", (required ? "必填重复行公式未成功: " : "重复行公式未成功: ") + label + "(" + value.getJisuanZhuangtai() + ")", blocking); } String tag = "eln.repeat." + row.getRepeatGroupCode() + "." + row.getRowId() + "." + binding.getZiduanBianma(); DocxControlSnapshot control = controls.get(tag); if (control == null || !"REPEAT".equals(control.getControlRole()) || !Objects.equals(binding.getKongjianLeixing(), control.getControlType())) { add(result, "DOCUMENT", "DOCX_REPEAT_FORMULA_BINDING_MISMATCH", "DOCX 重复行公式控件与固定绑定不一致: " + label); } else if (version == null || !tenantId.equals(version.getFTenantId())) { add(result, "DOCUMENT", "REPEAT_FORMULA_FIELD_VERSION_MISSING", "重复行公式固定字段版本不存在: " + label); } else if (value != null && !Objects.equals(normalizeStored(value.getZhiWenben(), version), normalizeDocx(control.getCurrentValue(), version))) { add(result, "DOCUMENT", "DOCX_REPEAT_FORMULA_VALUE_MISMATCH", "结构化重复行公式结果与 DOCX 值不一致: " + label); } } private static String repeatCellKey(String groupCode, String rowId, String bindingId) { return String.valueOf(groupCode) + '\u0000' + String.valueOf(rowId) + '\u0000' + String.valueOf(bindingId); } private static String formulaLabel(ElnTemplateFormulaBindingEntity formula) { if (formula == null) return "unknown"; return StringUtils.hasText(formula.getCaozuoBianma()) ? formula.getCaozuoBianma() : formula.getId(); } private boolean required(String versionId, String tenantId) { ElnFieldVersionEntity version = fieldVersionMapper.selectById(versionId); return version != null && tenantId.equals(version.getFTenantId()) && Integer.valueOf(1).equals(version.getShifouBitian()); } private static TestSubmissionValues submissionValues( Map bindings, Map valuesByBinding, RecordFreezeCheckResult result) { String testResult = valueForSystemBinding(bindings, valuesByBinding, ElnTestResultFields.RESULT, "TEST_RESULT", result); String testConclusion = valueForSystemBinding(bindings, valuesByBinding, ElnTestResultFields.CONCLUSION, "TEST_CONCLUSION", result); if (StringUtils.hasText(testConclusion) && !ElnTestResultFields.CONCLUSION_CODES.contains(testConclusion)) { add(result, "REQUIRED", "TEST_CONCLUSION_INVALID", "检验结论值非法:" + testConclusion); } return new TestSubmissionValues(testResult, testConclusion); } private static String valueForSystemBinding(Map bindings, Map valuesByBinding, String fieldCode, String diagnosticPrefix, RecordFreezeCheckResult result) { ElnTemplateBindingEntity matched = null; for (ElnTemplateBindingEntity binding : bindings.values()) { if (binding == null || !PRIMARY.equals(binding.getKongjianJuese()) || !fieldCode.equals(binding.getZiduanBianma())) continue; if (matched != null) { add(result, "REQUIRED", diagnosticPrefix + "_BINDING_DUPLICATED", "系统字段绑定重复:" + fieldCode); return null; } matched = binding; } if (matched == null) { add(result, "REQUIRED", diagnosticPrefix + "_BINDING_MISSING", "模板缺少系统字段绑定:" + fieldCode); return null; } ElnRecordFieldValueEntity value = valuesByBinding.get(matched.getId()); String text = value == null ? null : normalizeScalar(value.getZhiWenben(), "TEXT"); if (!StringUtils.hasText(text)) { add(result, "REQUIRED", diagnosticPrefix + "_VALUE_MISSING", "系统字段值不能为空:" + fieldCode); } return text; } private static final class TestSubmissionValues { private final String result; private final String conclusion; private TestSubmissionValues(String result, String conclusion) { this.result = result; this.conclusion = conclusion; } } private static String normalizeStored(String value, ElnFieldVersionEntity version) { return normalizeScalar(value, version == null ? null : version.getShujuLeixing()); } private static String storedValue(ElnRecordFieldValueEntity value, ElnFieldVersionEntity version) { String type = version == null ? null : version.getShujuLeixing(); if (("DECIMAL".equalsIgnoreCase(type) || "NUMBER".equalsIgnoreCase(type)) && value.getZhiShuzhi() != null) { return value.getZhiShuzhi().toPlainString(); } if (("DATE".equalsIgnoreCase(type) || "DATETIME".equalsIgnoreCase(type)) && value.getZhiRiqi() != null) { return new SimpleDateFormat("DATETIME".equalsIgnoreCase(type) ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd").format(value.getZhiRiqi()); } return value.getZhiWenben(); } @SuppressWarnings("unchecked") private static String normalizeDocx(String value, ElnFieldVersionEntity version) { String type = version == null ? null : version.getShujuLeixing(); String normalized = normalizeScalar(value, type); if (!StringUtils.hasText(normalized) || !("SELECT".equalsIgnoreCase(type) || "DROPDOWN".equalsIgnoreCase(type))) { return normalized; } try { Map config = StringUtils.hasText(version.getKongjianPeizhi()) ? JsonUtil.getJsonToBean(version.getKongjianPeizhi(), Map.class) : Collections.emptyMap(); Object options = config.get("options"); if (!(options instanceof List)) return normalized; for (Object option : (List) options) { if (!(option instanceof Map)) continue; Map item = (Map) option; if (normalized.equals(String.valueOf(item.get("display")))) return String.valueOf(item.get("value")); } } catch (RuntimeException ignored) { return normalized; } return normalized; } private static String normalizeScalar(String value, String type) { if (!StringUtils.hasText(value)) return null; String actual = value.trim(); if ("DECIMAL".equalsIgnoreCase(type) || "NUMBER".equalsIgnoreCase(type)) { try { return new BigDecimal(actual).stripTrailingZeros().toPlainString(); } catch (NumberFormatException ignored) { return actual; } } if ("BOOLEAN".equalsIgnoreCase(type) || "CHECKBOX".equalsIgnoreCase(type)) { return "true".equalsIgnoreCase(actual) ? "true" : "false".equalsIgnoreCase(actual) ? "false" : actual; } if ("DATE".equalsIgnoreCase(type) || "DATETIME".equalsIgnoreCase(type)) { String format = "DATETIME".equalsIgnoreCase(type) ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd"; try { SimpleDateFormat parser = new SimpleDateFormat(format); parser.setLenient(false); return new SimpleDateFormat(format).format(parser.parse(actual)); } catch (ParseException ignored) { try { return new SimpleDateFormat(format).format(Date.from(Instant.parse(actual))); } catch (DateTimeParseException ignoredAgain) { return actual; } } } return actual; } private String acquire(String tenantId, String recordId, String fileId, String operationCode) { ActionResult response = revisionFenceApi.acquireForOperation( tenantId, recordId, fileId, "annex", operationCode); if (response == null || !ActionResultCode.SUCCESS.getCode().equals(response.getCode()) || !StringUtils.hasText(response.getData())) { throw new DataException(response == null || !StringUtils.hasText(response.getMsg()) ? "电子记录正在编辑或保存,无法冻结" : response.getMsg()); } return response.getData(); } private void releaseQuietly(String tenantId, String recordId, String fileId, String token) { try { revisionFenceApi.release(tenantId, recordId, fileId, "annex", token); } catch (RuntimeException ignored) { // 原始诊断更有可操作性;失败 fence 仍由 OnlyOffice 运维恢复。 } } private static void requireSuccess(ActionResult result, String fallback) { if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || !Boolean.TRUE.equals(result.getData())) { throw new DataException(result == null || !StringUtils.hasText(result.getMsg()) ? fallback : result.getMsg()); } } private static RecordFreezeCheckResult result(String testWorkItemId) { RecordFreezeCheckResult result = new RecordFreezeCheckResult(); result.setTestWorkItemId(testWorkItemId); result.setStatus(RecordFreezeCheckResult.BLOCKED); return result; } private static RecordFreezeCheckResult blocked(RecordFreezeCheckResult result) { result.setStatus(RecordFreezeCheckResult.BLOCKED); result.setReleaseToken(null); return result; } private static void add(RecordFreezeCheckResult result, String category, String code, String message) { add(result, category, code, message, true); } private static void add(RecordFreezeCheckResult result, String category, String code, String message, boolean blocking) { add(result, category, code, message, blocking, null, null, null); } private static void add(RecordFreezeCheckResult result, String category, String code, String message, boolean blocking, String targetBindingId, String targetFieldCode, String sourceTemplateCode) { if (!StringUtils.hasText(message)) return; for (RecordFreezeIssue issue : result.getDiagnostics()) { if (Objects.equals(issue.getCode(), code) && Objects.equals(issue.getMessage(), message) && Objects.equals(issue.getTargetBindingId(), targetBindingId) && Objects.equals(issue.getTargetFieldCode(), targetFieldCode) && Objects.equals(issue.getSourceTemplateCode(), sourceTemplateCode)) return; } RecordFreezeIssue issue = new RecordFreezeIssue(); issue.setCategory(category); issue.setCode(code); issue.setMessage(message); issue.setBlocking(blocking); issue.setTargetBindingId(targetBindingId); issue.setTargetFieldCode(targetFieldCode); issue.setSourceTemplateCode(sourceTemplateCode); result.getDiagnostics().add(issue); } private static boolean hasBlockingIssue(RecordFreezeCheckResult result) { for (RecordFreezeIssue issue : result.getDiagnostics()) if (issue != null && issue.isBlocking()) return true; return false; } private static String message(RuntimeException e, String fallback) { return StringUtils.hasText(e.getMessage()) ? e.getMessage() : fallback; } private static Set uniqueIds(List ids) { Set result = new LinkedHashSet<>(); if (ids != null) for (String id : ids) if (StringUtils.hasText(id)) result.add(id.trim()); return result; } private static void requireRequest(RecordFreezeCheckRequest request) { if (request == null || !StringUtils.hasText(request.getTenantId()) || !StringUtils.hasText(request.getUserId()) || !StringUtils.hasText(request.getOperationCode()) || uniqueIds(request.getTestWorkItemIds()).isEmpty()) { throw new DataException("ELN 冻结校验请求不完整"); } } private T inTenant(String tenantId, TenantAction action) { try { tenantRoute.switchTo(tenantId.trim()); return action.get(); } finally { tenantRoute.clear(); } } @FunctionalInterface private interface TenantAction { T get(); } interface TenantRoute { void switchTo(String tenantId); void clear(); } }