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 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 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 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 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 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 itemIds = testWorkItemIds(command); UserContext user = currentUserProvider.get(); TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor( user.tenantId, user.userId, itemIds, Collections.emptyList()); List snapshots = snapshots(batch, itemIds); ProjectReference project = commonProject(snapshots); List targets = new ArrayList<>(); for (TestWorkItemSnapshot snapshot : snapshots) { targets.add("SUCCESS".equals(snapshot.getOutcomeCode()) ? prepareTarget(user, snapshot) : rejectedTarget(snapshot)); } Map 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 previewTargets = new LinkedHashMap<>(); Map 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.emptyList()); List 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 changes = new ArrayList<>(); List unchanged = new ArrayList<>(); for (Map.Entry 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 changedBindingIds = new LinkedHashSet<>(); List 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 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 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 changes, UserContext user) { List> differences = new ArrayList<>(); for (FieldChange change : changes) { Map 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 values, UserContext user) { List> snapshots = new ArrayList<>(); for (ElnRecordFieldValueEntity value : values) { Map 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 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 values = requestedValues(command); if (command.getTargets() == null || command.getTargets().isEmpty()) { throw new DataException("批量填写目标不能为空"); } List targets = new ArrayList<>(); Set 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 requestedValues( ElnRecordForms.SharedFillExecute command) { List 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 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 fields, String testWorkItemId) { for (ElnRecordVos.SharedField field : fields) { if (!field.getTargetTestWorkItemIds().contains(testWorkItemId)) return false; } return true; } private static List testWorkItemIds(List 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.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 recordValues = valueMapper.selectList( Wrappers.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 shared = new LinkedHashMap<>(); Map 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.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 groupedFields( List targets) { Map result = new LinkedHashMap<>(); for (PreparedTarget target : targets) { if (!target.eligible()) continue; for (Map.Entry field : target.versions.entrySet()) { result.putIfAbsent(field.getKey(), field.getValue()); } } return result; } private static ElnRecordFieldValueEntity firstValue( List 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 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 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 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 testWorkItemIds(ElnRecordForms.SharedFillPreview command) { if (command == null || command.getTestWorkItemIds() == null) { throw new DataException("批量填写检验项不能为空"); } LinkedHashSet 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 snapshots( TestWorkItemSnapshotBatch batch, List itemIds) { if (batch == null || batch.getItems() == null || batch.getItems().size() != itemIds.size()) { throw new DataException("LIMS 检验任务快照返回无效"); } Map 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 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 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 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 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 stateAction, Consumer failureAudit); } @FunctionalInterface interface DocumentSynchronizer { void synchronize(String tenantId, ElnRecordEntity record, ElnRecordRevisionEntity revision, List 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 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 testWorkItemIds; private final String previewToken; private final String operationCode; private final Map values; private final List targets; private SharedFillExecution(List testWorkItemIds, String previewToken, String operationCode, Map values, List 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 allValues; private final Map values; private final Map versions; private final String errorCode; private final String errorMessage; private PreparedTarget( TestWorkItemSnapshot snapshot, ElnRecordEntity record, ElnRecordRevisionEntity revision, List allValues, Map values, Map 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; } } }