package jnpf.elnService.impl;
|
|
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.ElnFieldEnums;
|
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.elnMapper.ElnFieldVersionMapper;
|
import jnpf.elnMapper.ElnRecordFieldValueMapper;
|
import jnpf.elnMapper.ElnRecordMapper;
|
import jnpf.elnMapper.ElnRecordRevisionMapper;
|
import jnpf.elnService.ElnTestWorkItemSnapshotProvider;
|
import jnpf.elnService.ElnRecordFormulaCalculationService;
|
import jnpf.elnService.ElnRecordSharedFillService;
|
import jnpf.elnService.template.ElnTemplateBindingConfirmationFacade;
|
import jnpf.exception.DataException;
|
import jnpf.lims.api.model.TestWorkItemSnapshotBatch;
|
import jnpf.lims.api.model.TestWorkItemSnapshot;
|
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.annotation.Transactional;
|
import org.springframework.util.StringUtils;
|
import org.slf4j.Logger;
|
import org.slf4j.LoggerFactory;
|
|
import java.nio.charset.StandardCharsets;
|
import java.math.BigDecimal;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
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.Date;
|
import java.util.function.Consumer;
|
import java.util.function.Supplier;
|
|
@Service
|
public class ElnRecordSharedFillServiceImpl implements ElnRecordSharedFillService {
|
|
private static final Logger LOG = LoggerFactory.getLogger(ElnRecordSharedFillServiceImpl.class);
|
private static final String DRAFT = "DRAFT";
|
private static final int MAX_OPERATION_CODE_LENGTH = 64;
|
private static final int MAX_AUDIT_CLIENT_EVENT_ID_LENGTH = 70;
|
|
private final ElnRecordMapper recordMapper;
|
private final ElnRecordRevisionMapper revisionMapper;
|
private final ElnRecordFieldValueMapper valueMapper;
|
private final ElnFieldVersionMapper fieldVersionMapper;
|
private final ElnTestWorkItemSnapshotProvider taskProvider;
|
private final ElnRecordFormulaCalculationService calculationService;
|
private final Consumer<AuditEventDTO> auditRecorder;
|
private final TargetTransactionExecutor targetTransactionExecutor;
|
private final DocumentSynchronizer documentSynchronizer;
|
private final OperationCoordinator operationCoordinator;
|
private final CurrentUserProvider currentUserProvider;
|
|
@Autowired
|
public ElnRecordSharedFillServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
ObjectProvider<AuditClient> auditClientProvider,
|
ElnTemplateBindingConfirmationFacade confirmationFacade,
|
ElnRecordSharedFillDocumentService documentService,
|
ElnRecordSharedFillOperationService operationService) {
|
this(recordMapper, revisionMapper, valueMapper, fieldVersionMapper,
|
taskProvider, calculationService,
|
event -> {
|
AuditClient client = auditClientProvider.getIfAvailable();
|
if (client != null) client.record(event);
|
}, confirmationFacade::execute,
|
documentService::synchronize,
|
operationService,
|
ElnRecordSharedFillServiceImpl::currentUser);
|
}
|
|
ElnRecordSharedFillServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
CurrentUserProvider currentUserProvider) {
|
this(recordMapper, revisionMapper, valueMapper, fieldVersionMapper,
|
taskProvider, null, ignored -> { }, null,
|
(tenantId, record, revision, values, operationCode) -> { },
|
new InMemoryOperationCoordinator(),
|
currentUserProvider);
|
}
|
|
ElnRecordSharedFillServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
TargetTransactionExecutor targetTransactionExecutor,
|
CurrentUserProvider currentUserProvider) {
|
this(recordMapper, revisionMapper, valueMapper, fieldVersionMapper,
|
taskProvider, calculationService, auditRecorder,
|
targetTransactionExecutor,
|
(tenantId, record, revision, values, operationCode) -> { },
|
new InMemoryOperationCoordinator(),
|
currentUserProvider);
|
}
|
|
ElnRecordSharedFillServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
TargetTransactionExecutor targetTransactionExecutor,
|
DocumentSynchronizer documentSynchronizer,
|
CurrentUserProvider currentUserProvider) {
|
this(recordMapper, revisionMapper, valueMapper, fieldVersionMapper,
|
taskProvider, calculationService, auditRecorder,
|
targetTransactionExecutor, documentSynchronizer,
|
new InMemoryOperationCoordinator(), currentUserProvider);
|
}
|
|
ElnRecordSharedFillServiceImpl(
|
ElnRecordMapper recordMapper,
|
ElnRecordRevisionMapper revisionMapper,
|
ElnRecordFieldValueMapper valueMapper,
|
ElnFieldVersionMapper fieldVersionMapper,
|
ElnTestWorkItemSnapshotProvider taskProvider,
|
ElnRecordFormulaCalculationService calculationService,
|
Consumer<AuditEventDTO> auditRecorder,
|
TargetTransactionExecutor targetTransactionExecutor,
|
DocumentSynchronizer documentSynchronizer,
|
OperationCoordinator operationCoordinator,
|
CurrentUserProvider currentUserProvider) {
|
this.recordMapper = recordMapper;
|
this.revisionMapper = revisionMapper;
|
this.valueMapper = valueMapper;
|
this.fieldVersionMapper = fieldVersionMapper;
|
this.taskProvider = taskProvider;
|
this.calculationService = calculationService;
|
this.auditRecorder = auditRecorder;
|
this.targetTransactionExecutor = targetTransactionExecutor;
|
this.documentSynchronizer = documentSynchronizer;
|
this.operationCoordinator = operationCoordinator;
|
this.currentUserProvider = currentUserProvider;
|
}
|
|
@Override
|
@Transactional(readOnly = true)
|
public ElnRecordVos.SharedFillPreview preview(ElnRecordForms.SharedFillPreview command) {
|
List<String> itemIds = testWorkItemIds(command);
|
UserContext user = currentUserProvider.get();
|
TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor(
|
user.tenantId, user.userId, itemIds, Collections.<String>emptyList());
|
List<TestWorkItemSnapshot> snapshots = snapshots(batch, itemIds);
|
ProjectReference project = commonProject(snapshots);
|
List<PreparedTarget> targets = new ArrayList<>();
|
for (TestWorkItemSnapshot snapshot : snapshots) {
|
targets.add("SUCCESS".equals(snapshot.getOutcomeCode())
|
? prepareTarget(user, snapshot) : rejectedTarget(snapshot));
|
}
|
Map<String, ElnFieldVersionEntity> groupedFields = groupedFields(targets);
|
ElnRecordVos.SharedFillPreview result = new ElnRecordVos.SharedFillPreview();
|
result.setTestDefinitionId(project == null ? null : project.id);
|
result.setTestDefinitionCode(project == null ? null : project.code);
|
result.setTestDefinitionName(project == null ? null : project.name);
|
for (ElnFieldVersionEntity version : groupedFields.values()) {
|
result.getFields().add(field(version, firstValue(targets, version.getId()), targets));
|
}
|
for (PreparedTarget target : targets) {
|
result.getTargets().add(toSharedTarget(target, groupedFields.keySet()));
|
}
|
result.setPreviewToken(previewToken(user, result));
|
return result;
|
}
|
|
@Override
|
public ElnRecordVos.SharedFilled execute(ElnRecordForms.SharedFillExecute command) {
|
SharedFillExecution execution = validateExecution(command);
|
UserContext user = currentUserProvider.get();
|
String requestDigest = executionDigest(user, execution);
|
OperationStart operation = operationCoordinator.begin(
|
user.tenantId, user.userId, execution.operationCode, requestDigest);
|
if (operation.replayed != null) return operation.replayed;
|
Map<String, ElnRecordVos.SharedTarget> previewTargets = new LinkedHashMap<>();
|
Map<String, ElnRecordVos.SharedField> sharedFields = new LinkedHashMap<>();
|
if (!operation.resuming) {
|
ElnRecordForms.SharedFillPreview previewCommand =
|
new ElnRecordForms.SharedFillPreview();
|
previewCommand.setTestWorkItemIds(execution.testWorkItemIds);
|
ElnRecordVos.SharedFillPreview preview = preview(previewCommand);
|
if (!MessageDigest.isEqual(
|
execution.previewToken.getBytes(StandardCharsets.UTF_8),
|
preview.getPreviewToken().getBytes(StandardCharsets.UTF_8))) {
|
throw new DataException("批量填写预览已变化,请重新预览");
|
}
|
for (ElnRecordVos.SharedField field : preview.getFields()) {
|
if (execution.values.containsKey(field.getFieldVersionId())) {
|
sharedFields.put(field.getFieldVersionId(), field);
|
}
|
}
|
if (sharedFields.size() != execution.values.size()) {
|
throw new DataException("批量填写字段固定版本无效");
|
}
|
for (ElnRecordVos.SharedTarget target : preview.getTargets()) {
|
previewTargets.put(target.getTestWorkItemId(), target);
|
}
|
}
|
ElnRecordVos.SharedFilled result = new ElnRecordVos.SharedFilled();
|
result.setOperationCode(execution.operationCode);
|
result.setFieldVersionIds(new ArrayList<>(execution.values.keySet()));
|
if (execution.values.size() == 1) {
|
result.setFieldVersionId(execution.values.keySet().iterator().next());
|
}
|
for (ElnRecordForms.SharedFillTarget target : execution.targets) {
|
if (!operation.resuming) {
|
ElnRecordVos.SharedTarget previewTarget =
|
previewTargets.get(target.getTestWorkItemId());
|
if (previewTarget == null || !previewTarget.isEligible()
|
|| !supportsAll(sharedFields.values(),
|
target.getTestWorkItemId())) {
|
result.getTargets().add(rejected(target, "TARGET_NOT_ELIGIBLE",
|
previewTarget != null
|
&& StringUtils.hasText(previewTarget.getErrorMessage())
|
? previewTarget.getErrorMessage()
|
: "当前目标不可共享填写"));
|
continue;
|
}
|
}
|
try {
|
result.getTargets().add(targetTransactionExecutor.execute(
|
() -> commitTarget(execution, target, user), exception -> {
|
if (!(exception instanceof DataException)) {
|
LOG.error("多批次共享填写目标提交系统异常,operationCode={}, testWorkItemId={}, recordId={}",
|
execution.operationCode, target.getTestWorkItemId(),
|
target.getRecordId(), exception);
|
}
|
}));
|
} catch (DataException exception) {
|
result.getTargets().add(rejected(target, "TARGET_COMMIT_REJECTED",
|
StringUtils.hasText(exception.getMessage())
|
? exception.getMessage() : "共享填写目标提交失败"));
|
} catch (RuntimeException exception) {
|
result.getTargets().add(rejected(target, "TARGET_SYSTEM_ERROR",
|
"共享填写目标提交发生系统异常"));
|
}
|
}
|
return operationCoordinator.complete(user.tenantId, user.userId,
|
execution.operationCode, requestDigest, result);
|
}
|
|
private ElnRecordVos.SharedFillTargetResult commitTarget(
|
SharedFillExecution execution, ElnRecordForms.SharedFillTarget command,
|
UserContext user) {
|
ElnRecordVos.SharedFillTargetResult replayed = operationCoordinator.reserveTarget(
|
user.tenantId, user.userId, execution.operationCode, command);
|
if (replayed != null) return replayed;
|
TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor(
|
user.tenantId, user.userId,
|
Collections.singletonList(command.getTestWorkItemId()),
|
Collections.<String>emptyList());
|
List<TestWorkItemSnapshot> currentSnapshots = snapshots(batch,
|
Collections.singletonList(command.getTestWorkItemId()));
|
TestWorkItemSnapshot snapshot = currentSnapshots.get(0);
|
if (!"SUCCESS".equals(snapshot.getOutcomeCode())) {
|
throw new DataException(StringUtils.hasText(snapshot.getReason())
|
? snapshot.getReason() : "当前目标不可共享填写");
|
}
|
PreparedTarget prepared = prepareTarget(user, snapshot);
|
if (!prepared.eligible()) throw new DataException(prepared.errorMessage);
|
if (!Objects.equals(command.getRecordId(), prepared.record.getId())) {
|
throw new DataException("批量填写目标记录已变化");
|
}
|
if (!Objects.equals(command.getExpectedRecordVersion(), prepared.record.getFVersion())) {
|
throw new DataException("电子记录已被其他操作更新");
|
}
|
List<FieldChange> changes = new ArrayList<>();
|
List<ElnRecordFieldValueEntity> unchanged = new ArrayList<>();
|
for (Map.Entry<String, String> requested : execution.values.entrySet()) {
|
ElnRecordFieldValueEntity current = prepared.values.get(requested.getKey());
|
ElnFieldVersionEntity version = prepared.versions.get(requested.getKey());
|
if (current == null || version == null) {
|
throw new DataException("目标记录不包含全部共享字段固定版本");
|
}
|
NormalizedValue normalized = normalize(requested.getValue(), version);
|
if (sameValue(current, normalized)) {
|
unchanged.add(current);
|
} else {
|
changes.add(new FieldChange(current, normalized, current.getZhiWenben()));
|
}
|
}
|
if (changes.isEmpty()) {
|
auditUnchanged(execution, prepared, unchanged, user);
|
ElnRecordVos.SharedFillTargetResult result = targetResult(command);
|
result.setStatus("UNCHANGED");
|
result.setRecordVersion(command.getExpectedRecordVersion());
|
operationCoordinator.completeTarget(user.tenantId, user.userId,
|
execution.operationCode, command, result);
|
return result;
|
}
|
for (FieldChange change : changes) {
|
if (StringUtils.hasText(change.current.getZhiWenben())
|
&& !Objects.equals(change.current.getZhiWenben(), change.normalized.text)) {
|
if (!Boolean.TRUE.equals(command.getOverwrite())
|
|| !StringUtils.hasText(command.getOverwriteReason())) {
|
throw new DataException("目标字段已有不同值,必须确认覆盖并填写原因");
|
}
|
command.setOverwriteReason(command.getOverwriteReason().trim());
|
}
|
}
|
if (recordMapper.advanceVersion(user.tenantId, prepared.record.getId(),
|
command.getExpectedRecordVersion(), user.userId) != 1) {
|
throw new DataException("电子记录已被其他操作更新");
|
}
|
Set<String> changedBindingIds = new LinkedHashSet<>();
|
List<ElnRecordFieldValueEntity> changedValues = new ArrayList<>();
|
for (FieldChange change : changes) {
|
apply(change.current, change.normalized, user.userId);
|
if (valueMapper.updateById(change.current) != 1) {
|
throw new DataException("共享填写字段保存冲突");
|
}
|
changedBindingIds.add(change.current.getMubanBangdingId());
|
changedValues.add(change.current);
|
}
|
List<ElnRecordFieldValueEntity> calculated = calculationService.recalculateAffected(
|
user.tenantId, prepared.record, prepared.revision, prepared.allValues,
|
changedBindingIds,
|
execution.operationCode, user.userId);
|
for (ElnRecordFieldValueEntity value : calculated) {
|
if (valueMapper.updateById(value) != 1) {
|
throw new DataException("共享填写后的公式结果保存冲突");
|
}
|
}
|
List<ElnRecordFieldValueEntity> documentValues = new ArrayList<>(changedValues);
|
documentValues.addAll(calculated);
|
documentSynchronizer.synchronize(user.tenantId, prepared.record,
|
prepared.revision, documentValues, execution.operationCode);
|
auditCommitted(execution, prepared, changes, user);
|
ElnRecordVos.SharedFillTargetResult result = targetResult(command);
|
result.setStatus("COMMITTED");
|
result.setRecordVersion(command.getExpectedRecordVersion() + 1);
|
result.setRecalculatedCount(calculated.size());
|
operationCoordinator.completeTarget(user.tenantId, user.userId,
|
execution.operationCode, command, result);
|
return result;
|
}
|
|
private static NormalizedValue normalize(String raw, ElnFieldVersionEntity version) {
|
String type = version.getShujuLeixing() == null
|
? "" : version.getShujuLeixing().toUpperCase();
|
if (!"DECIMAL".equals(type) && !"NUMBER".equals(type)) {
|
throw new DataException("当前批量填写仅支持数值字段");
|
}
|
if (!StringUtils.hasText(raw)) throw new DataException("批量填写值不能为空");
|
try {
|
BigDecimal number = new BigDecimal(raw.trim());
|
return new NormalizedValue(number.toPlainString(), number);
|
} catch (NumberFormatException exception) {
|
throw new DataException("批量填写数值格式无效");
|
}
|
}
|
|
private static boolean sameValue(ElnRecordFieldValueEntity current,
|
NormalizedValue normalized) {
|
if (!StringUtils.hasText(current.getZhiWenben())) return false;
|
try {
|
return new BigDecimal(current.getZhiWenben().trim())
|
.compareTo(normalized.number) == 0;
|
} catch (NumberFormatException exception) {
|
throw new DataException("目标字段现有数值格式无效");
|
}
|
}
|
|
private static void apply(ElnRecordFieldValueEntity target,
|
NormalizedValue normalized, String userId) {
|
Date now = new Date();
|
target.setZhiWenben(normalized.text);
|
target.setZhiShuzhi(normalized.number);
|
target.setZhiRiqi(null);
|
target.setZhiLaiyuan("MANUAL");
|
target.setYuanShuxing(null);
|
target.setYuanYinyongLeixing(null);
|
target.setYuanYinyongId(null);
|
target.setYuanHuoquShijian(null);
|
target.setFLastModifyUserId(userId);
|
target.setFLastModifyTime(now);
|
}
|
|
private void auditCommitted(SharedFillExecution execution, PreparedTarget prepared,
|
List<FieldChange> changes, UserContext user) {
|
List<Map<String, String>> differences = new ArrayList<>();
|
for (FieldChange change : changes) {
|
Map<String, String> difference = new LinkedHashMap<>();
|
difference.put("fieldVersionId", change.current.getZiduanBanbenId());
|
difference.put("fieldCode", change.current.getZiduanBianma());
|
difference.put("previousValue", change.previousValue);
|
difference.put("currentValue", change.current.getZhiWenben());
|
differences.add(difference);
|
}
|
auditRecorder.accept(auditBuilder(execution, prepared, user)
|
.eventType("DATA_CHANGE")
|
.fieldDiffs(JsonUtil.getObjectToString(differences))
|
.reason(execution.overwriteReason(prepared.snapshot.getTestWorkItemId()))
|
.build());
|
}
|
|
private void auditUnchanged(SharedFillExecution execution, PreparedTarget prepared,
|
List<ElnRecordFieldValueEntity> values, UserContext user) {
|
List<Map<String, String>> snapshots = new ArrayList<>();
|
for (ElnRecordFieldValueEntity value : values) {
|
Map<String, String> snapshot = new LinkedHashMap<>();
|
snapshot.put("fieldVersionId", value.getZiduanBanbenId());
|
snapshot.put("fieldCode", value.getZiduanBianma());
|
snapshot.put("currentValue", value.getZhiWenben());
|
snapshots.add(snapshot);
|
}
|
auditRecorder.accept(auditBuilder(execution, prepared, user)
|
.eventType("BIZ_ACTION")
|
.dataSnapshot(JsonUtil.getObjectToString(snapshots))
|
.build());
|
}
|
|
private static AuditEventDTO.AuditEventDTOBuilder auditBuilder(
|
SharedFillExecution execution, PreparedTarget prepared, UserContext user) {
|
return AuditEventDTO.builder()
|
.clientEventId(auditClientEventId(
|
execution.operationCode, prepared.record.getId()))
|
.operationId(execution.operationCode)
|
.tenantId(user.tenantId)
|
.operatorId(user.userId)
|
.eventTime(new Date())
|
.actionCode("ELN_RECORD_SHARED_FILL")
|
.actionLabel("多批次共享填写")
|
.bizModule("eln")
|
.bizType("eln_record")
|
.bizCode(prepared.record.getJiluBianhao())
|
.targetTable("eln_record")
|
.targetId(prepared.record.getId());
|
}
|
|
private static ElnRecordVos.SharedFillTargetResult rejected(
|
ElnRecordForms.SharedFillTarget command, String code, String message) {
|
ElnRecordVos.SharedFillTargetResult result = targetResult(command);
|
result.setStatus("REJECTED");
|
result.setErrorCode(code);
|
result.setErrorMessage(message);
|
result.setRecordVersion(command.getExpectedRecordVersion());
|
return result;
|
}
|
|
private static ElnRecordVos.SharedFillTargetResult targetResult(
|
ElnRecordForms.SharedFillTarget command) {
|
ElnRecordVos.SharedFillTargetResult result = new ElnRecordVos.SharedFillTargetResult();
|
result.setTestWorkItemId(command.getTestWorkItemId());
|
result.setRecordId(command.getRecordId());
|
return result;
|
}
|
|
private static SharedFillExecution validateExecution(
|
ElnRecordForms.SharedFillExecute command) {
|
if (command == null) throw new DataException("批量填写提交不能为空");
|
List<String> itemIds = testWorkItemIds(command.getTestWorkItemIds());
|
String previewToken = required(command.getPreviewToken(), "批量填写预览凭证不能为空");
|
String operationCode = required(command.getOperationCode(), "批量填写操作码不能为空");
|
if (operationCode.length() > MAX_OPERATION_CODE_LENGTH) {
|
throw new DataException("批量填写操作码长度不能超过64个字符");
|
}
|
Map<String, String> values = requestedValues(command);
|
if (command.getTargets() == null || command.getTargets().isEmpty()) {
|
throw new DataException("批量填写目标不能为空");
|
}
|
List<ElnRecordForms.SharedFillTarget> targets = new ArrayList<>();
|
Set<String> targetIds = new LinkedHashSet<>();
|
for (ElnRecordForms.SharedFillTarget target : command.getTargets()) {
|
if (target == null) throw new DataException("批量填写目标不能为空");
|
target.setTestWorkItemId(required(target.getTestWorkItemId(), "批量填写检验项不能为空"));
|
target.setRecordId(required(target.getRecordId(), "批量填写记录不能为空"));
|
if (target.getExpectedRecordVersion() == null
|
|| target.getExpectedRecordVersion() < 0) {
|
throw new DataException("电子记录版本不能为空");
|
}
|
if (!itemIds.contains(target.getTestWorkItemId())) {
|
throw new DataException("批量填写目标不在预览范围内");
|
}
|
if (!targetIds.add(target.getTestWorkItemId())) {
|
throw new DataException("批量填写目标不能重复");
|
}
|
targets.add(target);
|
}
|
return new SharedFillExecution(itemIds, previewToken, operationCode,
|
values, targets);
|
}
|
|
private static Map<String, String> requestedValues(
|
ElnRecordForms.SharedFillExecute command) {
|
List<ElnRecordForms.SharedFillValue> requested = command.getValues();
|
if (requested == null || requested.isEmpty()) {
|
ElnRecordForms.SharedFillValue compatible =
|
new ElnRecordForms.SharedFillValue();
|
compatible.setFieldVersionId(required(command.getFieldVersionId(),
|
"批量填写字段版本不能为空"));
|
compatible.setValue(command.getValue());
|
requested = Collections.singletonList(compatible);
|
}
|
Map<String, String> result = new LinkedHashMap<>();
|
for (ElnRecordForms.SharedFillValue value : requested) {
|
if (value == null) throw new DataException("批量填写字段不能为空");
|
String fieldVersionId = required(value.getFieldVersionId(),
|
"批量填写字段版本不能为空");
|
if (result.containsKey(fieldVersionId)) {
|
throw new DataException("批量填写字段不能重复");
|
}
|
result.put(fieldVersionId, value.getValue());
|
}
|
return result;
|
}
|
|
private static String auditClientEventId(String operationCode, String recordId) {
|
String candidate = operationCode + ":" + recordId;
|
if (candidate.length() <= MAX_AUDIT_CLIENT_EVENT_ID_LENGTH) return candidate;
|
return "SFS-" + sha256(candidate);
|
}
|
|
private static boolean supportsAll(
|
Iterable<ElnRecordVos.SharedField> fields, String testWorkItemId) {
|
for (ElnRecordVos.SharedField field : fields) {
|
if (!field.getTargetTestWorkItemIds().contains(testWorkItemId)) return false;
|
}
|
return true;
|
}
|
|
private static List<String> testWorkItemIds(List<String> values) {
|
ElnRecordForms.SharedFillPreview command = new ElnRecordForms.SharedFillPreview();
|
command.setTestWorkItemIds(values);
|
return testWorkItemIds(command);
|
}
|
|
private static String required(String value, String message) {
|
if (!StringUtils.hasText(value)) throw new DataException(message);
|
return value.trim();
|
}
|
|
private PreparedTarget prepareTarget(UserContext user, TestWorkItemSnapshot snapshot) {
|
ElnRecordEntity record = recordMapper.selectLiveByTestWorkItem(
|
user.tenantId, snapshot.getTestWorkItemId());
|
if (record == null || !StringUtils.hasText(record.getDangqianXiudingId())) {
|
return rejectedTarget(snapshot, "RECORD_NOT_FOUND", "检验项电子记录不存在");
|
}
|
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) {
|
return new PreparedTarget(snapshot, record, null,
|
"REVISION_NOT_FOUND", "电子记录当前修订不存在");
|
}
|
if (!DRAFT.equals(revision.getBizStatus())) {
|
return new PreparedTarget(snapshot, record, revision,
|
"REVISION_NOT_DRAFT", "电子记录当前修订不可批量填写");
|
}
|
List<ElnRecordFieldValueEntity> recordValues = 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)));
|
Map<String, ElnRecordFieldValueEntity> shared = new LinkedHashMap<>();
|
Map<String, ElnFieldVersionEntity> versions = new LinkedHashMap<>();
|
for (ElnRecordFieldValueEntity value : recordValues) {
|
ElnFieldVersionEntity version = fixedVersion(user.tenantId, value);
|
if (!shareable(value, version)) continue;
|
if (shared.put(version.getId(), value) != null) {
|
throw new DataException("电子记录存在重复的批量共享字段");
|
}
|
versions.put(version.getId(), version);
|
}
|
return new PreparedTarget(snapshot, record, revision, recordValues, shared, versions);
|
}
|
|
private static PreparedTarget rejectedTarget(TestWorkItemSnapshot snapshot) {
|
return rejectedTarget(snapshot, snapshot.getOutcomeCode(),
|
StringUtils.hasText(snapshot.getReason())
|
? snapshot.getReason() : "当前用户或检验项状态不允许批量填写");
|
}
|
|
private static PreparedTarget rejectedTarget(
|
TestWorkItemSnapshot snapshot, String errorCode, String errorMessage) {
|
return new PreparedTarget(snapshot, errorCode, errorMessage);
|
}
|
|
private ElnFieldVersionEntity fixedVersion(String tenantId, ElnRecordFieldValueEntity value) {
|
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 static boolean shareable(
|
ElnRecordFieldValueEntity value, ElnFieldVersionEntity version) {
|
if (!"PRIMARY".equals(value.getBangdingJuese())
|
|| !"ACTIVE".equals(value.getBangdingZhuangtai())
|
|| (StringUtils.hasText(value.getZhiLaiyuan())
|
&& !"MANUAL".equals(value.getZhiLaiyuan()))
|
|| !ElnFieldEnums.BatchMode.BATCH_SHARED.name().equals(version.getPiciMoshi())) {
|
return false;
|
}
|
ElnFieldForms.Binding binding = StringUtils.hasText(version.getBangdingPeizhi())
|
? JsonUtil.getJsonToBean(version.getBangdingPeizhi(), ElnFieldForms.Binding.class)
|
: null;
|
return binding != null && binding.getRead() != null && binding.getWrite() != null
|
&& "NONE".equals(binding.getRead().getSource())
|
&& "ELN_RECORD".equals(binding.getWrite().getTarget());
|
}
|
|
private static Map<String, ElnFieldVersionEntity> groupedFields(
|
List<PreparedTarget> targets) {
|
Map<String, ElnFieldVersionEntity> result = new LinkedHashMap<>();
|
for (PreparedTarget target : targets) {
|
if (!target.eligible()) continue;
|
for (Map.Entry<String, ElnFieldVersionEntity> field : target.versions.entrySet()) {
|
result.putIfAbsent(field.getKey(), field.getValue());
|
}
|
}
|
return result;
|
}
|
|
private static ElnRecordFieldValueEntity firstValue(
|
List<PreparedTarget> targets, String fieldVersionId) {
|
for (PreparedTarget target : targets) {
|
ElnRecordFieldValueEntity value = target.values.get(fieldVersionId);
|
if (target.eligible() && value != null) return value;
|
}
|
return null;
|
}
|
|
private static ElnRecordVos.SharedField field(
|
ElnFieldVersionEntity version,
|
ElnRecordFieldValueEntity value,
|
List<PreparedTarget> targets) {
|
ElnRecordVos.SharedField result = new ElnRecordVos.SharedField();
|
result.setFieldVersionId(version.getId());
|
result.setFieldCode(value.getZiduanBianma());
|
result.setFieldName(version.getZiduanMingcheng());
|
result.setDataType(version.getShujuLeixing());
|
result.setUnit(version.getDanwei());
|
Set<String> currentValues = new LinkedHashSet<>();
|
for (PreparedTarget target : targets) {
|
if (!target.eligible() || !target.values.containsKey(version.getId())) continue;
|
result.getTargetTestWorkItemIds().add(
|
target.snapshot.getTestWorkItemId());
|
currentValues.add(target.values.get(version.getId()).getZhiWenben());
|
}
|
result.setConflictingValues(currentValues.size() > 1);
|
if (result.isConflictingValues()) {
|
for (PreparedTarget target : targets) {
|
ElnRecordFieldValueEntity current = target.values.get(version.getId());
|
if (target.eligible() && current != null
|
&& StringUtils.hasText(current.getZhiWenben())) {
|
result.getDefaultExcludedTestWorkItemIds().add(
|
target.snapshot.getTestWorkItemId());
|
}
|
}
|
}
|
return result;
|
}
|
|
private static ElnRecordVos.SharedTarget toSharedTarget(
|
PreparedTarget prepared, Set<String> fieldVersionIds) {
|
ElnRecordVos.SharedTarget result = new ElnRecordVos.SharedTarget();
|
result.setTestWorkItemId(prepared.snapshot.getTestWorkItemId());
|
result.setTestWorkItemName(prepared.snapshot.getTestDefinitionName());
|
result.setEligible(prepared.eligible());
|
result.setErrorCode(prepared.errorCode);
|
result.setErrorMessage(prepared.errorMessage);
|
if (prepared.record != null) {
|
result.setRecordId(prepared.record.getId());
|
result.setRecordVersion(prepared.record.getFVersion());
|
}
|
if (prepared.revision != null) result.setRevisionId(prepared.revision.getId());
|
if (!prepared.eligible()) return result;
|
for (String fieldVersionId : fieldVersionIds) {
|
ElnRecordFieldValueEntity current = prepared.values.get(fieldVersionId);
|
if (current != null) {
|
result.getCurrentValues().put(fieldVersionId, current.getZhiWenben());
|
}
|
}
|
return result;
|
}
|
|
private static List<String> testWorkItemIds(ElnRecordForms.SharedFillPreview command) {
|
if (command == null || command.getTestWorkItemIds() == null) {
|
throw new DataException("批量填写检验项不能为空");
|
}
|
LinkedHashSet<String> result = new LinkedHashSet<>();
|
for (String value : command.getTestWorkItemIds()) {
|
if (!StringUtils.hasText(value)) {
|
throw new DataException("批量填写检验项不能为空");
|
}
|
if (!result.add(value.trim())) {
|
throw new DataException("批量填写检验项不能重复");
|
}
|
}
|
if (result.size() < 2) {
|
throw new DataException("批量填写至少选择两个检验项");
|
}
|
return new ArrayList<>(result);
|
}
|
|
private static List<TestWorkItemSnapshot> snapshots(
|
TestWorkItemSnapshotBatch batch, List<String> itemIds) {
|
if (batch == null || batch.getItems() == null
|
|| batch.getItems().size() != itemIds.size()) {
|
throw new DataException("LIMS 检验任务快照返回无效");
|
}
|
Map<String, TestWorkItemSnapshot> byId = new LinkedHashMap<>();
|
for (TestWorkItemSnapshot item : batch.getItems()) {
|
if (item == null || !StringUtils.hasText(item.getTestWorkItemId())
|
|| byId.put(item.getTestWorkItemId(), item) != null) {
|
throw new DataException("LIMS 检验任务快照返回无效");
|
}
|
}
|
List<TestWorkItemSnapshot> result = new ArrayList<>();
|
for (String itemId : itemIds) {
|
TestWorkItemSnapshot item = byId.get(itemId);
|
if (item == null || !StringUtils.hasText(item.getOutcomeCode())) {
|
throw new DataException("LIMS 检验任务快照返回无效");
|
}
|
result.add(item);
|
}
|
return result;
|
}
|
|
private static ProjectReference commonProject(List<TestWorkItemSnapshot> snapshots) {
|
ProjectReference result = null;
|
for (TestWorkItemSnapshot item : snapshots) {
|
if (!StringUtils.hasText(item.getTestDefinitionCode())) {
|
if ("SUCCESS".equals(item.getOutcomeCode())) {
|
throw new DataException("检验项缺少稳定检验项目编码");
|
}
|
continue;
|
}
|
if (result == null) {
|
result = new ProjectReference(
|
StringUtils.hasText(item.getTestDefinitionId())
|
? item.getTestDefinitionId().trim() : null,
|
item.getTestDefinitionCode().trim(),
|
StringUtils.hasText(item.getTestDefinitionName())
|
? item.getTestDefinitionName().trim() : null);
|
}
|
if (!result.code.equals(item.getTestDefinitionCode().trim())) {
|
throw new DataException("批量填写只允许选择同一检验项目");
|
}
|
}
|
return result;
|
}
|
|
private static String previewToken(
|
UserContext user, ElnRecordVos.SharedFillPreview preview) {
|
StringBuilder source = new StringBuilder();
|
append(source, user.tenantId);
|
append(source, user.userId);
|
append(source, preview.getTestDefinitionCode());
|
for (ElnRecordVos.SharedField field : preview.getFields()) {
|
append(source, field.getFieldVersionId());
|
}
|
for (ElnRecordVos.SharedTarget target : preview.getTargets()) {
|
append(source, target.getTestWorkItemId());
|
append(source, target.getRecordId());
|
append(source, target.getRevisionId());
|
append(source, target.getRecordVersion());
|
for (Map.Entry<String, String> value : target.getCurrentValues().entrySet()) {
|
append(source, value.getKey());
|
append(source, value.getValue());
|
}
|
}
|
try {
|
byte[] digest = MessageDigest.getInstance("SHA-256")
|
.digest(source.toString().getBytes(StandardCharsets.UTF_8));
|
StringBuilder result = new StringBuilder(64);
|
for (byte value : digest) result.append(String.format("%02x", value & 0xff));
|
return result.toString();
|
} catch (NoSuchAlgorithmException exception) {
|
throw new IllegalStateException("SHA-256 不可用", exception);
|
}
|
}
|
|
private static final class ProjectReference {
|
private final String id;
|
private final String code;
|
private final String name;
|
|
private ProjectReference(String id, String code, String name) {
|
this.id = id;
|
this.code = code;
|
this.name = name;
|
}
|
}
|
|
private static void append(StringBuilder target, Object value) {
|
String text = value == null ? "" : String.valueOf(value);
|
target.append(text.length()).append(':').append(text).append('|');
|
}
|
|
private static String executionDigest(UserContext user, SharedFillExecution execution) {
|
StringBuilder source = new StringBuilder();
|
append(source, user.tenantId);
|
append(source, user.userId);
|
append(source, execution.previewToken);
|
for (String testWorkItemId : execution.testWorkItemIds) {
|
append(source, testWorkItemId);
|
}
|
for (Map.Entry<String, String> value : execution.values.entrySet()) {
|
append(source, value.getKey());
|
append(source, value.getValue());
|
}
|
for (ElnRecordForms.SharedFillTarget target : execution.targets) {
|
append(source, target.getTestWorkItemId());
|
append(source, target.getRecordId());
|
append(source, target.getExpectedRecordVersion());
|
append(source, target.getOverwrite());
|
append(source, target.getOverwriteReason());
|
}
|
return sha256(source.toString());
|
}
|
|
private static String sha256(String source) {
|
try {
|
byte[] digest = MessageDigest.getInstance("SHA-256")
|
.digest(source.getBytes(StandardCharsets.UTF_8));
|
StringBuilder result = new StringBuilder(64);
|
for (byte value : digest) result.append(String.format("%02x", value & 0xff));
|
return result.toString();
|
} catch (NoSuchAlgorithmException exception) {
|
throw new IllegalStateException("SHA-256 不可用", exception);
|
}
|
}
|
|
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 TargetTransactionExecutor {
|
ElnRecordVos.SharedFillTargetResult execute(
|
Supplier<ElnRecordVos.SharedFillTargetResult> stateAction,
|
Consumer<RuntimeException> failureAudit);
|
}
|
|
@FunctionalInterface
|
interface DocumentSynchronizer {
|
void synchronize(String tenantId, ElnRecordEntity record,
|
ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> changedValues,
|
String operationCode);
|
}
|
|
interface OperationCoordinator {
|
OperationStart begin(
|
String tenantId, String userId, String operationCode,
|
String requestDigest);
|
|
ElnRecordVos.SharedFillTargetResult reserveTarget(
|
String tenantId, String userId, String operationCode,
|
ElnRecordForms.SharedFillTarget target);
|
|
void completeTarget(
|
String tenantId, String userId, String operationCode,
|
ElnRecordForms.SharedFillTarget target,
|
ElnRecordVos.SharedFillTargetResult result);
|
|
ElnRecordVos.SharedFilled complete(
|
String tenantId, String userId, String operationCode,
|
String requestDigest, ElnRecordVos.SharedFilled result);
|
}
|
|
static final class OperationStart {
|
private final boolean resuming;
|
private final ElnRecordVos.SharedFilled replayed;
|
|
private OperationStart(boolean resuming,
|
ElnRecordVos.SharedFilled replayed) {
|
this.resuming = resuming;
|
this.replayed = replayed;
|
}
|
|
static OperationStart fresh() {
|
return new OperationStart(false, null);
|
}
|
|
static OperationStart resuming() {
|
return new OperationStart(true, null);
|
}
|
|
static OperationStart replay(ElnRecordVos.SharedFilled result) {
|
return new OperationStart(true, result);
|
}
|
|
boolean isResuming() {
|
return resuming;
|
}
|
}
|
|
static final class InMemoryOperationCoordinator
|
implements OperationCoordinator {
|
private String tenantId;
|
private String userId;
|
private String operationCode;
|
private String requestDigest;
|
private ElnRecordVos.SharedFilled completed;
|
private final Map<String, ElnRecordVos.SharedFillTargetResult> targets =
|
new LinkedHashMap<>();
|
|
@Override
|
public OperationStart begin(
|
String tenantId, String userId, String operationCode,
|
String requestDigest) {
|
if (this.operationCode == null) {
|
this.tenantId = tenantId;
|
this.userId = userId;
|
this.operationCode = operationCode;
|
this.requestDigest = requestDigest;
|
return OperationStart.fresh();
|
}
|
if (!Objects.equals(this.tenantId, tenantId)
|
|| !Objects.equals(this.userId, userId)
|
|| !Objects.equals(this.operationCode, operationCode)
|
|| !Objects.equals(this.requestDigest, requestDigest)) {
|
throw new DataException("批量填写操作码已用于其他请求");
|
}
|
if (completed != null) return OperationStart.replay(completed);
|
return targets.isEmpty()
|
? OperationStart.fresh() : OperationStart.resuming();
|
}
|
|
@Override
|
public ElnRecordVos.SharedFillTargetResult reserveTarget(
|
String tenantId, String userId, String operationCode,
|
ElnRecordForms.SharedFillTarget target) {
|
return targets.get(target.getTestWorkItemId());
|
}
|
|
@Override
|
public void completeTarget(
|
String tenantId, String userId, String operationCode,
|
ElnRecordForms.SharedFillTarget target,
|
ElnRecordVos.SharedFillTargetResult result) {
|
targets.put(target.getTestWorkItemId(), result);
|
}
|
|
@Override
|
public ElnRecordVos.SharedFilled complete(
|
String tenantId, String userId, String operationCode,
|
String requestDigest, ElnRecordVos.SharedFilled result) {
|
completed = result;
|
return result;
|
}
|
}
|
|
private static final class NormalizedValue {
|
private final String text;
|
private final BigDecimal number;
|
|
private NormalizedValue(String text, BigDecimal number) {
|
this.text = text;
|
this.number = number;
|
}
|
}
|
|
private static final class FieldChange {
|
private final ElnRecordFieldValueEntity current;
|
private final NormalizedValue normalized;
|
private final String previousValue;
|
|
private FieldChange(ElnRecordFieldValueEntity current,
|
NormalizedValue normalized, String previousValue) {
|
this.current = current;
|
this.normalized = normalized;
|
this.previousValue = previousValue;
|
}
|
}
|
|
private static final class SharedFillExecution {
|
private final List<String> testWorkItemIds;
|
private final String previewToken;
|
private final String operationCode;
|
private final Map<String, String> values;
|
private final List<ElnRecordForms.SharedFillTarget> targets;
|
|
private SharedFillExecution(List<String> testWorkItemIds, String previewToken,
|
String operationCode, Map<String, String> values,
|
List<ElnRecordForms.SharedFillTarget> targets) {
|
this.testWorkItemIds = testWorkItemIds;
|
this.previewToken = previewToken;
|
this.operationCode = operationCode;
|
this.values = values;
|
this.targets = targets;
|
}
|
|
private String overwriteReason(String testWorkItemId) {
|
for (ElnRecordForms.SharedFillTarget target : targets) {
|
if (testWorkItemId.equals(target.getTestWorkItemId())) {
|
return target.getOverwriteReason();
|
}
|
}
|
return null;
|
}
|
}
|
|
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 PreparedTarget {
|
private final TestWorkItemSnapshot snapshot;
|
private final ElnRecordEntity record;
|
private final ElnRecordRevisionEntity revision;
|
private final List<ElnRecordFieldValueEntity> allValues;
|
private final Map<String, ElnRecordFieldValueEntity> values;
|
private final Map<String, ElnFieldVersionEntity> versions;
|
private final String errorCode;
|
private final String errorMessage;
|
|
private PreparedTarget(
|
TestWorkItemSnapshot snapshot,
|
ElnRecordEntity record,
|
ElnRecordRevisionEntity revision,
|
List<ElnRecordFieldValueEntity> allValues,
|
Map<String, ElnRecordFieldValueEntity> values,
|
Map<String, ElnFieldVersionEntity> versions) {
|
this.snapshot = snapshot;
|
this.record = record;
|
this.revision = revision;
|
this.allValues = allValues;
|
this.values = values;
|
this.versions = versions;
|
this.errorCode = null;
|
this.errorMessage = null;
|
}
|
|
private PreparedTarget(
|
TestWorkItemSnapshot snapshot, String errorCode, String errorMessage) {
|
this.snapshot = snapshot;
|
this.record = null;
|
this.revision = null;
|
this.allValues = Collections.emptyList();
|
this.values = Collections.emptyMap();
|
this.versions = Collections.emptyMap();
|
this.errorCode = errorCode;
|
this.errorMessage = errorMessage;
|
}
|
|
private PreparedTarget(
|
TestWorkItemSnapshot snapshot,
|
ElnRecordEntity record,
|
ElnRecordRevisionEntity revision,
|
String errorCode,
|
String errorMessage) {
|
this.snapshot = snapshot;
|
this.record = record;
|
this.revision = revision;
|
this.allValues = Collections.emptyList();
|
this.values = Collections.emptyMap();
|
this.versions = Collections.emptyMap();
|
this.errorCode = errorCode;
|
this.errorMessage = errorMessage;
|
}
|
|
private boolean eligible() {
|
return errorCode == null;
|
}
|
}
|
}
|