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<String> 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<String, ElnTemplateBindingEntity> bindings = activeBindings(request.getTenantId(), record.getMubanId(), result);
|
List<ElnRecordFieldValueEntity> values = values(request.getTenantId(), record.getId(), revision.getId(), result);
|
validateCrossTemplateSnapshots(request.getTenantId(), testWorkItemId, values, result);
|
Map<String, ElnRecordFieldValueEntity> valuesByBinding = valuesByBinding(values, result);
|
List<ElnTemplateFormulaBindingEntity> formulas = activeFormulas(request.getTenantId(), record.getMubanId());
|
validateFormulaResultBindings(formulas, bindings, valuesByBinding, result);
|
Map<String, DocxControlSnapshot> 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.<ElnRecordRevisionEntity>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.<ElnRecordSaveOperationEntity>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<String, ElnTemplateBindingEntity> activeBindings(String tenantId, String templateId, RecordFreezeCheckResult result) {
|
Map<String, ElnTemplateBindingEntity> map = new LinkedHashMap<>();
|
for (ElnTemplateBindingEntity binding : bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>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<ElnTemplateFormulaBindingEntity> activeFormulas(String tenantId, String templateId) {
|
return formulaBindingMapper.selectList(Wrappers.<ElnTemplateFormulaBindingEntity>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<ElnRecordFieldValueEntity> values(String tenantId, String recordId, String revisionId, RecordFreezeCheckResult result) {
|
List<ElnRecordFieldValueEntity> values = valueMapper.selectList(Wrappers.<ElnRecordFieldValueEntity>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<String, ElnRecordFieldValueEntity> valuesByBinding(List<ElnRecordFieldValueEntity> values, RecordFreezeCheckResult result) {
|
Map<String, ElnRecordFieldValueEntity> 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<ElnRecordFieldValueEntity> values, RecordFreezeCheckResult result) {
|
List<ElnCrossTemplateSnapshotResolver.SnapshotIssue> 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<ElnTemplateFormulaBindingEntity> formulas,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
Map<String, ElnRecordFieldValueEntity> values,
|
RecordFreezeCheckResult result) {
|
Set<String> 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<String, DocxControlSnapshot> controls(String tenantId, String fileId, RecordFreezeCheckResult result) {
|
Map<String, DocxControlSnapshot> 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<String, ElnTemplateBindingEntity> bindings,
|
Map<String, ElnRecordFieldValueEntity> values,
|
Map<String, DocxControlSnapshot> controls, RecordFreezeCheckResult result,
|
boolean compareValues) {
|
Set<String> 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<ElnTemplateFormulaBindingEntity> formulas,
|
Map<String, ElnRecordFieldValueEntity> values,
|
Map<String, ElnTemplateBindingEntity> 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<ElnTemplateFormulaBindingEntity> formulas,
|
ElnRecordFormulaCalculationResult calculated,
|
Map<String, ElnTemplateBindingEntity> bindings,
|
Map<String, DocxControlSnapshot> controls,
|
RecordFreezeCheckResult result) {
|
Map<String, ElnTemplateBindingEntity> 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<ElnRecordRepeatRowEntity> rows = calculated.getRepeatRows() == null
|
? Collections.<ElnRecordRepeatRowEntity>emptyList() : calculated.getRepeatRows();
|
List<ElnRecordRepeatValueEntity> values = calculated.getRepeatValues() == null
|
? Collections.<ElnRecordRepeatValueEntity>emptyList() : calculated.getRepeatValues();
|
Map<String, ElnRecordRepeatValueEntity> 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<String> 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<String, DocxControlSnapshot> 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<String, ElnTemplateBindingEntity> bindings,
|
Map<String, ElnRecordFieldValueEntity> 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<String, ElnTemplateBindingEntity> bindings,
|
Map<String, ElnRecordFieldValueEntity> 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<String, Object> config = StringUtils.hasText(version.getKongjianPeizhi())
|
? JsonUtil.getJsonToBean(version.getKongjianPeizhi(), Map.class) : Collections.<String, Object>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<String> 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<Boolean> 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<String> uniqueIds(List<String> ids) {
|
Set<String> 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> T inTenant(String tenantId, TenantAction<T> action) {
|
try {
|
tenantRoute.switchTo(tenantId.trim());
|
return action.get();
|
} finally {
|
tenantRoute.clear();
|
}
|
}
|
|
@FunctionalInterface
|
private interface TenantAction<T> { T get(); }
|
|
interface TenantRoute {
|
void switchTo(String tenantId);
|
void clear();
|
}
|
}
|