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<AuditEventDTO> 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<AuditClient> 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<AuditEventDTO> 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<AuditEventDTO> 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<AuditEventDTO> 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.<ElnRecordEntity>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.<ElnRecordRevisionEntity>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.<String>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<String, ElnTemplateBindingEntity> bindings = activeBindings(command.getTenantId(), current.record.getMubanId());
|
Map<String, ElnTemplateVos.CheckboxGroup> checkboxGroupRules = checkboxGroupRules(
|
command.getTenantId(), current.record.getMubanId());
|
Set<String> formulaResults = formulaResultBindingIds(
|
command.getTenantId(), current.record.getMubanId());
|
Map<String, DocxControlSnapshot> controls = controls(command.getTenantId(), command.getFileId(), bindings);
|
List<ElnRecordFieldValueEntity> values = valueMapper.selectList(Wrappers.<ElnRecordFieldValueEntity>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<String, ElnRecordFieldValueEntity> valuesByBinding = new LinkedHashMap<>();
|
for (ElnRecordFieldValueEntity value : values) {
|
if (valuesByBinding.put(value.getMubanBangdingId(), value) != null) {
|
throw new DataException("电子记录存在重复主字段值");
|
}
|
}
|
List<ElnRecordFieldValueEntity> changedValues = new java.util.ArrayList<>();
|
Set<String> 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<String, ElnTemplateBindingEntity> bindings,
|
List<ElnRecordFieldValueEntity> values,
|
Map<String, ElnTemplateVos.CheckboxGroup> rules) {
|
Map<String, ElnRecordFieldValueEntity> valuesByBinding = new LinkedHashMap<>();
|
for (ElnRecordFieldValueEntity value : values) {
|
valuesByBinding.put(value.getMubanBangdingId(), value);
|
}
|
Map<String, CheckboxGroupState> 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<String, ElnTemplateVos.CheckboxGroup> checkboxGroupRules(
|
String tenantId, String templateId) {
|
Map<String, ElnTemplateVos.CheckboxGroup> result = new LinkedHashMap<>();
|
if (checkboxGroupService == null) return result;
|
List<ElnTemplateVos.CheckboxGroup> 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<String, ElnTemplateVos.CheckboxGroup> rules) {
|
for (CheckboxGroupState state : prepared.checkboxGroups.values()) {
|
validateCheckboxGroupState(state, rules.get(state.groupCode));
|
}
|
}
|
|
private static String checkboxMembers(List<ElnTemplateBindingEntity> bindings) {
|
List<String> 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<String, ElnTemplateBindingEntity> bindings,
|
Map<String, DocxControlSnapshot> controls, Set<String> formulaResults) {
|
List<ElnTemplateBindingEntity> 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<String, List<ElnTemplateBindingEntity>> bindingsByGroup = new LinkedHashMap<>();
|
Map<String, Map<String, ElnTemplateBindingEntity>> bindingsByGroupAndField =
|
new LinkedHashMap<>();
|
Map<String, DocxControlSnapshot> 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<String, ElnTemplateBindingEntity> 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<ElnRecordRepeatRowEntity> rows = repeatRowMapper.selectList(
|
Wrappers.<ElnRecordRepeatRowEntity>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<String, ElnRecordRepeatRowEntity> rowsByIdentity = new LinkedHashMap<>();
|
Map<String, List<ElnRecordRepeatRowEntity>> 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<String, DocxControlSnapshot> runtimeControls = new LinkedHashMap<>();
|
Map<String, DocxControlSnapshot> 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<String, ElnTemplateBindingEntity> 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<ElnRecordRepeatValueEntity> existing = repeatValueMapper.selectList(
|
Wrappers.<ElnRecordRepeatValueEntity>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<String, ElnRecordRepeatValueEntity> valuesByCell = new LinkedHashMap<>();
|
for (ElnRecordRepeatValueEntity value : existing) {
|
String rowKey = repeatRowKey(value.getRepeatGroupCode(), value.getRepeatRowId());
|
String key = repeatCellKey(value.getRepeatGroupCode(),
|
value.getRepeatRowId(), value.getMubanBangdingId());
|
Map<String, ElnTemplateBindingEntity> 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<String, List<ElnTemplateBindingEntity>> groupEntry
|
: bindingsByGroup.entrySet()) {
|
String groupCode = groupEntry.getKey();
|
List<ElnTemplateBindingEntity> 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<String> formulaResultBindingIds(String tenantId, String templateId) {
|
Set<String> result = new LinkedHashSet<>();
|
for (ElnTemplateFormulaBindingEntity formula : formulaBindingMapper.selectList(
|
Wrappers.<ElnTemplateFormulaBindingEntity>lambdaQuery()
|
.eq(ElnTemplateFormulaBindingEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateFormulaBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai, "ACTIVE")
|
.and(q -> q.isNull(ElnTemplateFormulaBindingEntity::getFDeleteMark)
|
.or().eq(ElnTemplateFormulaBindingEntity::getFDeleteMark, 0)))) {
|
if (!result.add(formula.getJieguoBangdingId())) {
|
throw new DataException("公式结果字段被重复写入");
|
}
|
}
|
return result;
|
}
|
|
private void saveValues(List<ElnRecordFieldValueEntity> 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<ElnRecordRepeatValueEntity> 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<String, Object> 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<String, ElnTemplateBindingEntity> activeBindings(String tenantId, String templateId) {
|
List<ElnTemplateBindingEntity> list = bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>lambdaQuery()
|
.eq(ElnTemplateBindingEntity::getFTenantId, tenantId).eq(ElnTemplateBindingEntity::getMubanId, templateId)
|
.eq(ElnTemplateBindingEntity::getBangdingZhuangtai, "ACTIVE")
|
.and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark).or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)));
|
Map<String, ElnTemplateBindingEntity> result = new LinkedHashMap<>();
|
for (ElnTemplateBindingEntity binding : list) {
|
if (result.put(binding.getId(), binding) != null) throw new DataException("活动绑定重复");
|
}
|
return result;
|
}
|
|
private Map<String, DocxControlSnapshot> controls(String tenantId, String fileId,
|
Map<String, ElnTemplateBindingEntity> bindings) {
|
Map<String, DocxControlSnapshot> 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<String, ElnTemplateBindingEntity> bindings, String tag) {
|
for (ElnTemplateBindingEntity binding : bindings.values()) {
|
if (Objects.equals(binding.getKongjianBiaoqian(), tag)) return true;
|
}
|
return false;
|
}
|
|
private void verifyReferences(String tenantId, Map<String, ElnTemplateBindingEntity> bindings,
|
Map<String, DocxControlSnapshot> 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<String, String> options = new LinkedHashMap<>();
|
Map<String, Object> config = StringUtils.hasText(version.getKongjianPeizhi())
|
? JsonUtil.getJsonToBean(version.getKongjianPeizhi(), Map.class) : Collections.<String, Object>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<String, String> 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<String, Object> 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.<ElnFieldVersionEntity>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.<ElnRecordSaveOperationEntity>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> T inTenant(String tenantId, TenantAction<T> action) {
|
if (!StringUtils.hasText(tenantId)) throw new DataException("电子记录租户不能为空");
|
try {
|
tenantRoute.switchTo(tenantId.trim());
|
return action.get();
|
} finally {
|
tenantRoute.clear();
|
}
|
}
|
|
@FunctionalInterface
|
interface TenantAction<T> { T get(); }
|
|
interface TenantRoute {
|
void switchTo(String tenantId);
|
void clear();
|
}
|
|
private static final class PreparedValues {
|
private final List<ElnRecordFieldValueEntity> values;
|
private final List<ElnRecordFieldValueEntity> changedValues;
|
private final Set<String> changedBindingIds;
|
private final RepeatPrepared repeat;
|
|
private PreparedValues(List<ElnRecordFieldValueEntity> values,
|
List<ElnRecordFieldValueEntity> changedValues,
|
Set<String> changedBindingIds, RepeatPrepared repeat) {
|
this.values = values;
|
this.changedValues = changedValues;
|
this.changedBindingIds = changedBindingIds;
|
this.repeat = repeat;
|
}
|
}
|
|
private static final class RepeatPrepared {
|
private final List<ElnRecordRepeatValueEntity> inserts = new java.util.ArrayList<>();
|
private final List<ElnRecordRepeatValueEntity> updates = new java.util.ArrayList<>();
|
private final List<ElnRecordRepeatValueEntity> valuesToDelete = new java.util.ArrayList<>();
|
private final List<ElnRecordRepeatRowEntity> rowsToActivate = new java.util.ArrayList<>();
|
private final List<ElnRecordRepeatRowEntity> rowsToDelete = new java.util.ArrayList<>();
|
private final Set<String> changedBindingIds = new LinkedHashSet<>();
|
private final Map<String, CheckboxGroupState> checkboxGroups =
|
new LinkedHashMap<>();
|
|
private static RepeatPrepared empty() { return new RepeatPrepared(); }
|
}
|
|
private static final class CheckboxGroupState {
|
private final String groupCode;
|
private final List<ElnTemplateBindingEntity> 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);
|
}
|
}
|
}
|