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.eln.RecordFillSaveCommand; import jnpf.eln.RecordFillSaveOperation; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.field.ElnFieldForms; import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity; import jnpf.elnEntity.runtime.ElnRecordEntity; import jnpf.elnEntity.runtime.ElnRecordFieldValueEntity; import jnpf.elnEntity.runtime.ElnRecordFormulaCalculationResult; import jnpf.elnEntity.runtime.ElnRecordRevisionEntity; import jnpf.elnEntity.runtime.ElnRecordRepeatRowEntity; import jnpf.elnEntity.runtime.ElnRecordRepeatValueEntity; import jnpf.elnEntity.runtime.ElnRecordSaveOperationEntity; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateVos; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnRecordFieldValueMapper; import jnpf.elnMapper.ElnRecordMapper; import jnpf.elnMapper.ElnRecordRevisionMapper; import jnpf.elnMapper.ElnRecordRepeatRowMapper; import jnpf.elnMapper.ElnRecordRepeatValueMapper; import jnpf.elnMapper.ElnRecordSaveOperationMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnMapper.ElnTemplateFormulaBindingMapper; import jnpf.elnService.ElnTestWorkItemSnapshotProvider; import jnpf.elnService.ElnRecordFillAccessService; import jnpf.elnService.ElnRecordFormulaCalculationService; import jnpf.elnService.ElnRecordSourceSyncDocumentConfirmation; import jnpf.elnService.template.ElnTemplateDocumentEvidence; import jnpf.elnService.template.ElnTemplateBindingConfirmationFacade; import jnpf.elnService.template.ElnTemplateCheckboxGroupService; import jnpf.elnService.template.docx.DocxControlConfigMatcher; import jnpf.elnService.template.docx.DocxExpectedControlConfig; import jnpf.elnService.template.docx.DocxControlSnapshot; import jnpf.database.util.TenantDataSourceUtil; import jnpf.exception.DataException; import jnpf.lims.api.model.TestWorkItemSnapshotBatch; import jnpf.lims.api.model.TestWorkItemSnapshot; import jnpf.util.JsonUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.ObjectProvider; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.util.Collections; 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; /** 记录文件归属与 LIMS 当前负责人/状态的双重准入。 */ @Service public class ElnRecordFillAccessServiceImpl implements ElnRecordFillAccessService { private final ElnRecordMapper recordMapper; private final ElnRecordRevisionMapper revisionMapper; private final ElnTestWorkItemSnapshotProvider taskProvider; private final ElnRecordFieldValueMapper valueMapper; private final ElnTemplateBindingMapper bindingMapper; private final ElnFieldVersionMapper fieldVersionMapper; private final ElnRecordSaveOperationMapper operationMapper; private final ElnTemplateFormulaBindingMapper formulaBindingMapper; private final ElnRecordFormulaCalculationService formulaCalculationService; private final ElnTemplateDocumentEvidence documentEvidence; private final ElnRecordRepeatRowMapper repeatRowMapper; private final ElnRecordRepeatValueMapper repeatValueMapper; private final TenantRoute tenantRoute; private final ElnTemplateBindingConfirmationFacade confirmationFacade; private final Consumer auditRecorder; private final ElnRecordSourceSyncDocumentConfirmation sourceSyncConfirmation; private final ElnTemplateCheckboxGroupService checkboxGroupService; @Autowired public ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, ElnTemplateCheckboxGroupService checkboxGroupService, ElnRecordSourceSyncDocumentConfirmation sourceSyncConfirmation, ObjectProvider auditClientProvider) { this(recordMapper, revisionMapper, taskProvider, valueMapper, bindingMapper, fieldVersionMapper, operationMapper, formulaBindingMapper, formulaCalculationService, documentEvidence, confirmationFacade, repeatRowMapper, repeatValueMapper, event -> { AuditClient client = auditClientProvider.getIfAvailable(); if (client != null) client.record(event); }, new TenantRoute() { @Override public void switchTo(String tenantId) { TenantDataSourceUtil.switchTenant(tenantId); } @Override public void clear() { TenantDataSourceUtil.clearLocalTenantInfo(); } }, sourceSyncConfirmation, checkboxGroupService); } ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, TenantRoute tenantRoute) { this(recordMapper, revisionMapper, taskProvider, valueMapper, bindingMapper, fieldVersionMapper, operationMapper, formulaBindingMapper, formulaCalculationService, documentEvidence, confirmationFacade, null, null, event -> { }, tenantRoute); } ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, TenantRoute tenantRoute) { this(recordMapper, revisionMapper, taskProvider, valueMapper, bindingMapper, fieldVersionMapper, operationMapper, formulaBindingMapper, formulaCalculationService, documentEvidence, confirmationFacade, repeatRowMapper, repeatValueMapper, event -> { }, tenantRoute); } ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, Consumer auditRecorder, TenantRoute tenantRoute) { this(recordMapper, revisionMapper, taskProvider, valueMapper, bindingMapper, fieldVersionMapper, operationMapper, formulaBindingMapper, formulaCalculationService, documentEvidence, confirmationFacade, repeatRowMapper, repeatValueMapper, auditRecorder, tenantRoute, ElnRecordSourceSyncDocumentConfirmation.none()); } ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, Consumer auditRecorder, TenantRoute tenantRoute, ElnRecordSourceSyncDocumentConfirmation sourceSyncConfirmation) { this(recordMapper, revisionMapper, taskProvider, valueMapper, bindingMapper, fieldVersionMapper, operationMapper, formulaBindingMapper, formulaCalculationService, documentEvidence, confirmationFacade, repeatRowMapper, repeatValueMapper, auditRecorder, tenantRoute, sourceSyncConfirmation, null); } ElnRecordFillAccessServiceImpl(ElnRecordMapper recordMapper, ElnRecordRevisionMapper revisionMapper, ElnTestWorkItemSnapshotProvider taskProvider, ElnRecordFieldValueMapper valueMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldVersionMapper fieldVersionMapper, ElnRecordSaveOperationMapper operationMapper, ElnTemplateFormulaBindingMapper formulaBindingMapper, ElnRecordFormulaCalculationService formulaCalculationService, ElnTemplateDocumentEvidence documentEvidence, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnRecordRepeatRowMapper repeatRowMapper, ElnRecordRepeatValueMapper repeatValueMapper, Consumer auditRecorder, TenantRoute tenantRoute, ElnRecordSourceSyncDocumentConfirmation sourceSyncConfirmation, ElnTemplateCheckboxGroupService checkboxGroupService) { this.recordMapper = recordMapper; this.revisionMapper = revisionMapper; this.taskProvider = taskProvider; this.valueMapper = valueMapper; this.bindingMapper = bindingMapper; this.fieldVersionMapper = fieldVersionMapper; this.operationMapper = operationMapper; this.formulaBindingMapper = formulaBindingMapper; this.formulaCalculationService = formulaCalculationService; this.documentEvidence = documentEvidence; this.repeatRowMapper = repeatRowMapper; this.repeatValueMapper = repeatValueMapper; this.tenantRoute = tenantRoute; this.confirmationFacade = confirmationFacade; this.auditRecorder = auditRecorder; this.sourceSyncConfirmation = sourceSyncConfirmation; this.checkboxGroupService = checkboxGroupService; } @Override public boolean canFill(String tenantId, String userId, String recordId, String fileId) { try { return inTenant(tenantId, () -> resolveCurrent(tenantId, userId, recordId, fileId) != null); } catch (RuntimeException ignored) { return false; } } @Override public RecordFillSaveOperation preflight(RecordFillSaveCommand command) { return inTenant(command == null ? null : command.getTenantId(), () -> confirmationFacade.execute(() -> preflightInTenant(command), ignored -> { })); } private RecordFillSaveOperation preflightInTenant(RecordFillSaveCommand command) { requireSession(command); Current current = resolveCurrent(command.getTenantId(), command.getUserId(), command.getRecordId(), command.getFileId()); ElnRecordSaveOperationEntity entity = new ElnRecordSaveOperationEntity(); entity.setId(IdUtil.getSnowflakeNextIdStr()); entity.setOperationCode(IdUtil.getSnowflakeNextIdStr()); entity.setOperationStatus(RecordFillSaveOperation.PREPARED); entity.setDianziJiluId(current.record.getId()); entity.setXiudingId(current.revision.getId()); entity.setJiluWendang(command.getFileId()); entity.setExpectedRecordVersion(current.record.getFVersion()); entity.setSessionDocKey(command.getSessionDocKey()); entity.setSessionVersion(command.getSessionVersion()); entity.setOperatorUserId(command.getUserId()); entity.setFTenantId(command.getTenantId()); entity.setFVersion(0); entity.setFCreatorUserId(command.getUserId()); entity.setFLastModifyUserId(command.getUserId()); entity.setFDeleteMark(0); entity.setFCreatorTime(new Date()); if (operationMapper.insert(entity) != 1) { throw new DataException("电子记录保存操作创建冲突"); } return operation(entity); } @Override public RecordFillSaveOperation commit(RecordFillSaveCommand command) { return inTenant(command == null ? null : command.getTenantId(), () -> commitInTenant(command)); } private RecordFillSaveOperation commitInTenant(RecordFillSaveCommand command) { requireOperation(command); ElnRecordSaveOperationEntity operation = findOperation(command.getTenantId(), command.getOperationCode()); if (operation == null) return rejected(command, "电子记录保存操作不存在"); if (RecordFillSaveOperation.COMMITTED.equals(operation.getOperationStatus()) || RecordFillSaveOperation.REJECTED.equals(operation.getOperationStatus())) return operation(operation); try { return confirmationFacade.execute(() -> commitState(command), ignored -> rejectPrepared(command.getTenantId(), command.getOperationCode(), ignored.getMessage())); } catch (DataException e) { ElnRecordSaveOperationEntity rejected = findOperation(command.getTenantId(), command.getOperationCode()); if (rejected != null && RecordFillSaveOperation.REJECTED.equals(rejected.getOperationStatus())) { return operation(rejected); } throw e; } } private RecordFillSaveOperation commitState(RecordFillSaveCommand command) { ElnRecordSaveOperationEntity operation = findOperation(command.getTenantId(), command.getOperationCode()); if (operation == null) throw new DataException("电子记录保存操作不存在"); if (!RecordFillSaveOperation.PREPARED.equals(operation.getOperationStatus())) return operation(operation); assertOperationIdentity(operation, command); Current current = resolveCurrent(command.getTenantId(), command.getUserId(), command.getRecordId(), command.getFileId()); if (!Objects.equals(current.record.getFVersion(), operation.getExpectedRecordVersion())) { throw new DataException("电子记录已被其他保存更新"); } PreparedValues prepared = prepareValues(current, command); if (recordMapper.advanceVersion(command.getTenantId(), current.record.getId(), operation.getExpectedRecordVersion(), command.getUserId()) != 1) { throw new DataException("电子记录保存回调已过期"); } // From this point any failure must roll back record CAS, field values and operation status together. saveValues(prepared.changedValues); saveRepeatValues(prepared.repeat); prepared.changedBindingIds.addAll(prepared.repeat.changedBindingIds); ElnRecordFormulaCalculationResult calculated = formulaCalculationService.recalculateAffectedWithRepeats( command.getTenantId(), current.record, current.revision, prepared.values, prepared.changedBindingIds, operation.getOperationCode(), command.getUserId()); saveValues(calculated.getFieldValues()); saveCalculatedRepeatValues(calculated.getRepeatValues()); sourceSyncConfirmation.confirmSavedDocument(command.getTenantId(), command.getUserId(), current.record.getId(), current.revision.getId()); if (operationMapper.finishPrepared(command.getTenantId(), operation.getOperationCode(), RecordFillSaveOperation.COMMITTED, null) != 1) { throw new DataException("电子记录保存操作状态冲突"); } operation.setOperationStatus(RecordFillSaveOperation.COMMITTED); operation.setRejectReason(null); return operation(operation); } @Override public RecordFillSaveOperation saveStatus(String tenantId, String operationCode) { return inTenant(tenantId, () -> saveStatusInTenant(tenantId, operationCode)); } @Override public RecordFillSaveOperation abort(RecordFillSaveCommand command) { return inTenant(command == null ? null : command.getTenantId(), () -> { requireOperation(command); ElnRecordSaveOperationEntity operation = findOperation(command.getTenantId(), command.getOperationCode()); if (operation == null) return rejected(command, "电子记录保存操作不存在"); if (!RecordFillSaveOperation.PREPARED.equals(operation.getOperationStatus())) return operation(operation); assertOperationIdentity(operation, command); return confirmationFacade.execute(() -> reject(operation, "OnlyOffice 本地写回失败"), ignored -> { }); }); } private RecordFillSaveOperation saveStatusInTenant(String tenantId, String operationCode) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(operationCode)) { throw new DataException("电子记录保存操作标识不能为空"); } ElnRecordSaveOperationEntity operation = findOperation(tenantId, operationCode); return operation == null ? null : operation(operation); } private Current resolveCurrent(String tenantId, String userId, String recordId, String fileId) { if (!StringUtils.hasText(tenantId) || !StringUtils.hasText(userId) || !StringUtils.hasText(recordId) || !StringUtils.hasText(fileId)) { throw new DataException("电子记录保存身份不完整"); } ElnRecordEntity record = recordMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordEntity::getFTenantId, 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.lambdaQuery() .eq(ElnRecordRevisionEntity::getFTenantId, tenantId).eq(ElnRecordRevisionEntity::getId, record.getDangqianXiudingId()) .eq(ElnRecordRevisionEntity::getDianziJiluId, recordId).eq(ElnRecordRevisionEntity::getJiluWendang, fileId) .and(q -> q.isNull(ElnRecordRevisionEntity::getFDeleteMark).or().eq(ElnRecordRevisionEntity::getFDeleteMark, 0)).last("limit 1")); if (revision == null) throw new DataException("电子记录当前修订与文件不匹配"); if (!"DRAFT".equals(revision.getBizStatus())) { throw new DataException("电子记录当前修订不可编辑"); } TestWorkItemSnapshotBatch batch = taskProvider.resolveAsActor(tenantId, userId, Collections.singletonList(record.getJianyanXiangId()), Collections.emptyList()); 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())) { throw new DataException("当前用户或检验项状态不允许保存电子记录"); } return new Current(record, revision); } private PreparedValues prepareValues(Current current, RecordFillSaveCommand command) { Map bindings = activeBindings(command.getTenantId(), current.record.getMubanId()); Map checkboxGroupRules = checkboxGroupRules( command.getTenantId(), current.record.getMubanId()); Set formulaResults = formulaResultBindingIds( command.getTenantId(), current.record.getMubanId()); Map controls = controls(command.getTenantId(), command.getFileId(), bindings); List values = valueMapper.selectList(Wrappers.lambdaQuery() .eq(ElnRecordFieldValueEntity::getFTenantId, command.getTenantId()) .eq(ElnRecordFieldValueEntity::getDianziJiluId, current.record.getId()) .eq(ElnRecordFieldValueEntity::getXiudingId, current.revision.getId()) .and(q -> q.isNull(ElnRecordFieldValueEntity::getFDeleteMark).or().eq(ElnRecordFieldValueEntity::getFDeleteMark, 0))); Map valuesByBinding = new LinkedHashMap<>(); for (ElnRecordFieldValueEntity value : values) { if (valuesByBinding.put(value.getMubanBangdingId(), value) != null) { throw new DataException("电子记录存在重复主字段值"); } } List changedValues = new java.util.ArrayList<>(); Set changedBindingIds = new LinkedHashSet<>(); for (ElnTemplateBindingEntity binding : bindings.values()) { if (!"PRIMARY".equals(binding.getKongjianJuese())) continue; ElnRecordFieldValueEntity value = valuesByBinding.remove(binding.getId()); if (value == null || !Objects.equals(value.getZiduanBanbenId(), binding.getZiduanBanbenId()) || !Objects.equals(value.getZiduanBianma(), binding.getZiduanBianma())) { throw new DataException("DOCX 主字段绑定与记录固定版本不一致"); } DocxControlSnapshot control = controls.get(binding.getKongjianBiaoqian()); ElnFieldVersionEntity version = fixedVersion(command.getTenantId(), value.getZiduanBanbenId()); validateControlConfig(control, version); if (formulaResults.contains(binding.getId())) continue; if (isProviderSource(version) || "CROSS_TEMPLATE".equals(value.getZhiLaiyuan())) { assertProviderValueUnchanged(value, control, version); continue; } ValueState before = ValueState.of(value); applyNormalizedValue(value, control, version); if (!before.equals(ValueState.of(value))) { value.setZhiLaiyuan("MANUAL"); value.setFLastModifyUserId(command.getUserId()); changedValues.add(value); changedBindingIds.add(binding.getId()); } } if (!valuesByBinding.isEmpty()) throw new DataException("电子记录字段值与活动主绑定不一致"); verifyReferences(command.getTenantId(), bindings, controls); RepeatPrepared repeat = prepareRepeatValues(current, command, bindings, controls, formulaResults); validatePrimaryCheckboxGroups(bindings, values, checkboxGroupRules); validateRepeatCheckboxGroups(repeat, checkboxGroupRules); return new PreparedValues(values, changedValues, changedBindingIds, repeat); } private static void validatePrimaryCheckboxGroups( Map bindings, List values, Map rules) { Map valuesByBinding = new LinkedHashMap<>(); for (ElnRecordFieldValueEntity value : values) { valuesByBinding.put(value.getMubanBangdingId(), value); } Map states = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : bindings.values()) { if (!"PRIMARY".equals(binding.getKongjianJuese()) || !"CHECKBOX".equals(binding.getKongjianLeixing()) || !StringUtils.hasText(binding.getCheckboxGroupCode())) { continue; } String key = String.valueOf(binding.getStepId()) + "\n" + binding.getCheckboxGroupCode(); CheckboxGroupState state = states.computeIfAbsent(key, ignored -> new CheckboxGroupState(binding.getCheckboxGroupCode())); ElnRecordFieldValueEntity value = valuesByBinding.get(binding.getId()); if (value != null && "true".equals(value.getZhiWenben())) { state.checked.add(binding); } } for (CheckboxGroupState state : states.values()) { validateCheckboxGroupState(state, rules.get(state.groupCode)); } } private Map checkboxGroupRules( String tenantId, String templateId) { Map result = new LinkedHashMap<>(); if (checkboxGroupService == null) return result; List groups = checkboxGroupService.list(tenantId, templateId); if (groups == null) throw new DataException("电子记录选择组数量规则无效"); for (ElnTemplateVos.CheckboxGroup group : groups) { if (group == null || !StringUtils.hasText(group.getGroupCode()) || !validCheckboxGroupRule(group) || result.put(group.getGroupCode(), group) != null) { throw new DataException("电子记录选择组数量规则无效"); } } return result; } private static boolean validCheckboxGroupRule(ElnTemplateVos.CheckboxGroup group) { Integer minSelected = group.getMinSelected(); Integer maxSelected = group.getMaxSelected(); return minSelected != null && minSelected >= 0 && (maxSelected == null || (maxSelected >= 1 && minSelected <= maxSelected)); } private static void validateCheckboxGroupState( CheckboxGroupState state, ElnTemplateVos.CheckboxGroup configuredRule) { int minSelected = configuredRule == null ? 0 : configuredRule.getMinSelected(); Integer maxSelected = configuredRule == null ? Integer.valueOf(1) : configuredRule.getMaxSelected(); int selected = state.checked.size(); if (selected < minSelected) { throw new DataException("选择组“" + state.groupCode + "”至少选择 " + minSelected + " 项,当前已选择 " + selected + " 项"); } if (maxSelected != null && selected > maxSelected) { throw new DataException("选择组“" + state.groupCode + "”最多选择 " + maxSelected + " 项,当前已选择 " + selected + " 项:" + checkboxMembers(state.checked)); } } private static void validateRepeatCheckboxGroups(RepeatPrepared prepared, Map rules) { for (CheckboxGroupState state : prepared.checkboxGroups.values()) { validateCheckboxGroupState(state, rules.get(state.groupCode)); } } private static String checkboxMembers(List bindings) { List members = new java.util.ArrayList<>(); for (ElnTemplateBindingEntity binding : bindings) { String code = binding.getZiduanBianma(); String alias = binding.getKongjianBieming(); members.add(StringUtils.hasText(alias) ? alias + "(" + code + ")" : code); } return String.join("、", members); } private RepeatPrepared prepareRepeatValues(Current current, RecordFillSaveCommand command, Map bindings, Map controls, Set formulaResults) { List repeatBindings = new java.util.ArrayList<>(); for (ElnTemplateBindingEntity binding : bindings.values()) { if ("REPEAT".equals(binding.getKongjianJuese())) repeatBindings.add(binding); } boolean hasRepeatControl = controls.keySet().stream() .anyMatch(tag -> tag.startsWith("eln.repeat.")); if (repeatBindings.isEmpty()) { if (hasRepeatControl) throw new DataException("DOCX 存在未绑定的重复字段控件"); return RepeatPrepared.empty(); } if (repeatRowMapper == null || repeatValueMapper == null) { throw new DataException("重复行持久化服务不可用"); } Map> bindingsByGroup = new LinkedHashMap<>(); Map> bindingsByGroupAndField = new LinkedHashMap<>(); Map prototypesByGroup = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : repeatBindings) { String groupCode = binding.getRepeatGroupCode(); if (!StringUtils.hasText(groupCode) || !"prototype".equals(binding.getRepeatPrototypeRowId()) || !StringUtils.hasText(binding.getZiduanBianma())) { throw new DataException("模板重复组绑定无效"); } Map bindingsByField = bindingsByGroupAndField.computeIfAbsent(groupCode, ignored -> new LinkedHashMap<>()); if (bindingsByField.put(binding.getZiduanBianma(), binding) != null) { throw new DataException("模板重复组绑定无效"); } bindingsByGroup.computeIfAbsent(groupCode, ignored -> new java.util.ArrayList<>()).add(binding); DocxControlSnapshot control = controls.get(binding.getKongjianBiaoqian()); requireRepeatCell(control, binding); DocxControlSnapshot prototype = prototypesByGroup.putIfAbsent(groupCode, control); if (prototype != null && !samePhysicalRow(prototype, control)) { throw new DataException("重复组原型列不在同一物理行"); } } List rows = repeatRowMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatRowEntity::getFTenantId, command.getTenantId()) .eq(ElnRecordRepeatRowEntity::getDianziJiluId, current.record.getId()) .eq(ElnRecordRepeatRowEntity::getXiudingId, current.revision.getId()) .in(ElnRecordRepeatRowEntity::getOperationStatus, "PREPARED", "ACTIVE", "DELETE_PREPARED") .and(q -> q.isNull(ElnRecordRepeatRowEntity::getFDeleteMark) .or().eq(ElnRecordRepeatRowEntity::getFDeleteMark, 0)) .orderByAsc(ElnRecordRepeatRowEntity::getRowOrder)); Map rowsByIdentity = new LinkedHashMap<>(); Map> rowsByGroup = new LinkedHashMap<>(); for (ElnRecordRepeatRowEntity row : rows) { if (row == null || !Objects.equals(current.record.getMubanId(), row.getMubanId()) || !bindingsByGroup.containsKey(row.getRepeatGroupCode()) || !StringUtils.hasText(row.getRowId()) || rowsByIdentity.put(repeatRowKey( row.getRepeatGroupCode(), row.getRowId()), row) != null) { throw new DataException("重复行身份数据无效"); } rowsByGroup.computeIfAbsent(row.getRepeatGroupCode(), ignored -> new java.util.ArrayList<>()).add(row); } Map runtimeControls = new LinkedHashMap<>(); Map physicalRows = new LinkedHashMap<>(); for (DocxControlSnapshot control : controls.values()) { if (!control.getTag().startsWith("eln.repeat.")) continue; RepeatTag tag = RepeatTag.parse(control.getTag()); if (tag == null) { throw new DataException("DOCX 重复字段 Tag 无效"); } Map bindingsByField = bindingsByGroupAndField.get(tag.groupCode); if (bindingsByField == null) throw new DataException("DOCX 重复字段 Tag 无效"); ElnTemplateBindingEntity binding = bindingsByField.get(tag.fieldCode); if (binding == null) throw new DataException("DOCX 存在未知重复字段"); if ("prototype".equals(tag.rowId)) continue; String rowKey = repeatRowKey(tag.groupCode, tag.rowId); ElnRecordRepeatRowEntity row = rowsByIdentity.get(rowKey); if (row == null) throw new DataException("DOCX 存在未分配身份的重复行"); requireRepeatCell(control, binding); DocxControlSnapshot firstInRow = physicalRows.putIfAbsent(rowKey, control); if (firstInRow != null && !samePhysicalRow(firstInRow, control)) { throw new DataException("同一重复行的字段不在同一物理行"); } if (runtimeControls.put(repeatCellKey( tag.groupCode, tag.rowId, binding.getId()), control) != null) { throw new DataException("DOCX 重复行存在重复字段列"); } } List existing = repeatValueMapper.selectList( Wrappers.lambdaQuery() .eq(ElnRecordRepeatValueEntity::getFTenantId, command.getTenantId()) .eq(ElnRecordRepeatValueEntity::getDianziJiluId, current.record.getId()) .eq(ElnRecordRepeatValueEntity::getXiudingId, current.revision.getId()) .and(q -> q.isNull(ElnRecordRepeatValueEntity::getFDeleteMark) .or().eq(ElnRecordRepeatValueEntity::getFDeleteMark, 0))); Map valuesByCell = new LinkedHashMap<>(); for (ElnRecordRepeatValueEntity value : existing) { String rowKey = repeatRowKey(value.getRepeatGroupCode(), value.getRepeatRowId()); String key = repeatCellKey(value.getRepeatGroupCode(), value.getRepeatRowId(), value.getMubanBangdingId()); Map groupBindings = bindingsByGroupAndField.get(value.getRepeatGroupCode()); if (!rowsByIdentity.containsKey(rowKey) || groupBindings == null || groupBindings.values().stream().noneMatch(binding -> Objects.equals(binding.getId(), value.getMubanBangdingId())) || valuesByCell.put(key, value) != null) { throw new DataException("重复字段值与行模型不一致"); } } RepeatPrepared prepared = new RepeatPrepared(); for (Map.Entry> groupEntry : bindingsByGroup.entrySet()) { String groupCode = groupEntry.getKey(); List groupBindings = groupEntry.getValue(); DocxControlSnapshot prototype = prototypesByGroup.get(groupCode); int previousPhysicalRow = prototype.getTableRowIndex(); for (ElnRecordRepeatRowEntity row : rowsByGroup.getOrDefault( groupCode, Collections.emptyList())) { String rowKey = repeatRowKey(groupCode, row.getRowId()); DocxControlSnapshot firstInRow = physicalRows.get(rowKey); if (firstInRow == null) { if ("PREPARED".equals(row.getOperationStatus())) continue; if ("DELETE_PREPARED".equals(row.getOperationStatus())) { for (ElnTemplateBindingEntity binding : groupBindings) { String key = repeatCellKey( groupCode, row.getRowId(), binding.getId()); ElnRecordRepeatValueEntity value = valuesByCell.remove(key); if (value == null) { throw new DataException("待删除重复行缺少持久化字段值"); } requireRepeatValueIdentity(value, current, row, binding); prepared.valuesToDelete.add(value); prepared.changedBindingIds.add(binding.getId()); } prepared.rowsToDelete.add(row); continue; } throw new DataException("DOCX 缺少已激活的重复行"); } if (!Objects.equals(prototype.getPartName(), firstInRow.getPartName()) || !Objects.equals(prototype.getTableIndex(), firstInRow.getTableIndex()) || firstInRow.getTableRowIndex() <= previousPhysicalRow) { throw new DataException("DOCX 重复行未按末尾顺序追加"); } previousPhysicalRow = firstInRow.getTableRowIndex(); for (ElnTemplateBindingEntity binding : groupBindings) { String key = repeatCellKey(groupCode, row.getRowId(), binding.getId()); DocxControlSnapshot control = runtimeControls.remove(key); if (control == null) throw new DataException("DOCX 重复行缺少字段列"); ElnFieldVersionEntity version = fixedVersion( command.getTenantId(), binding.getZiduanBanbenId()); if (isProviderSource(version)) { throw new DataException("当前阶段重复字段仅支持人工填写"); } validateControlConfig(control, version); if (formulaResults.contains(binding.getId())) { ElnRecordRepeatValueEntity value = valuesByCell.remove(key); if (value == null) { value = newRepeatValue(current, command, row, binding, new ElnRecordFieldValueEntity()); value.setZhiLaiyuan("FORMULA"); prepared.inserts.add(value); } else { requireRepeatValueIdentity(value, current, row, binding); } continue; } ElnRecordFieldValueEntity normalized = new ElnRecordFieldValueEntity(); applyNormalizedValue(normalized, control, version); if ("CHECKBOX".equals(binding.getKongjianLeixing()) && StringUtils.hasText(binding.getCheckboxGroupCode())) { String checkboxGroupKey = groupCode + "\n" + row.getRowId() + "\n" + binding.getCheckboxGroupCode(); CheckboxGroupState state = prepared.checkboxGroups.computeIfAbsent( checkboxGroupKey, ignored -> new CheckboxGroupState( binding.getCheckboxGroupCode())); if ("true".equals(normalized.getZhiWenben())) { state.checked.add(binding); } } ElnRecordRepeatValueEntity value = valuesByCell.remove(key); if (value == null) { value = newRepeatValue(current, command, row, binding, normalized); prepared.inserts.add(value); prepared.changedBindingIds.add(binding.getId()); } else { requireRepeatValueIdentity(value, current, row, binding); if (!sameValue(value, normalized)) { copyValue(value, normalized); value.setZhiLaiyuan("MANUAL"); value.setFLastModifyUserId(command.getUserId()); value.setFLastModifyTime(new Date()); prepared.updates.add(value); prepared.changedBindingIds.add(binding.getId()); } } } if ("PREPARED".equals(row.getOperationStatus())) prepared.rowsToActivate.add(row); } } if (!runtimeControls.isEmpty() || !valuesByCell.isEmpty()) { throw new DataException("DOCX 重复字段与持久化值不一致"); } return prepared; } private Set formulaResultBindingIds(String tenantId, String templateId) { Set result = new LinkedHashSet<>(); for (ElnTemplateFormulaBindingEntity formula : formulaBindingMapper.selectList( Wrappers.lambdaQuery() .eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId) .eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, "ACTIVE") .and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark) .or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0)))) { if (!result.add(formula.getJieguoBangdingId())) { throw new DataException("公式结果字段被重复写入"); } } return result; } private void saveValues(List values) { for (ElnRecordFieldValueEntity value : values) { if (valueMapper.updateById(value) != 1) { throw new DataException("电子记录字段保存冲突"); } } } private void saveRepeatValues(RepeatPrepared prepared) { for (ElnRecordRepeatValueEntity value : prepared.inserts) { if (repeatValueMapper.insert(value) != 1) { throw new DataException("重复字段值创建冲突"); } } for (ElnRecordRepeatValueEntity value : prepared.updates) { if (repeatValueMapper.updateById(value) != 1) { throw new DataException("重复字段值保存冲突"); } } Date deletedAt = new Date(); for (ElnRecordRepeatValueEntity value : prepared.valuesToDelete) { ElnRecordRepeatRowEntity row = prepared.rowsToDelete.stream() .filter(item -> Objects.equals(item.getRepeatGroupCode(), value.getRepeatGroupCode()) && Objects.equals(item.getRowId(), value.getRepeatRowId())) .findFirst().orElseThrow(() -> new DataException("待删除重复字段缺少行身份")); value.setFDeleteMark(1); value.setFDeleteUserId(row.getDeleteRequestedUserId()); value.setFDeleteTime(deletedAt); value.setFLastModifyUserId(row.getDeleteRequestedUserId()); value.setFLastModifyTime(deletedAt); if (repeatValueMapper.updateById(value) != 1) { throw new DataException("重复字段值逻辑删除冲突"); } } for (ElnRecordRepeatRowEntity row : prepared.rowsToActivate) { row.setOperationStatus("ACTIVE"); row.setFLastModifyTime(new Date()); if (repeatRowMapper.updateById(row) != 1) { throw new DataException("重复行激活冲突"); } auditRepeat(row, row.getOperationCode(), "ELN_RECORD_REPEAT_APPEND_ACTIVATE", "激活电子记录重复行", "activate"); } for (ElnRecordRepeatRowEntity row : prepared.rowsToDelete) { row.setOperationStatus("DELETED"); row.setFDeleteMark(1); row.setFDeleteUserId(row.getDeleteRequestedUserId()); row.setFDeleteTime(deletedAt); row.setFLastModifyUserId(row.getDeleteRequestedUserId()); row.setFLastModifyTime(deletedAt); if (repeatRowMapper.updateById(row) != 1) { throw new DataException("重复行逻辑删除冲突"); } auditRepeat(row, row.getDeleteOperationCode(), "ELN_RECORD_REPEAT_DELETE", "删除电子记录重复行", "delete"); } } private void saveCalculatedRepeatValues(List values) { for (ElnRecordRepeatValueEntity value : values) { if (repeatValueMapper.updateById(value) != 1) { throw new DataException("重复公式结果字段保存冲突"); } } } private void auditRepeat(ElnRecordRepeatRowEntity row, String operationCode, String actionCode, String actionLabel, String eventSuffix) { if (!StringUtils.hasText(operationCode)) return; Map snapshot = new LinkedHashMap<>(); snapshot.put("groupCode", row.getRepeatGroupCode()); snapshot.put("rowId", row.getRowId()); snapshot.put("rowOrder", row.getRowOrder()); snapshot.put("status", row.getOperationStatus()); snapshot.put("deleteReason", row.getDeleteReason()); auditRecorder.accept(AuditEventDTO.builder() .clientEventId(ElnRepeatAuditEventIds.of(operationCode, eventSuffix)) .operationId(operationCode) .eventTime(new Date()) .tenantId(row.getFTenantId()) .operatorId(row.getFLastModifyUserId()) .eventType("DATA_CHANGE") .actionCode(actionCode) .actionLabel(actionLabel) .bizModule("eln") .bizType("eln_record_repeat_row") .bizCode(row.getDianziJiluId()) .targetTable("eln_record_repeat_row") .targetId(row.getId()) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private static void requireRepeatCell(DocxControlSnapshot control, ElnTemplateBindingEntity binding) { if (control == null || !"REPEAT".equals(control.getControlRole()) || !Objects.equals(binding.getKongjianLeixing(), control.getControlType()) || !"word/document.xml".equals(control.getPartName()) || control.getTableIndex() == null || control.getTableRowIndex() == null) { throw new DataException("重复字段必须位于正文表格数据行内"); } if (control.isNestedTable()) throw new DataException("当前阶段不支持嵌套重复表格"); if (control.isVerticalMerge()) throw new DataException("重复数据区不支持跨行合并"); } private static boolean samePhysicalRow(DocxControlSnapshot left, DocxControlSnapshot right) { return Objects.equals(left.getPartName(), right.getPartName()) && Objects.equals(left.getTableIndex(), right.getTableIndex()) && Objects.equals(left.getTableRowIndex(), right.getTableRowIndex()); } private static String repeatRowKey(String groupCode, String rowId) { return groupCode + "\n" + rowId; } private static String repeatCellKey(String groupCode, String rowId, String bindingId) { return repeatRowKey(groupCode, rowId) + "\n" + bindingId; } private static ElnRecordRepeatValueEntity newRepeatValue(Current current, RecordFillSaveCommand command, ElnRecordRepeatRowEntity row, ElnTemplateBindingEntity binding, ElnRecordFieldValueEntity normalized) { Date now = new Date(); ElnRecordRepeatValueEntity value = new ElnRecordRepeatValueEntity(); value.setId(IdUtil.getSnowflakeNextIdStr()); value.setDianziJiluId(current.record.getId()); value.setXiudingId(current.revision.getId()); value.setMubanId(current.record.getMubanId()); value.setRepeatRowId(row.getRowId()); value.setRepeatGroupCode(row.getRepeatGroupCode()); value.setMubanBangdingId(binding.getId()); value.setZiduanDingyiId(binding.getZiduanDingyiId()); value.setZiduanBianma(binding.getZiduanBianma()); value.setZiduanBanbenId(binding.getZiduanBanbenId()); value.setBangdingJuese("REPEAT"); value.setBangdingZhuangtai("ACTIVE"); copyValue(value, normalized); value.setZhiLaiyuan("MANUAL"); value.setFTenantId(command.getTenantId()); value.setFVersion(0); value.setFCreatorUserId(command.getUserId()); value.setFCreatorTime(now); value.setFLastModifyUserId(command.getUserId()); value.setFLastModifyTime(now); value.setFDeleteMark(0); return value; } private static void requireRepeatValueIdentity(ElnRecordRepeatValueEntity value, Current current, ElnRecordRepeatRowEntity row, ElnTemplateBindingEntity binding) { if (!Objects.equals(current.record.getId(), value.getDianziJiluId()) || !Objects.equals(current.revision.getId(), value.getXiudingId()) || !Objects.equals(current.record.getMubanId(), value.getMubanId()) || !Objects.equals(row.getRepeatGroupCode(), value.getRepeatGroupCode()) || !Objects.equals(binding.getZiduanDingyiId(), value.getZiduanDingyiId()) || !Objects.equals(binding.getZiduanBianma(), value.getZiduanBianma()) || !Objects.equals(binding.getZiduanBanbenId(), value.getZiduanBanbenId()) || !"REPEAT".equals(value.getBangdingJuese()) || !"ACTIVE".equals(value.getBangdingZhuangtai())) { throw new DataException("重复字段值固定绑定已漂移"); } } private static boolean sameValue(ElnRecordRepeatValueEntity value, ElnRecordFieldValueEntity normalized) { return Objects.equals(value.getZhiWenben(), normalized.getZhiWenben()) && Objects.equals(value.getZhiShuzhi(), normalized.getZhiShuzhi()) && Objects.equals(value.getZhiRiqi(), normalized.getZhiRiqi()); } private static void copyValue(ElnRecordRepeatValueEntity target, ElnRecordFieldValueEntity source) { target.setZhiWenben(source.getZhiWenben()); target.setZhiShuzhi(source.getZhiShuzhi()); target.setZhiRiqi(source.getZhiRiqi()); } private Map activeBindings(String tenantId, String templateId) { List list = bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId).eq(ElnTemplateBindingEntity::getMubanId, templateId) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, "ACTIVE") .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark).or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))); Map result = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : list) { if (result.put(binding.getId(), binding) != null) throw new DataException("活动绑定重复"); } return result; } private Map controls(String tenantId, String fileId, Map bindings) { Map result = new LinkedHashMap<>(); for (DocxControlSnapshot control : documentEvidence.readControls(tenantId, fileId)) { if (result.put(control.getTag(), control) != null) throw new DataException("DOCX 存在重复字段 Tag"); } for (ElnTemplateBindingEntity binding : bindings.values()) { DocxControlSnapshot control = result.get(binding.getKongjianBiaoqian()); if (control == null || !Objects.equals(binding.getKongjianJuese(), control.getControlRole()) || !Objects.equals(binding.getKongjianLeixing(), control.getControlType())) { throw new DataException("DOCX 与活动绑定不一致"); } } for (DocxControlSnapshot control : result.values()) { String tag = control.getTag(); if ((tag.startsWith("eln.field.") || tag.startsWith("eln.ref.")) && !hasActiveBindingTag(bindings, tag)) { throw new DataException("DOCX 存在未绑定的 ELN 字段控件"); } } return result; } private static boolean hasActiveBindingTag(Map bindings, String tag) { for (ElnTemplateBindingEntity binding : bindings.values()) { if (Objects.equals(binding.getKongjianBiaoqian(), tag)) return true; } return false; } private void verifyReferences(String tenantId, Map bindings, Map controls) { for (ElnTemplateBindingEntity reference : bindings.values()) { if (!"REFERENCE".equals(reference.getKongjianJuese())) continue; ElnTemplateBindingEntity primary = bindings.get(reference.getYinyongShiliId()); if (primary == null || !"PRIMARY".equals(primary.getKongjianJuese())) { throw new DataException("活动引用缺少对应主字段"); } if (!Objects.equals(primary.getZiduanDingyiId(), reference.getZiduanDingyiId()) || !Objects.equals(primary.getZiduanBianma(), reference.getZiduanBianma()) || !Objects.equals(primary.getZiduanBanbenId(), reference.getZiduanBanbenId())) { throw new DataException("活动引用与主字段固定版本不一致"); } DocxControlSnapshot primaryControl = controls.get(primary.getKongjianBiaoqian()); DocxControlSnapshot referenceControl = controls.get(reference.getKongjianBiaoqian()); ElnFieldVersionEntity version = fixedVersion(tenantId, primary.getZiduanBanbenId()); validateControlConfig(primaryControl, version); validateControlConfig(referenceControl, version); String expected = normalizedText(primaryControl, version); String observed = normalizedText(referenceControl, version); if (!Objects.equals(expected, observed)) throw new DataException("DOCX 引用字段与主字段值不一致"); } } private String normalizedText(DocxControlSnapshot control, ElnFieldVersionEntity version) { ElnRecordFieldValueEntity temporary = new ElnRecordFieldValueEntity(); applyNormalizedValue(temporary, control, version); return temporary.getZhiWenben(); } @SuppressWarnings("unchecked") private void applyNormalizedValue(ElnRecordFieldValueEntity value, DocxControlSnapshot control, ElnFieldVersionEntity version) { String type = version.getShujuLeixing() == null ? "" : version.getShujuLeixing().toUpperCase(); String raw = control.getCurrentValue(); value.setZhiShuzhi(null); value.setZhiRiqi(null); if ("DECIMAL".equals(type) || "NUMBER".equals(type)) { if (!StringUtils.hasText(raw)) { value.setZhiWenben(null); return; } try { BigDecimal number = new BigDecimal(raw.trim()); value.setZhiShuzhi(number); value.setZhiWenben(number.toPlainString()); return; } catch (NumberFormatException e) { throw new DataException("DOCX 数值字段格式无效"); } } if ("DATE".equals(type) || "DATETIME".equals(type)) { if (!StringUtils.hasText(raw)) { value.setZhiWenben(null); return; } String format = "DATETIME".equals(type) ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd"; try { SimpleDateFormat parser = new SimpleDateFormat(format); parser.setLenient(false); Date date = parser.parse(raw.trim()); value.setZhiRiqi(date); value.setZhiWenben(new SimpleDateFormat(format).format(date)); return; } catch (ParseException e) { try { Date date = Date.from(Instant.parse(raw.trim())); value.setZhiRiqi(date); value.setZhiWenben(new SimpleDateFormat(format).format(date)); return; } catch (DateTimeParseException ignored) { throw new DataException("DOCX 日期字段格式无效"); } } } if ("CHECKBOX".equals(type) || "BOOLEAN".equals(type)) { if (!"true".equalsIgnoreCase(raw) && !"false".equalsIgnoreCase(raw)) { throw new DataException("DOCX 勾选字段状态无效"); } value.setZhiWenben(raw.toLowerCase()); return; } if ("SELECT".equals(type) || "DROPDOWN".equals(type)) { Map options = new LinkedHashMap<>(); Map config = StringUtils.hasText(version.getKongjianPeizhi()) ? JsonUtil.getJsonToBean(version.getKongjianPeizhi(), Map.class) : Collections.emptyMap(); Object optionConfig = config.get("options"); if (!(optionConfig instanceof List)) throw new DataException("下拉字段配置无效"); for (Object option : (List) optionConfig) { if (!(option instanceof Map)) throw new DataException("下拉字段配置无效"); Map item = (Map) option; String optionValue = String.valueOf(item.get("value")); String display = String.valueOf(item.get("display")); if (options.containsKey(optionValue) || options.containsValue(display)) throw new DataException("下拉显示值必须唯一"); options.put(optionValue, display); } if (!StringUtils.hasText(raw)) { value.setZhiWenben(null); return; } for (Map.Entry option : options.entrySet()) { if (Objects.equals(option.getValue(), raw)) { value.setZhiWenben(option.getKey()); return; } } throw new DataException("DOCX 下拉显示值不在字段固定配置中"); } value.setZhiWenben(raw); } private void validateControlConfig(DocxControlSnapshot control, ElnFieldVersionEntity version) { Map expected = DocxExpectedControlConfig.from(version.getShujuLeixing(), version.getKongjianPeizhi()); if (!DocxControlConfigMatcher.matches(control.getControlType(), expected, control.getControlConfig())) { throw new DataException("DOCX 控件配置与固定字段版本不一致"); } } private ElnFieldVersionEntity fixedVersion(String tenantId, String versionId) { ElnFieldVersionEntity version = fieldVersionMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnFieldVersionEntity::getFTenantId, tenantId).eq(ElnFieldVersionEntity::getId, versionId).last("limit 1")); if (version == null) throw new DataException("固定字段版本不存在"); return version; } private boolean isProviderSource(ElnFieldVersionEntity version) { ElnFieldForms.Binding binding; try { binding = StringUtils.hasText(version.getBangdingPeizhi()) ? JsonUtil.getJsonToBean(version.getBangdingPeizhi(), ElnFieldForms.Binding.class) : null; } catch (RuntimeException e) { throw new DataException("字段绑定配置无效"); } String source = binding == null || binding.getRead() == null ? null : binding.getRead().getSource(); return "LIMS_TASK".equals(source) || "SYSTEM".equals(source); } private void assertProviderValueUnchanged( ElnRecordFieldValueEntity value, DocxControlSnapshot control, ElnFieldVersionEntity version) { boolean emptyConfiguredSnapshot = value.getYuanShuxingBanben() != null && value.getZhiWenben() == null && value.getZhiShuzhi() == null && value.getZhiRiqi() == null; if (emptyConfiguredSnapshot) { String documentValue = control.getCurrentValue(); if (documentValue == null || ("CHECKBOX".equals(version.getShujuLeixing()) && "false".equalsIgnoreCase(documentValue))) { return; } } ElnRecordFieldValueEntity observed = new ElnRecordFieldValueEntity(); boolean configuredDate = value.getYuanShuxingBanben() != null && ("DATE".equals(version.getShujuLeixing()) || "DATETIME".equals(version.getShujuLeixing())); if (configuredDate) { observed.setZhiRiqi(configuredDocumentDate(control.getCurrentValue(), version.getShujuLeixing())); } else { applyNormalizedValue(observed, control, version); } boolean unchanged = Objects.equals(value.getZhiWenben(), observed.getZhiWenben()); if (value.getYuanShuxingBanben() != null && "DECIMAL".equals(version.getShujuLeixing())) { BigDecimal storedNumber = value.getZhiShuzhi(); BigDecimal documentNumber = observed.getZhiShuzhi(); unchanged = storedNumber == null ? documentNumber == null : documentNumber != null && storedNumber.compareTo(documentNumber) == 0; } else if (configuredDate) { Date storedDate = value.getZhiRiqi(); Date documentDate = observed.getZhiRiqi(); unchanged = storedDate == null ? documentDate == null : documentDate != null && storedDate.getTime() == documentDate.getTime(); } if (!unchanged) { throw new DataException("外部来源字段不允许人工修改,请使用重新同步"); } } private static Date configuredDocumentDate(String raw, String type) { if (!StringUtils.hasText(raw)) return null; String input = raw.trim(); boolean calendarDate = "DATE".equals(type); try { return calendarDate ? java.sql.Date.valueOf(LocalDate.parse(input)) : java.sql.Timestamp.valueOf(LocalDateTime.parse(input, DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss").withResolverStyle(ResolverStyle.STRICT))); } catch (DateTimeParseException invalidLocal) { try { Instant instant = Instant.parse(input); if (instant.getNano() != 0) throw new DataException("DOCX 来源日期时间不能丢失小数秒精度"); return calendarDate ? java.sql.Date.valueOf(instant.atZone(ZoneId.systemDefault()).toLocalDate()) : Date.from(instant); } catch (DateTimeParseException invalidInstant) { throw new DataException("DOCX 来源日期字段格式无效"); } } } private void requireSession(RecordFillSaveCommand command) { if (command == null || !StringUtils.hasText(command.getSessionDocKey()) || command.getSessionVersion() == null || command.getSessionVersion() < 1 || (command.getCallbackStatus() != null && command.getCallbackStatus() != 2 && command.getCallbackStatus() != 6)) { throw new DataException("电子记录保存会话无效"); } } private void requireOperation(RecordFillSaveCommand command) { requireSession(command); if (!StringUtils.hasText(command.getOperationCode())) throw new DataException("电子记录保存操作码不能为空"); } private void assertOperationIdentity(ElnRecordSaveOperationEntity operation, RecordFillSaveCommand command) { if (!Objects.equals(operation.getDianziJiluId(), command.getRecordId()) || !Objects.equals(operation.getJiluWendang(), command.getFileId()) || !Objects.equals(operation.getOperatorUserId(), command.getUserId()) || !Objects.equals(operation.getSessionDocKey(), command.getSessionDocKey()) || !Objects.equals(operation.getSessionVersion(), command.getSessionVersion())) { throw new DataException("电子记录保存操作身份已漂移"); } } private ElnRecordSaveOperationEntity findOperation(String tenantId, String operationCode) { return operationMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnRecordSaveOperationEntity::getFTenantId, tenantId) .eq(ElnRecordSaveOperationEntity::getOperationCode, operationCode) .and(q -> q.isNull(ElnRecordSaveOperationEntity::getFDeleteMark) .or().eq(ElnRecordSaveOperationEntity::getFDeleteMark, 0)).last("limit 1")); } private RecordFillSaveOperation reject(ElnRecordSaveOperationEntity operation, String reason) { if (operationMapper.finishPrepared(operation.getFTenantId(), operation.getOperationCode(), RecordFillSaveOperation.REJECTED, reason) == 1) { operation.setOperationStatus(RecordFillSaveOperation.REJECTED); operation.setRejectReason(reason); return operation(operation); } ElnRecordSaveOperationEntity refreshed = findOperation(operation.getFTenantId(), operation.getOperationCode()); return refreshed == null ? rejected(null, reason) : operation(refreshed); } private void rejectPrepared(String tenantId, String operationCode, String reason) { operationMapper.finishPrepared(tenantId, operationCode, RecordFillSaveOperation.REJECTED, reason); } private RecordFillSaveOperation rejected(RecordFillSaveCommand command, String reason) { RecordFillSaveOperation result = new RecordFillSaveOperation(); result.setOperationCode(command == null ? null : command.getOperationCode()); result.setStatus(RecordFillSaveOperation.REJECTED); result.setReason(reason); return result; } private static RecordFillSaveOperation operation(ElnRecordSaveOperationEntity entity) { RecordFillSaveOperation result = new RecordFillSaveOperation(); result.setOperationCode(entity.getOperationCode()); result.setStatus(entity.getOperationStatus()); result.setExpectedRecordVersion(entity.getExpectedRecordVersion()); result.setReason(entity.getRejectReason()); return result; } private static final class Current { private final ElnRecordEntity record; private final ElnRecordRevisionEntity revision; private Current(ElnRecordEntity record, ElnRecordRevisionEntity revision) { this.record = record; this.revision = revision; } } private T inTenant(String tenantId, TenantAction action) { if (!StringUtils.hasText(tenantId)) throw new DataException("电子记录租户不能为空"); try { tenantRoute.switchTo(tenantId.trim()); return action.get(); } finally { tenantRoute.clear(); } } @FunctionalInterface interface TenantAction { T get(); } interface TenantRoute { void switchTo(String tenantId); void clear(); } private static final class PreparedValues { private final List values; private final List changedValues; private final Set changedBindingIds; private final RepeatPrepared repeat; private PreparedValues(List values, List changedValues, Set changedBindingIds, RepeatPrepared repeat) { this.values = values; this.changedValues = changedValues; this.changedBindingIds = changedBindingIds; this.repeat = repeat; } } private static final class RepeatPrepared { private final List inserts = new java.util.ArrayList<>(); private final List updates = new java.util.ArrayList<>(); private final List valuesToDelete = new java.util.ArrayList<>(); private final List rowsToActivate = new java.util.ArrayList<>(); private final List rowsToDelete = new java.util.ArrayList<>(); private final Set changedBindingIds = new LinkedHashSet<>(); private final Map checkboxGroups = new LinkedHashMap<>(); private static RepeatPrepared empty() { return new RepeatPrepared(); } } private static final class CheckboxGroupState { private final String groupCode; private final List checked = new java.util.ArrayList<>(); private CheckboxGroupState(String groupCode) { this.groupCode = groupCode; } } private static final class RepeatTag { private final String groupCode; private final String rowId; private final String fieldCode; private RepeatTag(String groupCode, String rowId, String fieldCode) { this.groupCode = groupCode; this.rowId = rowId; this.fieldCode = fieldCode; } private static RepeatTag parse(String value) { String[] parts = value == null ? new String[0] : value.split("\\.", -1); if (parts.length != 5 || !"eln".equals(parts[0]) || !"repeat".equals(parts[1]) || !token(parts[2]) || !token(parts[3]) || !token(parts[4])) { return null; } return new RepeatTag(parts[2], parts[3], parts[4]); } private static boolean token(String value) { return value != null && value.matches("[A-Za-z0-9_-]{1,80}"); } } private static final class ValueState { private final String text; private final BigDecimal decimal; private final Date date; private ValueState(String text, BigDecimal decimal, Date date) { this.text = text; this.decimal = decimal; this.date = date; } private static ValueState of(ElnRecordFieldValueEntity value) { return new ValueState(value.getZhiWenben(), value.getZhiShuzhi(), value.getZhiRiqi()); } @Override public boolean equals(Object other) { if (this == other) return true; if (!(other instanceof ValueState)) return false; ValueState that = (ValueState) other; return Objects.equals(text, that.text) && Objects.equals(decimal, that.decimal) && Objects.equals(date, that.date); } @Override public int hashCode() { return Objects.hash(text, decimal, date); } } }