package jnpf.elnService.impl;
|
|
import cn.hutool.core.util.IdUtil;
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.audit.sdk.AuditClient;
|
import jnpf.base.UserInfo;
|
import jnpf.database.util.TenantDataSourceUtil;
|
import jnpf.elnEntity.field.ElnFieldForms;
|
import jnpf.elnEntity.field.ElnFieldVersionEntity;
|
import jnpf.elnEntity.runtime.ElnRecordEntity;
|
import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity;
|
import jnpf.elnEntity.runtime.ElnRecordForms;
|
import jnpf.elnEntity.runtime.ElnRecordRevisionEntity;
|
import jnpf.elnEntity.runtime.ElnRecordVos;
|
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
|
import jnpf.elnMapper.ElnFieldVersionMapper;
|
import jnpf.elnMapper.ElnRecordFieldValueMapper;
|
import jnpf.elnMapper.ElnRecordMapper;
|
import jnpf.elnMapper.ElnRecordRevisionMapper;
|
import jnpf.elnMapper.ElnTemplateBindingMapper;
|
import jnpf.elnService.ElnTestWorkItemSnapshotProvider;
|
import jnpf.elnService.ElnCrossTemplateSnapshotResolver;
|
import jnpf.elnService.ElnRecordFormulaCalculationService;
|
import jnpf.elnService.ElnRecordSourceSyncService;
|
import jnpf.elnService.ElnRecordSourceSyncOperationCoordinator;
|
import jnpf.elnService.ElnRecordSourceSyncDocumentConfirmation;
|
import jnpf.elnService.ElnRecordFillTicketService;
|
import jnpf.exception.DataException;
|
import jnpf.lims.api.model.TestWorkItemSnapshotBatch;
|
import jnpf.lims.api.model.TestWorkItemSnapshot;
|
import jnpf.lims.api.model.TestWorkItemSnapshotProperty;
|
import jnpf.lims.api.model.ElnSourcePropertyRef;
|
import jnpf.lims.api.model.ElnSourceSnapshot;
|
import jnpf.model.tenant.TenantVO;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.UserProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.support.TransactionTemplate;
|
import org.springframework.util.StringUtils;
|
|
import java.nio.charset.StandardCharsets;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.math.BigDecimal;
|
import java.text.SimpleDateFormat;
|
import java.util.ArrayList;
|
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.Consumer;
|
|
@Service
|
public class ElnRecordSourceSyncServiceImpl implements ElnRecordSourceSyncService,
|
ElnRecordSourceSyncDocumentConfirmation {
|
|
private static final String DRAFT = "DRAFT";
|
private static final String LIMS_TASK = "LIMS_TASK";
|
private static final String CROSS_TEMPLATE = "CROSS_TEMPLATE";
|
private static final String PENDING_TEST = "pending_test";
|
|
private final ElnRecordMapper recordMapper;
|
private final ElnRecordRevisionMapper revisionMapper;
|
private final ElnRecordFieldValueMapper valueMapper;
|
private final ElnTemplateBindingMapper bindingMapper;
|
private final ElnFieldVersionMapper fieldVersionMapper;
|
private final ElnTestWorkItemSnapshotProvider taskProvider;
|
private final ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver;
|
private final ElnRecordFormulaCalculationService calculationService;
|
private final Consumer<AuditEventDTO> auditRecorder;
|
private final CurrentUserProvider currentUserProvider;
|
private final TenantRoute tenantRoute;
|
private final TransactionTemplate ticketReadTransaction;
|
private final TransactionTemplate ticketWriteTransaction;
|
private final ElnRecordSourceSyncOperationCoordinator operationCoordinator;
|
|
@Autowired
|
public ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnTemplateBindingMapper bindingMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver,
|
ElnRecordFormulaCalculationService calculationService,
|
ElnRecordSourceSyncOperationCoordinator operationCoordinator,
|
PlatformTransactionManager transactionManager,
|
ObjectProvider<AuditClient> auditClientProvider) {
|
this(recordMapper, revisionMapper, valueMapper, bindingMapper, fieldVersionMapper,
|
taskProvider, calculationService,
|
event -> {
|
AuditClient client = auditClientProvider.getIfAvailable();
|
if (client != null) client.record(event);
|
}, ElnRecordSourceSyncServiceImpl::currentUser,
|
new TenantRoute() {
|
@Override public void switchTo(String tenantId) {
|
TenantDataSourceUtil.switchTenant(tenantId);
|
}
|
@Override public void clear() {
|
TenantDataSourceUtil.clearLocalTenantInfo();
|
}
|
}, readTransaction(transactionManager), new TransactionTemplate(transactionManager),
|
crossTemplateSnapshotResolver, operationCoordinator);
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider) {
|
this(recordMapper, revisionMapper, valueMapper, null, fieldVersionMapper,
|
taskProvider, calculationService, auditRecorder, currentUserProvider,
|
new TenantRoute() {
|
@Override public void switchTo(String tenantId) { }
|
@Override public void clear() { }
|
}, null, null, ElnCrossTemplateSnapshotResolver.none(),
|
ElnRecordSourceSyncOperationCoordinator.none());
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider,
|
ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver) {
|
this(recordMapper, revisionMapper, valueMapper, null, fieldVersionMapper, taskProvider,
|
calculationService, auditRecorder, currentUserProvider,
|
crossTemplateSnapshotResolver, ElnRecordSourceSyncOperationCoordinator.none());
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnTemplateBindingMapper bindingMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider,
|
ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver,
|
ElnRecordSourceSyncOperationCoordinator operationCoordinator) {
|
this(recordMapper, revisionMapper, valueMapper, bindingMapper, fieldVersionMapper,
|
taskProvider, calculationService, auditRecorder, currentUserProvider,
|
new TenantRoute() {
|
@Override public void switchTo(String tenantId) { }
|
@Override public void clear() { }
|
}, null, null, crossTemplateSnapshotResolver, operationCoordinator);
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider,
|
TenantRoute tenantRoute,
|
TransactionTemplate ticketReadTransaction,
|
TransactionTemplate ticketWriteTransaction) {
|
this(recordMapper, revisionMapper, valueMapper, null, fieldVersionMapper, taskProvider,
|
calculationService, auditRecorder, currentUserProvider, tenantRoute,
|
ticketReadTransaction, ticketWriteTransaction,
|
ElnCrossTemplateSnapshotResolver.none(),
|
ElnRecordSourceSyncOperationCoordinator.none());
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider,
|
TenantRoute tenantRoute,
|
TransactionTemplate ticketReadTransaction,
|
TransactionTemplate ticketWriteTransaction,
|
ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver) {
|
this(recordMapper, revisionMapper, valueMapper, null, fieldVersionMapper, taskProvider,
|
calculationService, auditRecorder, currentUserProvider, tenantRoute,
|
ticketReadTransaction, ticketWriteTransaction, crossTemplateSnapshotResolver,
|
ElnRecordSourceSyncOperationCoordinator.none());
|
}
|
|
ElnRecordSourceSyncServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnTemplateBindingMapper bindingMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
CurrentUserProvider currentUserProvider,
|
TenantRoute tenantRoute,
|
TransactionTemplate ticketReadTransaction,
|
TransactionTemplate ticketWriteTransaction,
|
ElnCrossTemplateSnapshotResolver crossTemplateSnapshotResolver,
|
ElnRecordSourceSyncOperationCoordinator operationCoordinator) {
|
this.recordMapper = recordMapper;
|
this.revisionMapper = revisionMapper;
|
this.valueMapper = valueMapper;
|
this.bindingMapper = bindingMapper;
|
this.fieldVersionMapper = fieldVersionMapper;
|
this.taskProvider = taskProvider;
|
this.crossTemplateSnapshotResolver = crossTemplateSnapshotResolver;
|
this.calculationService = calculationService;
|
this.auditRecorder = auditRecorder;
|
this.currentUserProvider = currentUserProvider;
|
this.tenantRoute = tenantRoute;
|
this.ticketReadTransaction = ticketReadTransaction;
|
this.ticketWriteTransaction = ticketWriteTransaction;
|
this.operationCoordinator = operationCoordinator;
|
}
|
|
@Override
|
@Transactional(readOnly = true)
|
public ElnRecordVos.SourceSyncPreview preview(String recordId) {
|
PreparedSync prepared = prepare(required(recordId, "电子记录不能为空"),
|
currentUserProvider.get(), null);
|
return preview(prepared);
|
}
|
|
@Override
|
public ElnRecordVos.SourceSyncPreview preview(
|
ElnRecordFillTicketService.FillTicketContext ticket) {
|
requireRecordTicket(ticket);
|
String tenantId = required(ticket.getTenantId(), "填写凭证租户不能为空");
|
return inTenant(tenantId, ticketReadTransaction, () -> preview(prepare(
|
required(ticket.getBizDataId(), "填写凭证电子记录不能为空"),
|
new UserContext(tenantId,
|
required(ticket.getUserId(), "填写凭证用户不能为空")),
|
required(ticket.getFileId(), "填写凭证文档不能为空"))));
|
}
|
|
private static ElnRecordVos.SourceSyncPreview preview(PreparedSync prepared) {
|
ElnRecordVos.SourceSyncPreview result = new ElnRecordVos.SourceSyncPreview();
|
result.setRecordId(prepared.record.getId());
|
result.setRevisionId(prepared.revision.getId());
|
result.setRecordVersion(prepared.record.getFVersion());
|
result.setPreviewToken(previewToken(prepared));
|
result.setDifferences(prepared.differences());
|
result.setChangedCount(prepared.changed().size());
|
return result;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public ElnRecordVos.SourceSynchronized synchronize(
|
String recordId, ElnRecordForms.SourceSync command) {
|
return synchronize(required(recordId, "电子记录不能为空"), command,
|
currentUserProvider.get(), null);
|
}
|
|
@Override
|
public ElnRecordVos.SourceSynchronized synchronize(
|
ElnRecordFillTicketService.FillTicketContext ticket,
|
ElnRecordForms.SourceSync command) {
|
requireRecordTicket(ticket);
|
String tenantId = required(ticket.getTenantId(), "填写凭证租户不能为空");
|
return inTenant(tenantId, ticketWriteTransaction, () -> synchronize(
|
required(ticket.getBizDataId(), "填写凭证电子记录不能为空"), command,
|
new UserContext(tenantId,
|
required(ticket.getUserId(), "填写凭证用户不能为空")),
|
required(ticket.getFileId(), "填写凭证文档不能为空")));
|
}
|
|
@Override
|
public ElnRecordVos.SourceSynchronized pending(
|
ElnRecordFillTicketService.FillTicketContext ticket) {
|
requireRecordTicket(ticket);
|
String tenantId = required(ticket.getTenantId(), "填写凭证租户不能为空");
|
return inTenant(tenantId, ticketReadTransaction,
|
() -> operationCoordinator.pending(tenantId,
|
required(ticket.getUserId(), "填写凭证用户不能为空"),
|
required(ticket.getBizDataId(), "填写凭证电子记录不能为空")));
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public void confirmSavedDocument(String tenantId, String userId,
|
String recordId, String revisionId) {
|
ElnRecordVos.SourceSynchronized result = operationCoordinator.pending(
|
tenantId, userId, recordId);
|
if (result == null) return;
|
if (!Objects.equals(revisionId, result.getRevisionId())) {
|
throw new DataException("来源同步文档确认修订不匹配");
|
}
|
ElnRecordEntity record = recordMapper.selectOne(
|
Wrappers.<ElnRecordEntity>lambdaQuery()
|
.eq(ElnRecordEntity::getFTenantId, tenantId)
|
.eq(ElnRecordEntity::getId, recordId)
|
.and(q -> q.isNull(ElnRecordEntity::getFDeleteMark)
|
.or().eq(ElnRecordEntity::getFDeleteMark, 0)));
|
if (record == null || !Objects.equals(record.getDangqianXiudingId(), result.getRevisionId())) {
|
throw new DataException("来源同步文档确认修订不匹配");
|
}
|
boolean completed = operationCoordinator.confirm(
|
tenantId, userId, result.getOperationCode());
|
if (completed) audit(record, result, new UserContext(tenantId, userId));
|
}
|
|
private ElnRecordVos.SourceSynchronized synchronize(
|
String actualRecordId, ElnRecordForms.SourceSync command,
|
UserContext user, String expectedDocumentId) {
|
String operationCode = required(command == null ? null : command.getOperationCode(),
|
"重新同步操作码不能为空");
|
Integer expectedVersion = command == null ? null : command.getExpectedRecordVersion();
|
if (expectedVersion == null || expectedVersion < 0) {
|
throw new DataException("电子记录版本不能为空");
|
}
|
String expectedPreviewToken = required(
|
command == null ? null : command.getPreviewToken(),
|
"重新同步预览凭证不能为空");
|
Map<String, String> clearReasons = clearReasons(command);
|
String requestDigest = operationRequestDigest(
|
actualRecordId, expectedVersion, expectedPreviewToken, clearReasons);
|
ElnRecordVos.SourceSynchronized replay = operationCoordinator.begin(
|
user.tenantId, user.userId, operationCode, requestDigest);
|
if (replay != null) {
|
return replay;
|
}
|
if (expectedDocumentId != null
|
&& operationCoordinator.hasPendingDocumentConfirmation(
|
user.tenantId, actualRecordId)) {
|
throw new DataException("电子记录已有来源同步等待文档保存");
|
}
|
PreparedSync prepared = prepare(actualRecordId, user, expectedDocumentId);
|
if (!expectedVersion.equals(prepared.record.getFVersion())) {
|
throw new DataException("电子记录已被其他操作更新");
|
}
|
if (!MessageDigest.isEqual(
|
expectedPreviewToken.getBytes(StandardCharsets.UTF_8),
|
previewToken(prepared).getBytes(StandardCharsets.UTF_8))) {
|
throw new DataException("LIMS 任务快照已变化,请重新预览后再确认");
|
}
|
selectClears(prepared, clearReasons);
|
List<SyncCandidate> changed = prepared.changed();
|
if (changed.isEmpty()) {
|
ElnRecordVos.SourceSynchronized result = synchronizedResult(
|
prepared, operationCode, expectedVersion,
|
Collections.<ElnRecordFieldValueEntity>emptyList());
|
audit(prepared.record, result, user);
|
return operationCoordinator.complete(user.tenantId, user.userId,
|
operationCode, requestDigest,
|
result);
|
}
|
if (recordMapper.advanceVersion(user.tenantId, prepared.record.getId(),
|
expectedVersion, user.userId) != 1) {
|
throw new DataException("电子记录已被其他操作更新");
|
}
|
Set<String> valueChangedBindingIds = new LinkedHashSet<>();
|
for (SyncCandidate candidate : changed) {
|
if (candidate.newValue || !candidate.currentValue.matches(candidate.value)) {
|
valueChangedBindingIds.add(candidate.value.getMubanBangdingId());
|
}
|
apply(candidate, user.userId, prepared.fetchedTime);
|
int saved = candidate.newValue
|
? valueMapper.insert(candidate.value)
|
: candidate.configuredSnapshot == null
|
? valueMapper.updateById(candidate.value)
|
: valueMapper.updateConfiguredSnapshot(candidate.value);
|
if (saved != 1) {
|
throw new DataException("电子记录任务快照字段保存冲突");
|
}
|
}
|
List<ElnRecordFieldValueEntity> calculated = valueChangedBindingIds.isEmpty()
|
? Collections.<ElnRecordFieldValueEntity>emptyList()
|
: calculationService.recalculateAffected(
|
user.tenantId, prepared.record, prepared.revision, prepared.values,
|
valueChangedBindingIds, operationCode, user.userId);
|
for (ElnRecordFieldValueEntity value : calculated) {
|
if (valueMapper.updateById(value) != 1) {
|
throw new DataException("重新同步后的公式结果保存冲突");
|
}
|
}
|
ElnRecordVos.SourceSynchronized result = synchronizedResult(
|
prepared, operationCode, expectedVersion + 1, calculated);
|
if (expectedDocumentId != null) {
|
return operationCoordinator.prepare(user.tenantId, user.userId,
|
operationCode, requestDigest, result);
|
}
|
audit(prepared.record, result, user);
|
return operationCoordinator.complete(user.tenantId, user.userId,
|
operationCode, requestDigest, result);
|
}
|
|
private PreparedSync prepare(String recordId, UserContext user, String expectedDocumentId) {
|
ElnRecordEntity record = recordMapper.selectOne(
|
Wrappers.<ElnRecordEntity>lambdaQuery()
|
.eq(ElnRecordEntity::getFTenantId, user.tenantId)
|
.eq(ElnRecordEntity::getId, recordId)
|
.and(q -> q.isNull(ElnRecordEntity::getFDeleteMark)
|
.or().eq(ElnRecordEntity::getFDeleteMark, 0)).last("limit 1"));
|
if (record == null || !StringUtils.hasText(record.getDangqianXiudingId())) {
|
throw new DataException("电子记录不存在或当前修订无效");
|
}
|
ElnRecordRevisionEntity revision = revisionMapper.selectOne(
|
Wrappers.<ElnRecordRevisionEntity>lambdaQuery()
|
.eq(ElnRecordRevisionEntity::getFTenantId, user.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"));
|
if (revision != null && StringUtils.hasText(expectedDocumentId)
|
&& !Objects.equals(expectedDocumentId, revision.getJiluWendang())) {
|
throw new DataException("电子记录文档与当前修订不匹配");
|
}
|
if (revision == null || !DRAFT.equals(revision.getBizStatus())) {
|
throw new DataException("电子记录当前修订不允许重新同步");
|
}
|
List<ElnRecordFieldValueEntity> values = valueMapper.selectList(
|
Wrappers.<ElnRecordFieldValueEntity>lambdaQuery()
|
.eq(ElnRecordFieldValueEntity::getFTenantId, user.tenantId)
|
.eq(ElnRecordFieldValueEntity::getDianziJiluId, record.getId())
|
.eq(ElnRecordFieldValueEntity::getXiudingId, revision.getId())
|
.and(q -> q.isNull(ElnRecordFieldValueEntity::getFDeleteMark)
|
.or().eq(ElnRecordFieldValueEntity::getFDeleteMark, 0)));
|
List<VersionedValue> sourceValues = new ArrayList<>();
|
Set<String> crossTemplateBindingIds = new LinkedHashSet<>();
|
Map<String, ElnFieldVersionEntity> crossTemplateVersions = new LinkedHashMap<>();
|
Set<String> properties = new LinkedHashSet<>();
|
Set<ElnSourcePropertyRef> configuredProperties = new LinkedHashSet<>();
|
Set<String> bindingIds = new LinkedHashSet<>();
|
for (ElnRecordFieldValueEntity value : values) {
|
if (!Objects.equals(record.getMubanId(), value.getMubanId())
|
|| !bindingIds.add(value.getMubanBangdingId())) {
|
throw new DataException("电子记录字段值与固定模板不一致");
|
}
|
ElnFieldVersionEntity version = fixedVersion(user.tenantId, value);
|
ElnFieldForms.Binding binding = parseBinding(version.getBangdingPeizhi());
|
String source = binding == null || binding.getRead() == null
|
? null : binding.getRead().getSource();
|
if (CROSS_TEMPLATE.equals(value.getZhiLaiyuan())) {
|
crossTemplateBindingIds.add(value.getMubanBangdingId());
|
crossTemplateVersions.put(value.getMubanBangdingId(), version);
|
continue;
|
}
|
if (!LIMS_TASK.equals(source)) continue;
|
String property = required(binding.getRead().getProperty(),
|
"LIMS 任务快照字段缺少 property");
|
if (!LIMS_TASK.equals(value.getZhiLaiyuan())
|
|| !property.equals(value.getYuanShuxing())) {
|
throw new DataException("电子记录任务快照字段来源与固定版本不一致");
|
}
|
ElnSourcePropertyRef reference = binding.getRead().getPropertyRevision() == null
|
? null : ElnConfiguredSourceValues.reference(binding.getRead());
|
boolean initializationIncomplete = reference != null
|
&& value.getYuanShuxingBanben() == null
|
&& value.getYuanQuzhiZhuangtai() == null
|
&& !StringUtils.hasText(value.getYuanXueyuan())
|
&& value.getYuanHuoquShijian() == null;
|
if (reference != null && !initializationIncomplete
|
&& !reference.getRevision().equals(value.getYuanShuxingBanben())) {
|
throw new DataException("电子记录来源快照版本与固定字段版本不一致");
|
}
|
sourceValues.add(new VersionedValue(
|
value, version, property, reference, initializationIncomplete, false));
|
if (reference == null) properties.add(property);
|
else configuredProperties.add(reference);
|
}
|
addMissingConfiguredSourceValues(user, record, revision, values, bindingIds,
|
sourceValues, configuredProperties);
|
TestWorkItemSnapshot snapshot = snapshot(user, record,
|
new ArrayList<>(properties));
|
Map<String, TestWorkItemSnapshotProperty> byProperty = properties(snapshot);
|
List<ElnSourcePropertyRef> references = new ArrayList<>(configuredProperties);
|
Map<ElnSourcePropertyRef, ElnSourceSnapshot> configured = references.isEmpty() ? Collections.emptyMap()
|
: ElnConfiguredSourceValues.index(record.getJianyanXiangId(), references,
|
taskProvider.resolveConfiguredAsActor(user.tenantId, user.userId,
|
Collections.singletonList(record.getJianyanXiangId()), references));
|
Date fetchedTime = new Date();
|
List<SyncCandidate> candidates = new ArrayList<>();
|
for (VersionedValue sourceValue : sourceValues) {
|
if (sourceValue.reference != null) {
|
candidates.add(SyncCandidate.configured(sourceValue.value, sourceValue.version,
|
configured.get(sourceValue.reference),
|
sourceValue.initializationIncomplete, sourceValue.newValue));
|
continue;
|
}
|
TestWorkItemSnapshotProperty property = byProperty.get(sourceValue.property);
|
if (property == null || !"SUCCESS".equals(property.getOutcomeCode())) {
|
throw new DataException(property != null && StringUtils.hasText(property.getReason())
|
? property.getReason() : "LIMS 任务快照缺少字段:" + sourceValue.property);
|
}
|
if (!Objects.equals(sourceValue.version.getShujuLeixing(), property.getDataType())) {
|
throw new DataException("LIMS 任务快照字段类型与固定字段版本不一致");
|
}
|
candidates.add(SyncCandidate.lims(sourceValue.value, sourceValue.version,
|
property, fetchedTime));
|
}
|
if (!crossTemplateBindingIds.isEmpty()) {
|
Map<String, ElnRecordFieldValueEntity> sources =
|
crossTemplateSnapshotResolver.resolveByBindingIds(user.tenantId,
|
record.getJianyanXiangId(), crossTemplateBindingIds);
|
for (ElnRecordFieldValueEntity targetValue : values) {
|
if (!CROSS_TEMPLATE.equals(targetValue.getZhiLaiyuan())) continue;
|
ElnRecordFieldValueEntity source = sources.get(targetValue.getMubanBangdingId());
|
if (source == null) {
|
throw new DataException("跨模板字段来源规则无效或源记录不可用");
|
}
|
candidates.add(SyncCandidate.crossTemplate(targetValue,
|
crossTemplateVersions.get(targetValue.getMubanBangdingId()), source, fetchedTime));
|
}
|
}
|
return new PreparedSync(record, revision, values, candidates, fetchedTime);
|
}
|
|
private void addMissingConfiguredSourceValues(UserContext user, ElnRecordEntity record,
|
ElnRecordRevisionEntity revision, List<ElnRecordFieldValueEntity> values,
|
Set<String> bindingIds, List<VersionedValue> sourceValues,
|
Set<ElnSourcePropertyRef> configuredProperties) {
|
if (bindingMapper == null) return;
|
List<ElnTemplateBindingEntity> bindings = bindingMapper.selectList(
|
Wrappers.<ElnTemplateBindingEntity>lambdaQuery()
|
.eq(ElnTemplateBindingEntity::getFTenantId, user.tenantId)
|
.eq(ElnTemplateBindingEntity::getMubanId, record.getMubanId())
|
.eq(ElnTemplateBindingEntity::getBangdingZhuangtai, "ACTIVE")
|
.eq(ElnTemplateBindingEntity::getKongjianJuese, "PRIMARY")
|
.and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))
|
.orderByAsc(ElnTemplateBindingEntity::getId));
|
if (bindings == null) return;
|
for (ElnTemplateBindingEntity binding : bindings) {
|
if (binding == null || bindingIds.contains(binding.getId())) continue;
|
ElnRecordFieldValueEntity value = missingValue(user, record, revision, binding);
|
ElnFieldVersionEntity version = fixedVersion(user.tenantId, value);
|
ElnFieldForms.Binding config = parseBinding(version.getBangdingPeizhi());
|
if (config == null || config.getRead() == null
|
|| !LIMS_TASK.equals(config.getRead().getSource())
|
|| config.getRead().getPropertyRevision() == null) {
|
continue;
|
}
|
ElnSourcePropertyRef reference = ElnConfiguredSourceValues.reference(config.getRead());
|
values.add(value);
|
bindingIds.add(binding.getId());
|
sourceValues.add(new VersionedValue(value, version, reference.getPropertyCode(),
|
reference, true, true));
|
configuredProperties.add(reference);
|
}
|
}
|
|
private static ElnRecordFieldValueEntity missingValue(UserContext user,
|
ElnRecordEntity record, ElnRecordRevisionEntity revision,
|
ElnTemplateBindingEntity binding) {
|
ElnRecordFieldValueEntity value = new ElnRecordFieldValueEntity();
|
value.setId(IdUtil.getSnowflakeNextIdStr());
|
value.setDianziJiluId(record.getId());
|
value.setXiudingId(revision.getId());
|
value.setMubanId(record.getMubanId());
|
value.setMubanBangdingId(binding.getId());
|
value.setZiduanDingyiId(binding.getZiduanDingyiId());
|
value.setZiduanBianma(binding.getZiduanBianma());
|
value.setZiduanBanbenId(binding.getZiduanBanbenId());
|
value.setBangdingJuese("PRIMARY");
|
value.setBangdingZhuangtai("ACTIVE");
|
value.setFTenantId(user.tenantId);
|
value.setFVersion(0);
|
value.setFDeleteMark(0);
|
value.setFCreatorUserId(user.userId);
|
value.setFLastModifyUserId(user.userId);
|
return value;
|
}
|
|
private static Map<String, String> clearReasons(ElnRecordForms.SourceSync command) {
|
Map<String, String> result = new java.util.TreeMap<>();
|
if (command.getClearReasons() == null) return result;
|
command.getClearReasons().forEach((bindingId, reason) -> {
|
String actualReason = required(reason, "清空来源快照必须填写原因");
|
if (actualReason.length() > 500) throw new DataException("清空原因不能超过 500 字");
|
result.put(required(bindingId, "清空字段绑定不能为空"), actualReason);
|
});
|
if (result.size() != command.getClearReasons().size()) throw new DataException("清空字段绑定重复");
|
return result;
|
}
|
|
private static void selectClears(PreparedSync prepared, Map<String, String> reasons) {
|
for (Map.Entry<String, String> entry : reasons.entrySet()) {
|
SyncCandidate candidate = prepared.candidates.stream()
|
.filter(item -> entry.getKey().equals(item.value.getMubanBangdingId())).findFirst()
|
.orElseThrow(() -> new DataException("清空字段不属于本次来源同步"));
|
if (!candidate.difference.isCanClear()) throw new DataException("本次来源状态不允许清空该字段");
|
candidate.currentValue = ValueSlots.text(null);
|
candidate.difference.setCurrentValue(null);
|
candidate.difference.setCurrentFetchedTime(candidate.difference.getSourceFetchedAt());
|
candidate.difference.setClearReason(entry.getValue());
|
candidate.difference.setChanged(true);
|
}
|
}
|
|
private ElnFieldVersionEntity fixedVersion(
|
String tenantId, ElnRecordFieldValueEntity value) {
|
if (!"PRIMARY".equals(value.getBangdingJuese())
|
|| !"ACTIVE".equals(value.getBangdingZhuangtai())) {
|
throw new DataException("电子记录字段值不是活动主绑定");
|
}
|
ElnFieldVersionEntity version = fieldVersionMapper.selectOne(
|
Wrappers.<ElnFieldVersionEntity>lambdaQuery()
|
.eq(ElnFieldVersionEntity::getFTenantId, tenantId)
|
.eq(ElnFieldVersionEntity::getId, value.getZiduanBanbenId())
|
.and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark)
|
.or().eq(ElnFieldVersionEntity::getFDeleteMark, 0)).last("limit 1"));
|
if (version == null || !Objects.equals(value.getZiduanDingyiId(), version.getZiduanDingyiId())) {
|
throw new DataException("电子记录固定字段版本无效");
|
}
|
return version;
|
}
|
|
private TestWorkItemSnapshot snapshot(
|
UserContext user, ElnRecordEntity record, List<String> properties) {
|
TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor(
|
user.tenantId, user.userId,
|
Collections.singletonList(record.getJianyanXiangId()), properties);
|
if (batch == null || batch.getItems() == null || batch.getItems().size() != 1) {
|
throw new DataException("LIMS 检验任务快照返回无效");
|
}
|
TestWorkItemSnapshot item = batch.getItems().get(0);
|
if (item == null || !record.getJianyanXiangId().equals(item.getTestWorkItemId())
|
|| !"SUCCESS".equals(item.getOutcomeCode())
|
|| !PENDING_TEST.equals(item.getTaskStatus())) {
|
throw new DataException(item != null && StringUtils.hasText(item.getReason())
|
? item.getReason() : "当前用户或检验项状态不允许重新同步电子记录");
|
}
|
return item;
|
}
|
|
private static Map<String, TestWorkItemSnapshotProperty> properties(
|
TestWorkItemSnapshot item) {
|
Map<String, TestWorkItemSnapshotProperty> result = new LinkedHashMap<>();
|
if (item.getProperties() == null) return result;
|
for (TestWorkItemSnapshotProperty property : item.getProperties()) {
|
if (property == null || !StringUtils.hasText(property.getProperty())) continue;
|
if (result.put(property.getProperty(), property) != null) {
|
throw new DataException("LIMS 任务快照返回重复字段");
|
}
|
}
|
return result;
|
}
|
|
private static void apply(SyncCandidate candidate, String userId, Date fetchedTime) {
|
candidate.currentValue.apply(candidate.value);
|
candidate.value.setZhiLaiyuan(candidate.sourceType);
|
candidate.value.setYuanShuxing(candidate.sourceProperty);
|
candidate.value.setYuanYinyongLeixing(candidate.sourceType);
|
candidate.value.setYuanYinyongId(candidate.sourceRef);
|
candidate.value.setYuanHuoquShijian(fetchedTime);
|
if (candidate.configuredSnapshot != null) {
|
ElnConfiguredSourceValues.apply(candidate.value, candidate.version, candidate.configuredSnapshot);
|
if (candidate.difference.getClearReason() != null) candidate.value.setYuanQuzhiZhuangtai("CLEARED");
|
}
|
if (CROSS_TEMPLATE.equals(candidate.sourceType)) {
|
candidate.value.setKuamoLaiyuanJiluId(candidate.source.getDianziJiluId());
|
candidate.value.setKuamoLaiyuanXiudingId(candidate.source.getXiudingId());
|
candidate.value.setKuamoLaiyuanZhiId(candidate.source.getId());
|
candidate.value.setKuamoLaiyuanZiduanBanbenId(candidate.source.getZiduanBanbenId());
|
candidate.value.setKuamoHuoquShijian(fetchedTime);
|
}
|
candidate.value.setFLastModifyUserId(userId);
|
candidate.value.setFLastModifyTime(fetchedTime);
|
}
|
|
private void audit(ElnRecordEntity record, ElnRecordVos.SourceSynchronized result,
|
UserContext user) {
|
auditRecorder.accept(AuditEventDTO.builder()
|
.clientEventId(result.getOperationCode())
|
.operationId(result.getOperationCode())
|
.tenantId(user.tenantId)
|
.operatorId(user.userId)
|
.eventTime(auditEventTime(result))
|
.eventType(result.getChangedCount() == 0 ? "BIZ_ACTION" : "DATA_CHANGE")
|
.actionCode("ELN_RECORD_SOURCE_SYNC")
|
.actionLabel("重新同步电子记录任务快照")
|
.bizModule("eln")
|
.bizType("eln_record")
|
.bizCode(record.getJiluBianhao())
|
.targetTable("eln_record")
|
.targetId(record.getId())
|
.fieldDiffs(auditFieldDiffs(result.getDifferences()))
|
.dataSnapshot(JsonUtil.getObjectToString(result.getDifferences()))
|
.build());
|
}
|
|
private static Date auditEventTime(ElnRecordVos.SourceSynchronized result) {
|
for (ElnRecordVos.SourceSyncDifference difference : result.getDifferences()) {
|
if (difference.getCurrentFetchedTime() != null) {
|
return difference.getCurrentFetchedTime();
|
}
|
}
|
return new Date();
|
}
|
|
private static String auditFieldDiffs(
|
List<ElnRecordVos.SourceSyncDifference> differences) {
|
List<Map<String, Object>> diffs = new ArrayList<>();
|
for (ElnRecordVos.SourceSyncDifference source : differences) {
|
if (!source.isChanged()) continue;
|
Map<String, Object> diff = new LinkedHashMap<>();
|
String valueColumn = "zhi_wenben";
|
if ("DECIMAL".equalsIgnoreCase(source.getDataType())) valueColumn = "zhi_shuzhi";
|
else if ("DATE".equalsIgnoreCase(source.getDataType())
|
|| "DATETIME".equalsIgnoreCase(source.getDataType())) valueColumn = "zhi_riqi";
|
diff.put("field", valueColumn);
|
diff.put("fieldName", source.getFieldName());
|
diff.put("oldData", source.getPreviousValue() == null ? "" : source.getPreviousValue());
|
diff.put("newData", source.getCurrentValue() == null ? "" : source.getCurrentValue());
|
diff.put("type", source.getPreviousValue() == null || source.getPreviousValue().isEmpty() ? 0 : 1);
|
diff.put("targetTable", "eln_record_field_value");
|
diff.put("targetId", source.getFieldValueId());
|
diff.put("elnFieldCode", source.getFieldCode());
|
diff.put("sourceType", source.getSourceType());
|
diff.put("sourceProperty", source.getSourceProperty());
|
if (source.getSourcePropertyRevision() != null) {
|
diff.put("sourcePropertyRevision", source.getSourcePropertyRevision());
|
diff.put("sourceOutcome", source.getSourceOutcome());
|
diff.put("sourceReason", source.getSourceReason());
|
diff.put("sourceLineage", source.getSourceLineage());
|
diff.put("clearReason", source.getClearReason());
|
}
|
diff.put("sourceRecordId", source.getSourceRecordId());
|
diff.put("sourceRevisionId", source.getSourceRevisionId());
|
diff.put("sourceValueId", source.getSourceValueId());
|
diff.put("sourceFieldVersionId", source.getSourceFieldVersionId());
|
diffs.add(diff);
|
}
|
return JsonUtil.getObjectToString(diffs);
|
}
|
|
private static ElnRecordVos.SourceSynchronized synchronizedResult(
|
PreparedSync prepared, String operationCode, int recordVersion,
|
List<ElnRecordFieldValueEntity> calculated) {
|
ElnRecordVos.SourceSynchronized result = new ElnRecordVos.SourceSynchronized();
|
result.setRecordId(prepared.record.getId());
|
result.setRevisionId(prepared.revision.getId());
|
result.setOperationCode(operationCode);
|
result.setRecordVersion(recordVersion);
|
result.setDifferences(prepared.differences());
|
result.setChangedCount(prepared.changed().size());
|
result.setRecalculatedCount(calculated.size());
|
Set<String> changedFieldCodes = new LinkedHashSet<>();
|
for (SyncCandidate candidate : prepared.changed()) {
|
changedFieldCodes.add(candidate.difference.getFieldCode());
|
}
|
for (ElnRecordFieldValueEntity value : calculated) {
|
if (StringUtils.hasText(value.getZiduanBianma())) {
|
changedFieldCodes.add(value.getZiduanBianma());
|
}
|
}
|
result.setChangedFieldCodes(new ArrayList<>(changedFieldCodes));
|
return result;
|
}
|
|
private static ElnFieldForms.Binding parseBinding(String json) {
|
try {
|
return StringUtils.hasText(json)
|
? JsonUtil.getJsonToBean(json, ElnFieldForms.Binding.class) : null;
|
} catch (RuntimeException e) {
|
throw new DataException("字段绑定配置无效");
|
}
|
}
|
|
private static String previewToken(PreparedSync prepared) {
|
try {
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
updateDigest(digest, prepared.record.getId());
|
updateDigest(digest, prepared.revision.getId());
|
updateDigest(digest, String.valueOf(prepared.record.getFVersion()));
|
List<SyncCandidate> candidates = new ArrayList<>(prepared.candidates);
|
candidates.sort(Comparator
|
.comparing((SyncCandidate candidate) -> candidate.value.getMubanBangdingId())
|
.thenComparing(candidate -> candidate.sourceType));
|
for (SyncCandidate candidate : candidates) {
|
ElnRecordVos.SourceSyncDifference difference = candidate.difference;
|
updateDigest(digest, difference.getBindingId());
|
updateDigest(digest, difference.getSourceProperty());
|
updateDigest(digest, difference.getDataType());
|
updateDigest(digest, difference.getPreviousValue());
|
updateDigest(digest, difference.getCurrentValue());
|
updateDigest(digest, difference.getPreviousSourceType());
|
updateDigest(digest, difference.getCurrentSourceType());
|
updateDigest(digest, difference.getPreviousSourceRef());
|
updateDigest(digest, difference.getCurrentSourceRef());
|
updateDigest(digest, difference.getSourceType());
|
updateDigest(digest, difference.getSourceRecordId());
|
updateDigest(digest, difference.getSourceRevisionId());
|
updateDigest(digest, difference.getSourceValueId());
|
updateDigest(digest, difference.getSourceFieldVersionId());
|
if (candidate.configuredSnapshot != null) {
|
updateDigest(digest, String.valueOf(difference.getSourcePropertyRevision()));
|
updateDigest(digest, difference.getSourceOutcome());
|
updateDigest(digest, difference.getSourceReason());
|
updateDigest(digest, difference.getSourceLineage());
|
updateDigest(digest, candidate.value.getYuanQuzhiZhuangtai());
|
updateDigest(digest, candidate.value.getYuanXueyuan());
|
updateDigest(digest, String.valueOf(candidate.value.getYuanHuoquShijian() == null
|
? null : candidate.value.getYuanHuoquShijian().getTime()));
|
}
|
}
|
return toHex(digest.digest());
|
} catch (NoSuchAlgorithmException e) {
|
throw new IllegalStateException("JVM 不支持 SHA-256", e);
|
}
|
}
|
|
private static String operationRequestDigest(
|
String recordId, Integer expectedVersion, String previewToken, Map<String, String> clearReasons) {
|
try {
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
updateDigest(digest, "ELN_RECORD_SOURCE_SYNC");
|
updateDigest(digest, recordId);
|
updateDigest(digest, String.valueOf(expectedVersion));
|
updateDigest(digest, previewToken);
|
for (Map.Entry<String, String> entry : clearReasons.entrySet()) {
|
updateDigest(digest, entry.getKey());
|
updateDigest(digest, entry.getValue());
|
}
|
return toHex(digest.digest());
|
} catch (NoSuchAlgorithmException e) {
|
throw new DataException("来源同步请求摘要生成失败");
|
}
|
}
|
|
private static void updateDigest(MessageDigest digest, String value) {
|
byte[] bytes = value == null ? new byte[0] : value.getBytes(StandardCharsets.UTF_8);
|
digest.update((byte) (bytes.length >>> 24));
|
digest.update((byte) (bytes.length >>> 16));
|
digest.update((byte) (bytes.length >>> 8));
|
digest.update((byte) bytes.length);
|
digest.update(bytes);
|
}
|
|
private static String toHex(byte[] bytes) {
|
StringBuilder result = new StringBuilder(bytes.length * 2);
|
for (byte value : bytes) {
|
result.append(Character.forDigit((value >>> 4) & 0x0f, 16));
|
result.append(Character.forDigit(value & 0x0f, 16));
|
}
|
return result.toString();
|
}
|
|
private static String required(String value, String message) {
|
if (!StringUtils.hasText(value)) throw new DataException(message);
|
return value.trim();
|
}
|
|
private <T> T inTenant(String tenantId, TransactionTemplate transaction,
|
TenantAction<T> action) {
|
try {
|
tenantRoute.switchTo(tenantId);
|
return transaction == null ? action.get() : transaction.execute(status -> action.get());
|
} finally {
|
tenantRoute.clear();
|
}
|
}
|
|
private static TransactionTemplate readTransaction(
|
PlatformTransactionManager transactionManager) {
|
TransactionTemplate transaction = new TransactionTemplate(transactionManager);
|
transaction.setReadOnly(true);
|
return transaction;
|
}
|
|
private static void requireRecordTicket(
|
ElnRecordFillTicketService.FillTicketContext ticket) {
|
if (ticket == null
|
|| !ElnRecordFillTicketService.BIZ_MODULE.equals(ticket.getBizModule())
|
|| !ElnRecordFillTicketService.BIZ_SCENE.equals(ticket.getBizScene())) {
|
throw new DataException("电子记录填写凭证无效");
|
}
|
}
|
|
private static UserContext currentUser() {
|
UserInfo user = UserProvider.getUser();
|
if (user == null || !StringUtils.hasText(user.getUserId())) {
|
throw new DataException("当前登录用户无效");
|
}
|
TenantVO routed = TenantDataSourceUtil.getTenantInfo();
|
String tenantId = routed != null && StringUtils.hasText(routed.getEnCode())
|
? routed.getEnCode().trim() : user.getTenantId();
|
return new UserContext(StringUtils.hasText(tenantId) ? tenantId.trim() : "0",
|
user.getUserId().trim());
|
}
|
|
@FunctionalInterface
|
interface CurrentUserProvider {
|
UserContext get();
|
}
|
|
@FunctionalInterface
|
interface TenantAction<T> {
|
T get();
|
}
|
|
interface TenantRoute {
|
void switchTo(String tenantId);
|
void clear();
|
}
|
|
static final class UserContext {
|
private final String tenantId;
|
private final String userId;
|
|
UserContext(String tenantId, String userId) {
|
this.tenantId = tenantId;
|
this.userId = userId;
|
}
|
}
|
|
private static final class VersionedValue {
|
private final ElnRecordFieldValueEntity value;
|
private final ElnFieldVersionEntity version;
|
private final String property;
|
private final ElnSourcePropertyRef reference;
|
private final boolean initializationIncomplete;
|
private final boolean newValue;
|
|
private VersionedValue(ElnRecordFieldValueEntity value,
|
ElnFieldVersionEntity version, String property,
|
ElnSourcePropertyRef reference, boolean initializationIncomplete,
|
boolean newValue) {
|
this.value = value;
|
this.version = version;
|
this.property = property;
|
this.reference = reference;
|
this.initializationIncomplete = initializationIncomplete;
|
this.newValue = newValue;
|
}
|
}
|
|
private static final class SyncCandidate {
|
private final ElnRecordFieldValueEntity value;
|
private final ElnFieldVersionEntity version;
|
private final String sourceType;
|
private final String sourceProperty;
|
private final String sourceRef;
|
private ValueSlots currentValue;
|
private final ElnRecordFieldValueEntity source;
|
private final ElnRecordVos.SourceSyncDifference difference;
|
private ElnSourceSnapshot configuredSnapshot;
|
private boolean newValue;
|
|
private SyncCandidate(ElnRecordFieldValueEntity value, ElnFieldVersionEntity version,
|
String sourceType, String sourceProperty, String sourceRef,
|
ValueSlots currentValue, ElnRecordFieldValueEntity source,
|
Date fetchedTime) {
|
this.value = value;
|
this.version = version;
|
this.sourceType = sourceType;
|
this.sourceProperty = sourceProperty;
|
this.sourceRef = sourceRef;
|
this.currentValue = currentValue;
|
this.source = source;
|
this.difference = difference(value, version, sourceType, sourceProperty, sourceRef,
|
currentValue, source, fetchedTime);
|
}
|
|
private static SyncCandidate lims(ElnRecordFieldValueEntity value,
|
ElnFieldVersionEntity version, TestWorkItemSnapshotProperty property,
|
Date fetchedTime) {
|
return new SyncCandidate(value, version, LIMS_TASK, property.getProperty(),
|
property.getSourceRef(), ValueSlots.text(property.getValue()), null, fetchedTime);
|
}
|
|
private static SyncCandidate configured(ElnRecordFieldValueEntity value,
|
ElnFieldVersionEntity version, ElnSourceSnapshot snapshot,
|
boolean initializationIncomplete, boolean newValue) {
|
ElnRecordFieldValueEntity resolved = new ElnRecordFieldValueEntity();
|
ElnConfiguredSourceValues.apply(resolved, version, snapshot);
|
boolean missing = !"SUCCESS".equals(snapshot.getOutcomeCode());
|
SyncCandidate candidate = new SyncCandidate(value, version, LIMS_TASK, snapshot.getPropertyCode(),
|
snapshot.getSourceRef(), ValueSlots.of(
|
missing && !initializationIncomplete ? value : resolved, version), null,
|
missing && !initializationIncomplete
|
? value.getYuanHuoquShijian() : resolved.getYuanHuoquShijian());
|
candidate.configuredSnapshot = snapshot;
|
candidate.newValue = newValue;
|
candidate.difference.setSourcePropertyRevision(snapshot.getPropertyRevision());
|
candidate.difference.setSourceOutcome(snapshot.getOutcomeCode());
|
candidate.difference.setSourceReason(snapshot.getReasonCode());
|
candidate.difference.setSourceLineage(snapshot.getLineage());
|
candidate.difference.setSourceFetchedAt(resolved.getYuanHuoquShijian());
|
candidate.difference.setCanClear(missing);
|
if (missing && !initializationIncomplete) candidate.difference.setChanged(false);
|
if (initializationIncomplete) candidate.difference.setChanged(true);
|
return candidate;
|
}
|
|
private static SyncCandidate crossTemplate(ElnRecordFieldValueEntity value,
|
ElnFieldVersionEntity version, ElnRecordFieldValueEntity source, Date fetchedTime) {
|
return new SyncCandidate(value, version, CROSS_TEMPLATE, null, source.getId(),
|
ValueSlots.of(source, version), source, fetchedTime);
|
}
|
|
private static ElnRecordVos.SourceSyncDifference difference(
|
ElnRecordFieldValueEntity value, ElnFieldVersionEntity version,
|
String sourceType, String sourceProperty, String sourceRef,
|
ValueSlots currentValue, ElnRecordFieldValueEntity source, Date fetchedTime) {
|
ElnRecordVos.SourceSyncDifference result = new ElnRecordVos.SourceSyncDifference();
|
result.setBindingId(value.getMubanBangdingId());
|
result.setFieldValueId(value.getId());
|
result.setFieldCode(value.getZiduanBianma());
|
result.setFieldName(version.getZiduanMingcheng());
|
result.setDataType(version.getShujuLeixing());
|
result.setSourceType(sourceType);
|
result.setSourceProperty(sourceProperty);
|
result.setPreviousValue(ValueSlots.of(value, version).display(version));
|
result.setCurrentValue(currentValue.display(version));
|
result.setPreviousSourceType(value.getYuanYinyongLeixing());
|
result.setCurrentSourceType(sourceType);
|
result.setPreviousSourceRef(value.getYuanYinyongId());
|
result.setCurrentSourceRef(sourceRef);
|
result.setPreviousFetchedTime(value.getYuanHuoquShijian());
|
result.setCurrentFetchedTime(fetchedTime);
|
if (source != null) {
|
result.setSourceRecordId(source.getDianziJiluId());
|
result.setSourceRevisionId(source.getXiudingId());
|
result.setSourceValueId(source.getId());
|
result.setSourceFieldVersionId(source.getZiduanBanbenId());
|
result.setSourceFetchedAt(fetchedTime);
|
}
|
result.setChanged(!currentValue.matches(value)
|
|| !Objects.equals(result.getPreviousSourceType(), result.getCurrentSourceType())
|
|| !Objects.equals(result.getPreviousSourceRef(), result.getCurrentSourceRef())
|
|| (source != null && (!Objects.equals(value.getKuamoLaiyuanJiluId(),
|
source.getDianziJiluId())
|
|| !Objects.equals(value.getKuamoLaiyuanXiudingId(), source.getXiudingId())
|
|| !Objects.equals(value.getKuamoLaiyuanZhiId(), source.getId())
|
|| !Objects.equals(value.getKuamoLaiyuanZiduanBanbenId(),
|
source.getZiduanBanbenId()))));
|
return result;
|
}
|
}
|
|
private static final class ValueSlots {
|
private final String text;
|
private final BigDecimal number;
|
private final Date date;
|
|
private ValueSlots(String text, BigDecimal number, Date date) {
|
this.text = text;
|
this.number = number;
|
this.date = date;
|
}
|
|
private static ValueSlots text(String value) {
|
return new ValueSlots(value, null, null);
|
}
|
|
private static ValueSlots of(ElnRecordFieldValueEntity value, ElnFieldVersionEntity version) {
|
String type = version == null || version.getShujuLeixing() == null
|
? "TEXT" : version.getShujuLeixing().trim().toUpperCase();
|
if ("DECIMAL".equals(type)) return new ValueSlots(null, value.getZhiShuzhi(), null);
|
if ("DATE".equals(type) || "DATETIME".equals(type)) {
|
return new ValueSlots(null, null, value.getZhiRiqi());
|
}
|
return text(value.getZhiWenben());
|
}
|
|
private boolean matches(ElnRecordFieldValueEntity value) {
|
return Objects.equals(text, value.getZhiWenben())
|
&& Objects.equals(number, value.getZhiShuzhi())
|
&& Objects.equals(date, value.getZhiRiqi());
|
}
|
|
private void apply(ElnRecordFieldValueEntity value) {
|
value.setZhiWenben(text);
|
value.setZhiShuzhi(number);
|
value.setZhiRiqi(date);
|
}
|
|
private String display(ElnFieldVersionEntity version) {
|
if (number != null) return number.toPlainString();
|
if (date != null) {
|
return new SimpleDateFormat("DATETIME".equalsIgnoreCase(
|
version == null ? null : version.getShujuLeixing())
|
? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd").format(date);
|
}
|
return text;
|
}
|
}
|
|
private static final class PreparedSync {
|
private final ElnRecordEntity record;
|
private final ElnRecordRevisionEntity revision;
|
private final List<ElnRecordFieldValueEntity> values;
|
private final List<SyncCandidate> candidates;
|
private final Date fetchedTime;
|
|
private PreparedSync(ElnRecordEntity record, ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> values,
|
List<SyncCandidate> candidates, Date fetchedTime) {
|
this.record = record;
|
this.revision = revision;
|
this.values = values;
|
this.candidates = candidates;
|
this.fetchedTime = fetchedTime;
|
}
|
|
private List<SyncCandidate> changed() {
|
List<SyncCandidate> result = new ArrayList<>();
|
for (SyncCandidate candidate : candidates) {
|
if (candidate.difference.isChanged()) result.add(candidate);
|
}
|
return result;
|
}
|
|
private List<ElnRecordVos.SourceSyncDifference> differences() {
|
List<ElnRecordVos.SourceSyncDifference> result = new ArrayList<>();
|
for (SyncCandidate candidate : candidates) result.add(candidate.difference);
|
return result;
|
}
|
}
|
}
|