package jnpf.elnService.template.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.sdk.AuditClient; import jnpf.base.UserInfo; import jnpf.database.util.TenantDataSourceUtil; import jnpf.elnEntity.ElnDocumentFileVo; import jnpf.elnEntity.field.ElnFieldDefEntity; import jnpf.elnEntity.field.ElnFieldEnums; import jnpf.elnEntity.field.ElnFieldForms; import jnpf.elnEntity.field.ElnFieldVersionEntity; import jnpf.elnEntity.field.ElnFieldVos; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateEnums; import jnpf.elnEntity.template.ElnTemplateForms; import jnpf.elnEntity.template.ElnTemplateVersionEntity; import jnpf.elnEntity.template.ElnTemplateVos; import jnpf.elnMapper.ElnTemplateMapper; import jnpf.elnMapper.ElnFieldDefMapper; import jnpf.elnMapper.ElnFieldVersionMapper; import jnpf.elnMapper.ElnTemplateBindingMapper; import jnpf.elnService.ElnFieldCatalogService; import jnpf.elnService.template.ElnTemplateBindingConfirmationFacade; import jnpf.elnService.template.ElnTemplateBindingDependencyValidator; import jnpf.elnService.template.ElnTemplateBindingLifecycleParticipant; import jnpf.elnService.template.ElnTemplateBindingService; import jnpf.elnService.template.ElnTemplateCheckboxGroupService; import jnpf.elnService.template.ElnTemplateDocumentEvidence; import jnpf.elnService.template.ElnTemplateOperationException; import jnpf.elnService.template.ElnPendingBindingCleanupService; import jnpf.elnService.template.ElnTemplateRepeatGroupService; import jnpf.elnService.template.ElnTemplateVersionCopyParticipant; import jnpf.elnService.template.ElnTemplateStepService; import jnpf.elnService.template.TemplateAnnotationAuthorization; import jnpf.elnService.template.docx.DocxControlConfigMatcher; import jnpf.elnService.template.docx.DocxExpectedControlConfig; import jnpf.elnService.template.docx.DocxControlSnapshot; import jnpf.exception.DataException; import jnpf.model.tenant.TenantVO; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.UUID; import java.util.function.Consumer; import java.util.function.Supplier; @Service public class ElnTemplateBindingServiceImpl implements ElnTemplateBindingService { private static final String ENABLED = "enabled"; private static final String DRAFT = ElnTemplateEnums.TemplateStatus.DRAFT.name(); private static final String PENDING = ElnTemplateEnums.BindingStatus.PENDING.name(); private static final String ACTIVE = ElnTemplateEnums.BindingStatus.ACTIVE.name(); private static final String CANCELLED = ElnTemplateEnums.BindingStatus.CANCELLED.name(); private static final String REMOVED = ElnTemplateEnums.BindingStatus.REMOVED.name(); private static final String PRIMARY = ElnTemplateEnums.ControlRole.PRIMARY.name(); private static final String REFERENCE = ElnTemplateEnums.ControlRole.REFERENCE.name(); private static final String REPEAT = ElnTemplateEnums.ControlRole.REPEAT.name(); private static final String REPEAT_PROTOTYPE_ROW_ID = "prototype"; private final BindingStore store; private final ElnFieldCatalogService fieldCatalogService; private final ElnTemplateRepeatGroupService repeatGroupService; private final List dependencyValidators; private final List copyParticipants; private final List lifecycleParticipants; private final Supplier bindingIdSupplier; private final Supplier referenceInstanceIdSupplier; private final Consumer auditRecorder; private final ElnTemplateDocumentEvidence documentEvidence; private final ElnPendingBindingCleanupService pendingCleanupService; private final Supplier userContextSupplier; private final ElnTemplateBindingConfirmationFacade confirmationFacade; private final StepAssignmentResolver stepAssignmentResolver; private final StepConfigurationReader stepConfigurationReader; private ElnTemplateCheckboxGroupService checkboxGroupService; private long pendingTimeoutMinutes; @Autowired public ElnTemplateBindingServiceImpl(ElnTemplateMapper templateMapper, ElnFieldDefMapper fieldMapper, ElnFieldVersionMapper fieldVersionMapper, ElnTemplateBindingMapper bindingMapper, ElnFieldCatalogService fieldCatalogService, ElnTemplateRepeatGroupService repeatGroupService, ElnTemplateCheckboxGroupService checkboxGroupService, ElnTemplateStepService stepService, List dependencyValidators, List copyParticipants, List lifecycleParticipants, ObjectProvider auditClientProvider, ObjectProvider documentEvidenceProvider, ElnPendingBindingCleanupService pendingCleanupService, @Value("${eln.template-binding.pending-timeout-minutes:30}") long pendingTimeoutMinutes, ElnTemplateBindingConfirmationFacade confirmationFacade) { this(new MyBatisBindingStore(templateMapper, fieldMapper, fieldVersionMapper, bindingMapper), fieldCatalogService, repeatGroupService, dependencyValidators, copyParticipants, lifecycleParticipants, IdUtil::getSnowflakeNextIdStr, () -> UUID.randomUUID().toString().replace("-", ""), event -> { AuditClient client = auditClientProvider.getIfAvailable(); if (client != null) { client.record(event); } }, evidence(documentEvidenceProvider), pendingCleanupService, ElnTemplateBindingServiceImpl::currentUserContext, confirmationFacade, stepService::resolveAssignment, stepService::configuration); this.checkboxGroupService = checkboxGroupService; this.pendingTimeoutMinutes = pendingTimeoutMinutes; } ElnTemplateBindingServiceImpl(BindingStore store, ElnFieldCatalogService fieldCatalogService, ElnTemplateRepeatGroupService repeatGroupService, List dependencyValidators, List copyParticipants, List lifecycleParticipants, Supplier bindingIdSupplier, Supplier referenceInstanceIdSupplier, Consumer auditRecorder, ElnTemplateDocumentEvidence documentEvidence, Supplier userContextSupplier, ElnTemplateBindingConfirmationFacade confirmationFacade) { this(store, fieldCatalogService, repeatGroupService, dependencyValidators, copyParticipants, lifecycleParticipants, bindingIdSupplier, referenceInstanceIdSupplier, auditRecorder, documentEvidence, null, userContextSupplier, confirmationFacade); } ElnTemplateBindingServiceImpl(BindingStore store, ElnFieldCatalogService fieldCatalogService, ElnTemplateRepeatGroupService repeatGroupService, List dependencyValidators, List copyParticipants, List lifecycleParticipants, Supplier bindingIdSupplier, Supplier referenceInstanceIdSupplier, Consumer auditRecorder, ElnTemplateDocumentEvidence documentEvidence, ElnPendingBindingCleanupService pendingCleanupService, Supplier userContextSupplier, ElnTemplateBindingConfirmationFacade confirmationFacade) { this(store, fieldCatalogService, repeatGroupService, dependencyValidators, copyParticipants, lifecycleParticipants, bindingIdSupplier, referenceInstanceIdSupplier, auditRecorder, documentEvidence, pendingCleanupService, userContextSupplier, confirmationFacade, (tenantId, templateId, requestedStepId) -> StringUtils.hasText(requestedStepId) ? requestedStepId.trim() : "test-single-step", templateId -> singleStepConfiguration("test-single-step")); } ElnTemplateBindingServiceImpl(BindingStore store, ElnFieldCatalogService fieldCatalogService, ElnTemplateRepeatGroupService repeatGroupService, List dependencyValidators, List copyParticipants, List lifecycleParticipants, Supplier bindingIdSupplier, Supplier referenceInstanceIdSupplier, Consumer auditRecorder, ElnTemplateDocumentEvidence documentEvidence, Supplier userContextSupplier, ElnTemplateBindingConfirmationFacade confirmationFacade, ElnTemplateCheckboxGroupService checkboxGroupService) { this(store, fieldCatalogService, repeatGroupService, dependencyValidators, copyParticipants, lifecycleParticipants, bindingIdSupplier, referenceInstanceIdSupplier, auditRecorder, documentEvidence, userContextSupplier, confirmationFacade); this.checkboxGroupService = checkboxGroupService; } private ElnTemplateBindingServiceImpl(BindingStore store, ElnFieldCatalogService fieldCatalogService, ElnTemplateRepeatGroupService repeatGroupService, List dependencyValidators, List copyParticipants, List lifecycleParticipants, Supplier bindingIdSupplier, Supplier referenceInstanceIdSupplier, Consumer auditRecorder, ElnTemplateDocumentEvidence documentEvidence, ElnPendingBindingCleanupService pendingCleanupService, Supplier userContextSupplier, ElnTemplateBindingConfirmationFacade confirmationFacade, StepAssignmentResolver stepAssignmentResolver, StepConfigurationReader stepConfigurationReader) { this.store = store; this.fieldCatalogService = fieldCatalogService; this.repeatGroupService = repeatGroupService; this.dependencyValidators = dependencyValidators == null ? Collections.emptyList() : new ArrayList<>(dependencyValidators); this.copyParticipants = copyParticipants == null ? Collections.emptyList() : new ArrayList<>(copyParticipants); this.lifecycleParticipants = lifecycleParticipants == null ? Collections.emptyList() : new ArrayList<>(lifecycleParticipants); this.bindingIdSupplier = bindingIdSupplier; this.referenceInstanceIdSupplier = referenceInstanceIdSupplier; this.auditRecorder = auditRecorder; this.documentEvidence = documentEvidence; this.pendingCleanupService = pendingCleanupService; this.userContextSupplier = userContextSupplier; this.confirmationFacade = confirmationFacade; this.stepAssignmentResolver = stepAssignmentResolver; this.stepConfigurationReader = stepConfigurationReader; this.pendingTimeoutMinutes = 30L; } @Override public ElnTemplateVos.AnnotationContext context(TemplateAnnotationAuthorization authorization) { prepareDocumentEvidence(authorization); ElnTemplateVersionEntity template = requireEditableTemplate(authorization); fieldCatalogService.ensureTestSubmissionFields(); ElnTemplateVos.AnnotationContext result = new ElnTemplateVos.AnnotationContext(); result.setTemplateId(template.getId()); result.setTemplateCode(template.getMubanBianhao()); result.setUpgradeOperationCode(template.getShengbanCaozuoBianma()); result.setStepConfiguration(stepConfigurationReader.read(template.getId())); result.setRepeatGroups(repeatGroupService.list( authorization.getTenantId(), template.getId())); if (checkboxGroupService != null) { result.setCheckboxGroups(checkboxGroupService.list( authorization.getTenantId(), template.getId())); } for (ElnFieldDefEntity field : store.listFields(authorization.getTenantId())) { if (!ENABLED.equals(field.getBizEnabled()) || !fieldVisibleToTemplate(field, template)) { continue; } ElnFieldVos.Detail detail = toFieldDetail(field); for (ElnFieldVersionEntity version : store.listFieldVersions(authorization.getTenantId(), field.getId())) { boolean preRelease = ElnFieldEnums.VersionStatus.PRE_RELEASE.name() .equals(version.getBizStatus()); boolean currentPublished = ElnFieldEnums.VersionStatus.PUBLISHED.name() .equals(version.getBizStatus()) && version.getId().equals(field.getDangqianZiduanBanbenId()); if (preRelease || currentPublished) { detail.getVersions().add(toFieldVersion(version)); } } if (!detail.getVersions().isEmpty()) { result.getFields().add(detail); } } for (ElnTemplateBindingEntity binding : store.listBindings(authorization.getTenantId(), template.getId())) { if (PENDING.equals(binding.getBangdingZhuangtai()) || ACTIVE.equals(binding.getBangdingZhuangtai())) { result.getBindings().add(toBinding(binding)); } if (PENDING.equals(binding.getBangdingZhuangtai()) && StringUtils.hasText(binding.getLaiyuanBangdingId())) { result.getCopiedPendingBindingIds().add(binding.getId()); } } return result; } @Override @Transactional public ElnTemplateVos.Binding reservePrimary(TemplateAnnotationAuthorization authorization, ElnTemplateForms.ReserveBinding form) { prepareDocumentEvidence(authorization); return reservePrimaryPrepared(authorization, form); } private ElnTemplateVos.Binding reservePrimaryPrepared( TemplateAnnotationAuthorization authorization, ElnTemplateForms.ReserveBinding form) { ElnTemplateVersionEntity template = requireEditableTemplateForUpdate(authorization); requireReserveForm(form); boolean repeat = Boolean.TRUE.equals(form.getRepeatRequest()); String stepId = stepAssignmentResolver.resolve(authorization.getTenantId(), template.getId(), form.getStepId()); String repeatGroupCode = repeat ? requireRepeatToken(form.getRepeatGroupCode(), "重复组编码无效") : null; String prototypeRowId = repeat ? requireRepeatToken(form.getRepeatPrototypeRowId(), "原型行标识无效") : null; if (repeat && !REPEAT_PROTOTYPE_ROW_ID.equals(prototypeRowId)) { throw new DataException("当前阶段原型行标识必须为 prototype"); } if (repeat) { repeatGroupService.ensureConfigured(authorization.getTenantId(), template.getId(), repeatGroupCode, form.getMinRows(), form.getMaxRows(), stepId, authorization.getUserId()); } String role = repeat ? REPEAT : PRIMARY; String checkboxGroupCode = normalizeCheckboxGroupCode(form.getCheckboxGroupCode()); String operation = form.getOperationCode().trim(); ElnTemplateBindingEntity retried = store.findByOperation( authorization.getTenantId(), template.getId(), operation); if (retried != null) { if (!role.equals(retried.getKongjianJuese()) || !form.getFieldId().trim().equals(retried.getZiduanDingyiId()) || !form.getFieldVersionId().trim().equals(retried.getZiduanBanbenId()) || !Objects.equals(repeatGroupCode, retried.getRepeatGroupCode()) || !Objects.equals(prototypeRowId, retried.getRepeatPrototypeRowId()) || !Objects.equals(checkboxGroupCode, retried.getCheckboxGroupCode()) || !Objects.equals(repeat ? null : stepId, retried.getStepId())) { throw ElnTemplateOperationException.conflict("绑定操作码已用于其他请求"); } ensureCheckboxGroupConfigured(authorization, template.getId(), checkboxGroupCode, form.getCheckboxGroupMinSelected(), form.getCheckboxGroupMaxSelected()); return toBinding(retried); } ElnFieldDefEntity field = requireField(authorization.getTenantId(), form.getFieldId()); ElnFieldVersionEntity version = requireSelectableVersion( authorization.getTenantId(), field, form.getFieldVersionId(), template); validateCheckboxGroupAssignment(authorization, null, checkboxGroupCode, role, repeatGroupCode, repeat ? null : stepId, version); ensureCheckboxGroupConfigured(authorization, template.getId(), checkboxGroupCode, form.getCheckboxGroupMinSelected(), form.getCheckboxGroupMaxSelected()); for (ElnTemplateBindingEntity binding : store.listBindings(authorization.getTenantId(), template.getId())) { if (!isLive(binding)) continue; if (!repeat && PRIMARY.equals(binding.getKongjianJuese()) && field.getZiduanBianma().equals(binding.getZiduanBianma())) { throw ElnTemplateOperationException.conflict("同一模板字段只能有一个有效主控件"); } if (repeat && REPEAT.equals(binding.getKongjianJuese())) { if (repeatGroupCode.equals(binding.getRepeatGroupCode()) && !prototypeRowId.equals(binding.getRepeatPrototypeRowId())) { throw ElnTemplateOperationException.conflict("同一重复组只能有一条原型行"); } if (repeatGroupCode.equals(binding.getRepeatGroupCode()) && field.getZiduanBianma().equals(binding.getZiduanBianma())) { throw ElnTemplateOperationException.conflict("同一重复组字段只能绑定一次"); } } } String tag = repeat ? "eln.repeat." + repeatGroupCode + "." + prototypeRowId + "." + field.getZiduanBianma() : "eln.field." + field.getZiduanBianma(); ElnTemplateBindingEntity created = newBinding(authorization, template, field, version, role, null, tag, operation); created.setRepeatGroupCode(repeatGroupCode); created.setRepeatPrototypeRowId(prototypeRowId); created.setCheckboxGroupCode(checkboxGroupCode); created.setStepId(repeat ? null : stepId); insert(created); audit("ELN_TEMPLATE_BINDING_RESERVE", authorization.getTenantId(), authorization.getUserId(), operation, template.getId(), created.getId(), null, created, null, "SUCCESS"); return toBinding(created); } @Override @Transactional public ElnTemplateVos.Binding reserveReference(TemplateAnnotationAuthorization authorization, String primaryBindingId, String operationCode) { prepareDocumentEvidence(authorization); ElnTemplateVersionEntity template = requireEditableTemplateForUpdate(authorization); String operation = requireText(operationCode, "操作码不能为空"); ElnTemplateBindingEntity retried = store.findByOperation( authorization.getTenantId(), template.getId(), operation); if (retried != null) { if (!REFERENCE.equals(retried.getKongjianJuese()) || !trim(primaryBindingId).equals(retried.getYinyongShiliId())) { throw ElnTemplateOperationException.conflict("绑定操作码已用于其他请求"); } return toBinding(retried); } ElnTemplateBindingEntity primary = requireBindingForUpdate( authorization, primaryBindingId); if (!ACTIVE.equals(primary.getBangdingZhuangtai()) || !PRIMARY.equals(primary.getKongjianJuese())) { throw ElnTemplateOperationException.conflict("引用控件必须指向有效主控件"); } ElnFieldDefEntity field = requireField( authorization.getTenantId(), primary.getZiduanDingyiId()); ElnFieldVersionEntity version = requireSelectableVersion(authorization.getTenantId(), field, primary.getZiduanBanbenId(), template); String instanceId = referenceInstanceIdSupplier.get(); if (instanceId == null || !instanceId.matches("[0-9a-fA-F]{32}")) { throw new DataException("引用实例标识生成失败"); } String tag = "eln.ref." + field.getZiduanBianma() + "." + instanceId.toLowerCase(); ElnTemplateBindingEntity created = newBinding(authorization, template, field, version, REFERENCE, primary.getId(), tag, operation); insert(created); audit("ELN_TEMPLATE_BINDING_RESERVE", authorization.getTenantId(), authorization.getUserId(), operation, template.getId(), created.getId(), null, created, null, "SUCCESS"); return toBinding(created); } @Override @Transactional public ElnTemplateVos.Binding changeCheckboxGroup( TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.ChangeCheckboxGroup form) { if (form == null || form.getExpectedVersion() == null) { throw new DataException("绑定期望版本不能为空"); } String operation = requireText(form.getOperationCode(), "操作码不能为空"); String checkboxGroupCode = normalizeCheckboxGroupCode(form.getCheckboxGroupCode()); ElnTemplateBindingEntity replayed = store.findByOperation( authorization.getTenantId(), authorization.getTemplateId(), operation); if (replayed != null) { if (replayed.getId().equals(trim(bindingId)) && Objects.equals(checkboxGroupCode, replayed.getCheckboxGroupCode())) { return toBinding(replayed); } throw ElnTemplateOperationException.conflict("选择组操作码已用于其他请求"); } requireEditableTemplateForUpdate(authorization); ElnTemplateBindingEntity current = requireBindingForUpdate(authorization, bindingId); if (!isLive(current)) { throw ElnTemplateOperationException.conflict("只有待确认或有效绑定可以修改选择组"); } if (!form.getExpectedVersion().equals(current.getFVersion())) { throw ElnTemplateOperationException.conflict("绑定状态已变化,请刷新后重试"); } ElnFieldVersionEntity version = requireFieldVersion( authorization.getTenantId(), current.getZiduanBanbenId()); validateCheckboxGroupAssignment(authorization, current.getId(), checkboxGroupCode, current.getKongjianJuese(), current.getRepeatGroupCode(), current.getStepId(), version); ensureCheckboxGroupConfigured(authorization, authorization.getTemplateId(), checkboxGroupCode, form.getCheckboxGroupMinSelected(), form.getCheckboxGroupMaxSelected()); if (Objects.equals(checkboxGroupCode, current.getCheckboxGroupCode())) { return toBinding(current); } ElnTemplateBindingEntity after = copy(current); after.setCheckboxGroupCode(checkboxGroupCode); after.setCaozuoBianma(operation); casExpected(current, after, current.getBangdingZhuangtai(), form.getExpectedVersion()); ElnTemplateBindingEntity persisted = requireBinding(authorization, current.getId()); removeUnusedCheckboxGroups(authorization.getTenantId(), authorization.getTemplateId(), authorization.getUserId()); audit("ELN_TEMPLATE_BINDING_CHECKBOX_GROUP_CHANGE", authorization.getTenantId(), authorization.getUserId(), operation, authorization.getTemplateId(), current.getId(), current, persisted, null, "SUCCESS"); return toBinding(persisted); } @Override @Transactional public ElnTemplateVos.CheckboxGroup changeCheckboxGroupRule( TemplateAnnotationAuthorization authorization, String groupCode, ElnTemplateForms.ChangeCheckboxGroupRule form) { if (form == null || form.getExpectedVersion() == null) { throw new DataException("选择组期望版本不能为空"); } String operation = requireText(form.getOperationCode(), "操作码不能为空"); requireEditableTemplateForUpdate(authorization); if (checkboxGroupService == null) throw new DataException("选择组服务不可用"); ElnTemplateVos.CheckboxGroup before = checkboxGroupService.list( authorization.getTenantId(), authorization.getTemplateId()).stream() .filter(item -> Objects.equals(item.getGroupCode(), trim(groupCode))) .findFirst().orElse(null); ElnTemplateVos.CheckboxGroup changed = checkboxGroupService.change( authorization.getTenantId(), authorization.getTemplateId(), groupCode, form.getMinSelected(), form.getMaxSelected(), form.getExpectedVersion(), authorization.getUserId()); audit("ELN_TEMPLATE_CHECKBOX_GROUP_RULE_CHANGE", authorization.getTenantId(), authorization.getUserId(), operation, authorization.getTemplateId(), changed.getGroupCode(), before, changed, null, "SUCCESS"); return changed; } @Override @Transactional public ElnTemplateVos.Binding createPrivateFieldAndReserve( TemplateAnnotationAuthorization authorization, ElnTemplateForms.PrivateField form) { prepareDocumentEvidence(authorization); ElnTemplateVersionEntity template = requireEditableTemplateForUpdate(authorization); if (form == null || !StringUtils.hasText(form.getOperationCode())) { throw new DataException("私有字段操作码不能为空"); } ElnTemplateBindingEntity retried = store.findByOperation(authorization.getTenantId(), template.getId(), form.getOperationCode().trim()); if (retried != null) { requireSamePrivateFieldRequest(authorization, template, retried, form); ensureCheckboxGroupConfigured(authorization, template.getId(), normalizeCheckboxGroupCode(form.getCheckboxGroupCode()), form.getCheckboxGroupMinSelected(), form.getCheckboxGroupMaxSelected()); return toBinding(retried); } ElnFieldForms.TemplateFieldCreate command = new ElnFieldForms.TemplateFieldCreate(); command.setTenantId(authorization.getTenantId()); command.setTemplateCode(template.getMubanBianhao()); command.setOperatorId(authorization.getUserId()); command.setBeizhu(form.getBeizhu()); command.setZiduanMingcheng(form.getZiduanMingcheng()); command.setShujuLeixing(form.getShujuLeixing()); command.setDanwei(form.getDanwei()); command.setShifouBitian(form.getShifouBitian()); command.setPiciMoshi(ElnFieldEnums.BatchMode.RECORD_PRIVATE.name()); command.setKongjianPeizhi(form.getKongjianPeizhi()); command.setBinding(manualBinding()); ElnFieldVos.Detail created = fieldCatalogService.createTemplateFieldAndPreRelease(command); if (created == null || !StringUtils.hasText(created.getId()) || created.getVersions() == null || created.getVersions().size() != 1 || !ElnFieldEnums.VersionStatus.PRE_RELEASE.name().equals( created.getVersions().get(0).getBizStatus())) { throw new DataException("私有字段创建结果无效"); } ElnTemplateForms.ReserveBinding reserve = new ElnTemplateForms.ReserveBinding(); reserve.setFieldId(created.getId()); reserve.setFieldVersionId(created.getVersions().get(0).getId()); reserve.setOperationCode(form.getOperationCode()); reserve.setStepId(form.getStepId()); reserve.setCheckboxGroupCode(form.getCheckboxGroupCode()); reserve.setCheckboxGroupMinSelected(form.getCheckboxGroupMinSelected()); reserve.setCheckboxGroupMaxSelected(form.getCheckboxGroupMaxSelected()); return reservePrimaryPrepared(authorization, reserve); } private void ensureCheckboxGroupConfigured(TemplateAnnotationAuthorization authorization, String templateId, String groupCode, Integer minSelected, Integer maxSelected) { if (checkboxGroupService == null || groupCode == null) return; checkboxGroupService.ensureConfigured(authorization.getTenantId(), templateId, groupCode, minSelected, maxSelected, authorization.getUserId()); } @Override @Transactional public void activate(TemplateAnnotationAuthorization authorization, String bindingId, String operationCode) { requireEditableTemplateForUpdate(authorization); ElnTemplateBindingEntity observed = requireBinding(authorization, bindingId); ElnTemplateBindingEntity lockedPrimary = null; if (REFERENCE.equals(observed.getKongjianJuese())) { lockedPrimary = requireBindingForUpdate( authorization, observed.getYinyongShiliId()); } ElnTemplateBindingEntity before = requireBindingForUpdate(authorization, bindingId); if (REFERENCE.equals(before.getKongjianJuese())) { requireValidReferencePrimary(before, lockedPrimary); } rejectCopiedBindingSingleTransition(before); if (ACTIVE.equals(before.getBangdingZhuangtai())) { return; } if (!PENDING.equals(before.getBangdingZhuangtai())) { throw ElnTemplateOperationException.conflict("只有待确认绑定可以激活"); } ElnTemplateBindingEntity after = copy(before); after.setBangdingZhuangtai(ACTIVE); after.setZhuangtaiYuanyin(null); cas(before, after, PENDING); ElnTemplateBindingEntity persisted = requireBinding(authorization, bindingId); audit("ELN_TEMPLATE_BINDING_ACTIVATE", authorization.getTenantId(), authorization.getUserId(), requireText(operationCode, "操作码不能为空"), authorization.getTemplateId(), bindingId, before, persisted, null, "SUCCESS"); } @Override @Transactional public void cancel(TemplateAnnotationAuthorization authorization, String bindingId, String operationCode, String reason) { requireEditableTemplateForUpdate(authorization); ElnTemplateBindingEntity before = requireBinding(authorization, bindingId); rejectCopiedBindingSingleTransition(before); if (CANCELLED.equals(before.getBangdingZhuangtai())) { return; } if (!PENDING.equals(before.getBangdingZhuangtai())) { throw ElnTemplateOperationException.conflict("只有待确认绑定可以取消"); } ElnTemplateBindingEntity after = copy(before); after.setBangdingZhuangtai(CANCELLED); after.setZhuangtaiYuanyin(requireText(reason, "取消原因不能为空")); cas(before, after, PENDING); terminateBinding(authorization.getTenantId(), bindingId, authorization.getUserId()); ElnTemplateBindingEntity persisted = requireBinding(authorization, bindingId); audit("ELN_TEMPLATE_BINDING_CANCEL", authorization.getTenantId(), authorization.getUserId(), requireText(operationCode, "操作码不能为空"), authorization.getTemplateId(), bindingId, before, persisted, reason, "SUCCESS"); } @Override public ElnTemplateVos.RepairContext repairContext(TemplateAnnotationAuthorization authorization, String bindingId) { requireEditableTemplate(authorization); ElnTemplateBindingEntity binding = requireActiveBinding(authorization, bindingId); ElnTemplateVos.ChangeCheck check = changeCheck("repair", authorization, Collections.singletonList(binding), activeSnapshots(authorization), Collections.emptyList(), null); ElnTemplateVos.RepairContext result = new ElnTemplateVos.RepairContext(); copyCheck(check, result); result.setBinding(toBinding(binding)); return result; } @Override public void confirmRepair(TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.RepairConfirm form) { confirmationFacade.execute(() -> { ElnTemplateBindingEntity current = requireBinding(authorization, bindingId); if (ACTIVE.equals(current.getBangdingZhuangtai()) && form != null && Objects.equals(form.getOperationCode(), current.getCaozuoBianma())) { return; } requireEditableTemplateForUpdate(authorization); current = requireBinding(authorization, bindingId); if (ACTIVE.equals(current.getBangdingZhuangtai()) && form != null && Objects.equals(form.getOperationCode(), current.getCaozuoBianma())) { return; } ElnTemplateVos.RepairContext check = repairContext(authorization, bindingId); requireConfirmation(check, form); ElnTemplateBindingEntity after = copy(current); after.setCaozuoBianma(form.getOperationCode()); casExpected(current, after, ACTIVE, form.getExpectedVersions().get(bindingId)); ElnTemplateBindingEntity persisted = requireBinding(authorization, bindingId); audit("ELN_TEMPLATE_BINDING_REPAIR", authorization.getTenantId(), authorization.getUserId(), form.getOperationCode(), authorization.getTemplateId(), bindingId, current, persisted, null, "SUCCESS"); }, error -> failureAudit("REPAIR", authorization.getTenantId(), authorization.getUserId(), operation(form), authorization.getTemplateId(), bindingId, error)); } @Override @Transactional public ElnTemplateVos.ChangeCheck checkUpgrade(TemplateAnnotationAuthorization authorization, String bindingId, String targetVersionId) { requireEditableTemplate(authorization); ElnTemplateBindingEntity primary = requireActivePrimaryForUpdate( authorization, bindingId); ElnFieldDefEntity field = requireField( authorization.getTenantId(), primary.getZiduanDingyiId()); ElnFieldVersionEntity target = requireSelectableVersion(authorization.getTenantId(), field, targetVersionId, requireTemplate(authorization)); List affected = fieldBindingFamily(authorization, primary); List dependencies = validateUpgrade( authorization, ids(affected), target.getId()); List controls = activeSnapshots(authorization); Set affectedIds = ids(affected); for (ElnTemplateVos.ControlSnapshot control : controls) { if (affectedIds.contains(control.getBindingId())) { control.setFieldVersionId(target.getId()); control.setControlAlias(target.getZiduanMingcheng()); control.setControlType(controlType(target.getShujuLeixing())); control.setControlConfig(DocxExpectedControlConfig.from( target.getShujuLeixing(), target.getKongjianPeizhi())); } } return changeCheck("upgrade:" + target.getId(), authorization, affected, controls, dependencies, target.getId()); } @Override public void confirmUpgrade(TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.UpgradeConfirm form) { confirmationFacade.execute(() -> confirmUpgradeInternal(authorization, bindingId, form), error -> failureAudit("UPGRADE", authorization.getTenantId(), authorization.getUserId(), operation(form), authorization.getTemplateId(), bindingId, error)); } private void confirmUpgradeInternal(TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.UpgradeConfirm form) { if (isUpgradeRetry(authorization, bindingId, form)) { return; } requireEditableTemplateForUpdate(authorization); if (isUpgradeRetry(authorization, bindingId, form)) { return; } ElnTemplateVos.ChangeCheck check = checkUpgrade( authorization, bindingId, form == null ? null : form.getTargetFieldVersionId()); requireConfirmation(check, form); List beforeSnapshots = new ArrayList<>(); List afterSnapshots = new ArrayList<>(); for (Map.Entry entry : check.getExpectedVersions().entrySet()) { ElnTemplateBindingEntity before = requireActiveBinding(authorization, entry.getKey()); beforeSnapshots.add(copy(before)); ElnTemplateBindingEntity after = copy(before); ElnFieldVersionEntity target = requireFieldVersion( authorization.getTenantId(), form.getTargetFieldVersionId()); validateCheckboxGroupAssignment(authorization, before.getId(), before.getCheckboxGroupCode(), before.getKongjianJuese(), before.getRepeatGroupCode(), before.getStepId(), target); after.setZiduanBanbenId(target.getId()); after.setKongjianBieming(target.getZiduanMingcheng()); after.setKongjianLeixing(controlType(target.getShujuLeixing())); after.setCaozuoBianma(bindingOperationCode(form.getOperationCode(), before.getId())); casExpected(before, after, ACTIVE, entry.getValue()); afterSnapshots.add(requireActiveBinding(authorization, before.getId())); } audit("ELN_TEMPLATE_BINDING_UPGRADE", authorization.getTenantId(), authorization.getUserId(), form.getOperationCode(), authorization.getTemplateId(), bindingId, beforeSnapshots, afterSnapshots, null, "SUCCESS"); } @Override @Transactional public ElnTemplateVos.ChangeCheck checkRemove(TemplateAnnotationAuthorization authorization, String bindingId) { requireEditableTemplate(authorization); ElnTemplateBindingEntity binding = requireActiveBindingForUpdate( authorization, bindingId); Set affected = Collections.singleton(binding.getId()); if (PRIMARY.equals(binding.getKongjianJuese()) && hasLiveReferences(authorization, binding)) { throw new DataException("主控件仍有引用,不能删除"); } List dependencies = validateRemoval(authorization, affected); List controls = activeSnapshots(authorization); controls.removeIf(item -> binding.getId().equals(item.getBindingId())); ElnTemplateVos.ChangeCheck result = changeCheck("remove", authorization, Collections.singletonList(binding), controls, dependencies, null); audit("ELN_TEMPLATE_BINDING_REMOVE_CHECK", authorization.getTenantId(), authorization.getUserId(), result.getOperationCode(), authorization.getTemplateId(), bindingId, binding, result, null, "SUCCESS"); return result; } @Override public void confirmRemove(TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.RemoveConfirm form) { confirmationFacade.execute(() -> { ElnTemplateBindingEntity current = requireBinding(authorization, bindingId); if (REMOVED.equals(current.getBangdingZhuangtai()) && form != null && Objects.equals( form.getOperationCode(), current.getCaozuoBianma())) { return; } requireEditableTemplateForUpdate(authorization); current = requireBinding(authorization, bindingId); if (REMOVED.equals(current.getBangdingZhuangtai()) && form != null && Objects.equals( form.getOperationCode(), current.getCaozuoBianma())) { return; } ElnTemplateVos.ChangeCheck check = checkRemove(authorization, bindingId); requireConfirmation(check, form); requireObservedControlTags(check, form, current.getKongjianBiaoqian()); ElnTemplateBindingEntity after = copy(current); after.setBangdingZhuangtai(REMOVED); after.setCaozuoBianma(form.getOperationCode()); casExpected(current, after, ACTIVE, form.getExpectedVersions().get(bindingId)); terminateBinding(authorization.getTenantId(), bindingId, authorization.getUserId()); removeUnusedCheckboxGroups(authorization.getTenantId(), authorization.getTemplateId(), authorization.getUserId()); ElnTemplateBindingEntity persisted = requireBinding(authorization, bindingId); audit("ELN_TEMPLATE_BINDING_REMOVE", authorization.getTenantId(), authorization.getUserId(), form.getOperationCode(), authorization.getTemplateId(), bindingId, current, persisted, null, "SUCCESS"); }, error -> failureAudit("REMOVE", authorization.getTenantId(), authorization.getUserId(), operation(form), authorization.getTemplateId(), bindingId, error)); } @Override public void confirmRemoveMissing(String bindingId, ElnTemplateForms.RemoveMissingConfirm form) { UserContext user = requireUserContext(); confirmationFacade.execute(() -> { if (form == null || form.getExpectedVersion() == null) { throw new DataException("绑定期望版本不能为空"); } String operation = requireText(form.getOperationCode(), "操作码不能为空"); String reason = requireText(form.getReason(), "确认删除原因不能为空"); ElnTemplateBindingEntity observed = requireBinding(user.getTenantId(), bindingId); if (REMOVED.equals(observed.getBangdingZhuangtai()) && operation.equals(observed.getCaozuoBianma())) { if (Objects.equals(reason, trim(observed.getZhuangtaiYuanyin()))) { return; } throw ElnTemplateOperationException.conflict( "确认删除操作码已用于其他原因"); } ElnTemplateVersionEntity template = requireEditableTemplateForUpdate( user.getTenantId(), observed.getMubanId()); ElnTemplateBindingEntity current = requireBindingForUpdate( user.getTenantId(), bindingId); if (REMOVED.equals(current.getBangdingZhuangtai()) && operation.equals(current.getCaozuoBianma())) { if (Objects.equals(reason, trim(current.getZhuangtaiYuanyin()))) { return; } throw ElnTemplateOperationException.conflict( "确认删除操作码已用于其他原因"); } if (!ACTIVE.equals(current.getBangdingZhuangtai())) { throw ElnTemplateOperationException.conflict("只有有效绑定可以确认缺失删除"); } String fileId = uniqueDocxFileId(template.getMubanFile()); if (documentEvidence.containsControlTag( user.getTenantId(), fileId, current.getKongjianBiaoqian())) { throw new DataException("服务端文档证据显示控件仍然存在"); } if (PRIMARY.equals(current.getKongjianJuese()) && hasLiveReferences(user.getTenantId(), template.getId(), current)) { throw new DataException("主控件仍有引用,不能删除"); } validateRemoval(user.getTenantId(), template.getId(), Collections.singleton(current.getId())); ElnTemplateBindingEntity after = copy(current); after.setBangdingZhuangtai(REMOVED); after.setCaozuoBianma(operation); after.setZhuangtaiYuanyin(reason); casExpected(current, after, ACTIVE, form.getExpectedVersion()); terminateBinding(user.getTenantId(), bindingId, user.getUserId()); removeUnusedCheckboxGroups(user.getTenantId(), template.getId(), user.getUserId()); ElnTemplateBindingEntity persisted = requireBinding(user.getTenantId(), bindingId); audit("ELN_TEMPLATE_BINDING_REMOVE", user.getTenantId(), user.getUserId(), operation, template.getId(), bindingId, current, persisted, reason, "SUCCESS"); }, error -> failureAudit("REMOVE", user.getTenantId(), user.getUserId(), form == null ? null : form.getOperationCode(), templateIdForFailure(user.getTenantId(), bindingId), bindingId, error)); } private void terminateBinding(String tenantId, String bindingId, String userId) { for (ElnTemplateBindingLifecycleParticipant participant : lifecycleParticipants) { participant.bindingTerminated(tenantId, bindingId, userId); } } private void removeUnusedCheckboxGroups(String tenantId, String templateId, String operatorId) { if (checkboxGroupService == null) return; Set usedGroupCodes = new LinkedHashSet<>(); for (ElnTemplateBindingEntity binding : store.listBindings(tenantId, templateId)) { String groupCode = trim(binding.getCheckboxGroupCode()); if (isLive(binding) && groupCode != null) usedGroupCodes.add(groupCode); } checkboxGroupService.removeUnused( tenantId, templateId, usedGroupCodes, operatorId); } @Override @Transactional public ElnTemplateVos.ChangeCheck checkPromoteReference( TemplateAnnotationAuthorization authorization, String referenceBindingId) { requireEditableTemplate(authorization); ElnTemplateBindingEntity reference = requireActiveBinding(authorization, referenceBindingId); if (!REFERENCE.equals(reference.getKongjianJuese())) { throw new DataException("只能提升引用控件"); } ElnTemplateBindingEntity primary = requireActivePrimaryForUpdate( authorization, reference.getYinyongShiliId()); reference = requireActiveBindingForUpdate(authorization, referenceBindingId); requireValidReferencePrimary(reference, primary); List affected = fieldBindingFamily(authorization, primary); if (!ids(affected).contains(reference.getId())) { throw new DataException("待提升引用不属于主控件引用集合"); } List dependencies = validatePromotion( authorization, primary.getId(), reference.getId()); List controls = activeSnapshots(authorization); controls.removeIf(item -> primary.getId().equals(item.getBindingId())); for (ElnTemplateVos.ControlSnapshot control : controls) { if (reference.getId().equals(control.getBindingId())) { control.setControlRole(PRIMARY); control.setPrimaryBindingId(null); control.setControlTag("eln.field." + reference.getZiduanBianma()); control.setStepId(primary.getStepId()); } else if (primary.getId().equals(control.getPrimaryBindingId())) { control.setPrimaryBindingId(reference.getId()); } } return changeCheck("promote", authorization, affected, controls, dependencies, null); } @Override public void confirmPromoteReference(TemplateAnnotationAuthorization authorization, String referenceBindingId, ElnTemplateForms.PromoteConfirm form) { confirmationFacade.execute(() -> { ElnTemplateBindingEntity possibleRetry = requireBinding(authorization, referenceBindingId); if (PRIMARY.equals(possibleRetry.getKongjianJuese()) && ACTIVE.equals( possibleRetry.getBangdingZhuangtai()) && form != null && Objects.equals(bindingOperationCode( form.getOperationCode(), possibleRetry.getId()), possibleRetry.getCaozuoBianma())) { return; } requireEditableTemplateForUpdate(authorization); possibleRetry = requireBinding(authorization, referenceBindingId); if (PRIMARY.equals(possibleRetry.getKongjianJuese()) && ACTIVE.equals( possibleRetry.getBangdingZhuangtai()) && form != null && Objects.equals(bindingOperationCode( form.getOperationCode(), possibleRetry.getId()), possibleRetry.getCaozuoBianma())) { return; } ElnTemplateVos.ChangeCheck check = checkPromoteReference( authorization, referenceBindingId); requireConfirmation(check, form); ElnTemplateBindingEntity reference = requireActiveBinding( authorization, referenceBindingId); requireObservedControlTags(check, form, reference.getKongjianBiaoqian()); ElnTemplateBindingEntity primary = requireActiveBinding( authorization, reference.getYinyongShiliId()); List family = fieldBindingFamily(authorization, primary); List transitionOrder = new ArrayList<>(); transitionOrder.add(primary); transitionOrder.add(reference); for (ElnTemplateBindingEntity member : family) { if (!primary.getId().equals(member.getId()) && !reference.getId().equals(member.getId())) { transitionOrder.add(member); } } List changed = new ArrayList<>(); for (ElnTemplateBindingEntity before : transitionOrder) { ElnTemplateBindingEntity after = copy(before); if (primary.getId().equals(before.getId())) { after.setBangdingZhuangtai(REMOVED); } else if (reference.getId().equals(before.getId())) { after.setKongjianJuese(PRIMARY); after.setYinyongShiliId(null); after.setKongjianBiaoqian("eln.field." + reference.getZiduanBianma()); after.setStepId(primary.getStepId()); } else { after.setYinyongShiliId(reference.getId()); } after.setCaozuoBianma(bindingOperationCode( form.getOperationCode(), before.getId())); casExpected(before, after, ACTIVE, form.getExpectedVersions().get(before.getId())); changed.add(requireBinding(authorization, before.getId())); } audit("ELN_TEMPLATE_BINDING_PROMOTE", authorization.getTenantId(), authorization.getUserId(), form.getOperationCode(), authorization.getTemplateId(), referenceBindingId, family, changed, null, "SUCCESS"); }, error -> failureAudit("PROMOTE", authorization.getTenantId(), authorization.getUserId(), operation(form), authorization.getTemplateId(), referenceBindingId, error)); } @Override public void confirmVersionCopy(TemplateAnnotationAuthorization authorization, ElnTemplateForms.VersionCopyConfirm form) { confirmationFacade.execute(() -> confirmVersionCopyInternal(authorization, form), error -> failureAudit("VERSION_COPY_CONFIRM", authorization.getTenantId(), authorization.getUserId(), form == null ? null : form.getUpgradeOperationCode(), authorization.getTemplateId(), null, error)); } private void confirmVersionCopyInternal(TemplateAnnotationAuthorization authorization, ElnTemplateForms.VersionCopyConfirm form) { ElnTemplateVersionEntity template = requireEditableTemplateForUpdate(authorization); if (form == null || !StringUtils.hasText(form.getUpgradeOperationCode()) || !form.getUpgradeOperationCode().trim().equals(template.getShengbanCaozuoBianma())) { throw new DataException("升版操作码无效"); } String upgradeOperation = form.getUpgradeOperationCode().trim(); List copies = copiedBindings(authorization); if (copies.isEmpty()) { throw new DataException("本次升版没有可确认的复制绑定"); } boolean allPending = true; boolean allActive = true; for (ElnTemplateBindingEntity binding : copies) { String expectedOperation = upgradeOperation + ":" + binding.getLaiyuanBangdingId(); if (!expectedOperation.equals(binding.getCaozuoBianma())) { throw new DataException("升版复制绑定操作码无效"); } allPending &= PENDING.equals(binding.getBangdingZhuangtai()); allActive &= ACTIVE.equals(binding.getBangdingZhuangtai()); } if (allActive) { requireVersionCopyRetryVersions(copies, form.getExpectedVersions()); requireControls(snapshots(copies), form.getControls()); return; } if (!allPending) { throw ElnTemplateOperationException.conflict( "升版复制绑定必须全部为待确认或全部为已激活状态"); } for (ElnTemplateBindingEntity binding : copies) { if (binding.getDaoqiShijian() != null) { throw ElnTemplateOperationException.conflict("升版复制绑定状态无效"); } ElnFieldVersionEntity version = requireFieldVersion( authorization.getTenantId(), binding.getZiduanBanbenId()); ElnFieldDefEntity field = requireField( authorization.getTenantId(), binding.getZiduanDingyiId()); if (!field.getId().equals(version.getZiduanDingyiId())) { throw new DataException("升版复制绑定的字段版本关系无效"); } boolean published = ElnFieldEnums.VersionStatus.PUBLISHED.name() .equals(version.getBizStatus()); boolean privatePreRelease = ElnFieldEnums.VersionStatus.PRE_RELEASE.name() .equals(version.getBizStatus()) && ElnFieldEnums.Scope.TEMPLATE.name().equals(field.getZuoyongFanwei()) && template.getMubanBianhao().equals(field.getMubanBianhao()); if (!published && !privatePreRelease) { throw new DataException("升版复制只能继承已发布字段版本或同模板的预发布私有字段版本"); } } Map expected = versions(copies); requireExpectedVersions(expected, form.getExpectedVersions()); requireControls(snapshots(copies), form.getControls()); Map bindingMap = new LinkedHashMap<>(); List beforeSnapshots = new ArrayList<>(); List afterSnapshots = new ArrayList<>(); copies.sort(Comparator.comparing(ElnTemplateBindingEntity::getLaiyuanBangdingId)); for (ElnTemplateBindingEntity before : copies) { beforeSnapshots.add(copy(before)); ElnTemplateBindingEntity after = copy(before); after.setBangdingZhuangtai(ACTIVE); casExpected(before, after, PENDING, expected.get(before.getId())); afterSnapshots.add(requireActiveBinding(authorization, before.getId())); bindingMap.put(before.getLaiyuanBangdingId(), before.getId()); } for (ElnTemplateVersionCopyParticipant participant : copyParticipants) { participant.afterBindingsActivated(authorization.getTenantId(), template.getLaiyuanMubanId(), template.getId(), bindingMap); } audit("ELN_TEMPLATE_BINDING_VERSION_COPY_CONFIRM", authorization.getTenantId(), authorization.getUserId(), form.getUpgradeOperationCode(), template.getId(), null, beforeSnapshots, afterSnapshots, null, "SUCCESS"); } private ElnTemplateBindingEntity newBinding(TemplateAnnotationAuthorization authorization, ElnTemplateVersionEntity template, ElnFieldDefEntity field, ElnFieldVersionEntity version, String role, String primaryBindingId, String tag, String operation) { ElnTemplateBindingEntity created = new ElnTemplateBindingEntity(); created.setId(bindingIdSupplier.get()); created.setFTenantId(authorization.getTenantId()); created.setMubanId(template.getId()); created.setMubanBianhao(template.getMubanBianhao()); created.setZiduanDingyiId(field.getId()); created.setZiduanBianma(field.getZiduanBianma()); created.setZiduanBanbenId(version.getId()); created.setKongjianBiaoqian(tag); created.setKongjianBieming(version.getZiduanMingcheng()); created.setKongjianLeixing(controlType(version.getShujuLeixing())); created.setKongjianJuese(role); created.setYinyongShiliId(primaryBindingId); created.setBangdingZhuangtai(PENDING); created.setCaozuoBianma(operation); if (pendingTimeoutMinutes <= 0) { throw new DataException("PENDING 绑定超时时间配置无效"); } long timeoutMillis; try { timeoutMillis = Math.multiplyExact(pendingTimeoutMinutes, 60L * 1000L); } catch (ArithmeticException e) { throw new DataException("PENDING 绑定超时时间配置无效"); } created.setDaoqiShijian(new Date(System.currentTimeMillis() + timeoutMillis)); created.setFVersion(0); created.setFDeleteMark(0); created.setFCreatorUserId(authorization.getUserId()); created.setFLastModifyUserId(authorization.getUserId()); return created; } private ElnTemplateVos.ChangeCheck changeCheck(String action, TemplateAnnotationAuthorization authorization, List affected, List controls, List dependencies, String targetVersionId) { ElnTemplateVos.ChangeCheck result = new ElnTemplateVos.ChangeCheck(); result.setExpectedVersions(versions(affected)); result.setControls(sortedSnapshots(controls)); result.setDependencies(dependencies == null ? new ArrayList<>() : dependencies); result.setOperationCode(operationCode(action, authorization, affected, targetVersionId)); return result; } private void requireConfirmation(ElnTemplateVos.ChangeCheck check, ElnTemplateForms.ChangeConfirm form) { if (form == null || !check.getOperationCode().equals(form.getOperationCode())) { throw ElnTemplateOperationException.conflict("预检操作码无效,请重新预检"); } requireExpectedVersions(check.getExpectedVersions(), form.getExpectedVersions()); requireControls(check.getControls(), form.getControls()); } private static void requireObservedControlTags(ElnTemplateVos.ChangeCheck check, ElnTemplateForms.ChangeConfirm form, String removedTag) { List observedTags = form.getObservedControlTags(); if (observedTags == null) { throw ElnTemplateOperationException.conflict("DOCX 控件标签证据未提交"); } Set observed = new LinkedHashSet<>(); for (String tag : observedTags) { if (!StringUtils.hasText(tag) || !observed.add(tag)) { throw ElnTemplateOperationException.conflict("DOCX 控件标签证据无效"); } } for (ElnTemplateVos.ControlSnapshot control : check.getControls()) { String expectedTag = control.getControlTag(); if (Collections.frequency(observedTags, expectedTag) != 1) { throw ElnTemplateOperationException.conflict("DOCX 控件标签证据不完整"); } } if (observed.contains(removedTag)) { throw ElnTemplateOperationException.conflict("DOCX 控件标签证据包含已移除控件"); } } private static void requireExpectedVersions(Map expected, Map actual) { if (actual == null || !expected.equals(actual)) { throw ElnTemplateOperationException.conflict("受影响绑定版本集合不完整或已变化"); } } private static void requireControls(List expected, List actual) { if (!canonicalControls(expected).equals(canonicalControls(actual))) { throw ElnTemplateOperationException.conflict("控件快照不完整或与预检结果不一致"); } Map actualByBindingId = new LinkedHashMap<>(); for (ElnTemplateVos.ControlSnapshot item : actual) { if (item == null || actualByBindingId.put(item.getBindingId(), item) != null) { throw ElnTemplateOperationException.conflict("控件快照不完整或与预检结果不一致"); } } for (ElnTemplateVos.ControlSnapshot item : expected) { ElnTemplateVos.ControlSnapshot observed = actualByBindingId.get(item.getBindingId()); if (!DocxControlConfigMatcher.matches(item.getControlType(), item.getControlConfig(), observed.getControlConfig())) { throw ElnTemplateOperationException.conflict("控件快照不完整或与预检结果不一致"); } } } private List validateUpgrade( TemplateAnnotationAuthorization authorization, Set bindingIds, String targetVersionId) { List result = new ArrayList<>(); for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { addDiagnostics(result, validator.validateUpgrade(authorization.getTenantId(), authorization.getTemplateId(), bindingIds, targetVersionId)); } rejectDependencyErrors(result); return result; } private List validateRemoval( TemplateAnnotationAuthorization authorization, Set bindingIds) { return validateRemoval(authorization.getTenantId(), authorization.getTemplateId(), bindingIds); } private List validateRemoval( String tenantId, String templateId, Set bindingIds) { List result = new ArrayList<>(); for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { addDiagnostics(result, validator.validateRemoval(tenantId, templateId, bindingIds)); } rejectDependencyErrors(result); return result; } private List validatePromotion( TemplateAnnotationAuthorization authorization, String primaryId, String referenceId) { List result = new ArrayList<>(); for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { addDiagnostics(result, validator.validatePromotion(authorization.getTenantId(), authorization.getTemplateId(), primaryId, referenceId)); } rejectDependencyErrors(result); return result; } private static void rejectDependencyErrors(List diagnostics) { for (ElnTemplateVos.Diagnostic diagnostic : diagnostics) { if (diagnostic != null && "ERROR".equalsIgnoreCase(diagnostic.getSeverity())) { throw new DataException(StringUtils.hasText(diagnostic.getMessage()) ? diagnostic.getMessage() : "绑定依赖校验失败"); } } } private static void addDiagnostics(List target, List source) { if (source != null) { target.addAll(source); } } private List fieldBindingFamily( TemplateAnnotationAuthorization authorization, ElnTemplateBindingEntity primary) { List result = new ArrayList<>(); result.add(primary); for (ElnTemplateBindingEntity item : store.listBindings(authorization.getTenantId(), authorization.getTemplateId())) { if (isLive(item) && REFERENCE.equals(item.getKongjianJuese()) && primary.getId().equals(item.getYinyongShiliId())) { if (PENDING.equals(item.getBangdingZhuangtai())) { throw new DataException("主控件仍有待确认引用,请先完成或取消引用标注"); } if (!primary.getZiduanBanbenId().equals(item.getZiduanBanbenId())) { throw new DataException("引用控件字段版本与主控件不一致"); } result.add(item); } } result.sort(Comparator.comparing(ElnTemplateBindingEntity::getId)); return result; } private boolean hasLiveReferences(TemplateAnnotationAuthorization authorization, ElnTemplateBindingEntity primary) { return hasLiveReferences(authorization.getTenantId(), authorization.getTemplateId(), primary); } private boolean hasLiveReferences(String tenantId, String templateId, ElnTemplateBindingEntity primary) { for (ElnTemplateBindingEntity item : store.listBindings(tenantId, templateId)) { if (isLive(item) && REFERENCE.equals(item.getKongjianJuese()) && primary.getId().equals(item.getYinyongShiliId())) { return true; } } return false; } private List copiedBindings( TemplateAnnotationAuthorization authorization) { List result = new ArrayList<>(); for (ElnTemplateBindingEntity binding : store.listBindings(authorization.getTenantId(), authorization.getTemplateId())) { if (StringUtils.hasText(binding.getLaiyuanBangdingId())) { result.add(binding); } } result.sort(Comparator.comparing(ElnTemplateBindingEntity::getId)); return result; } private static void requireVersionCopyRetryVersions( List copies, Map requestedVersions) { if (requestedVersions == null || !ids(copies).equals(requestedVersions.keySet())) { throw ElnTemplateOperationException.conflict("升版复制绑定版本集合不完整"); } for (ElnTemplateBindingEntity binding : copies) { Integer requested = requestedVersions.get(binding.getId()); if (requested == null || binding.getFVersion() == null || requested == Integer.MAX_VALUE || binding.getFVersion() != requested + 1) { throw ElnTemplateOperationException.conflict("升版复制绑定幂等版本无效"); } } } private static void rejectCopiedBindingSingleTransition( ElnTemplateBindingEntity binding) { if (StringUtils.hasText(binding.getLaiyuanBangdingId())) { throw ElnTemplateOperationException.conflict("升版复制绑定只能批量确认"); } } private boolean isUpgradeRetry(TemplateAnnotationAuthorization authorization, String bindingId, ElnTemplateForms.UpgradeConfirm form) { if (form == null || !StringUtils.hasText(form.getOperationCode()) || !StringUtils.hasText(form.getTargetFieldVersionId())) { return false; } ElnTemplateBindingEntity binding = requireBinding(authorization, bindingId); if (!ACTIVE.equals(binding.getBangdingZhuangtai()) || !PRIMARY.equals(binding.getKongjianJuese()) || !form.getTargetFieldVersionId().equals(binding.getZiduanBanbenId())) { return false; } for (ElnTemplateBindingEntity member : fieldBindingFamily(authorization, binding)) { if (!form.getTargetFieldVersionId().equals(member.getZiduanBanbenId()) || !bindingOperationCode(form.getOperationCode(), member.getId()) .equals(member.getCaozuoBianma())) { return false; } } return true; } private ElnTemplateVersionEntity requireEditableTemplate( TemplateAnnotationAuthorization authorization) { if (authorization == null || !StringUtils.hasText(authorization.getTenantId()) || !StringUtils.hasText(authorization.getTemplateId())) { throw new DataException("模板标注授权无效"); } return requireEditableTemplate(authorization.getTenantId(), authorization.getTemplateId()); } private ElnTemplateVersionEntity requireEditableTemplate(String tenantId, String templateId) { ElnTemplateVersionEntity template = store.findTemplate(tenantId, trim(templateId)); if (template == null || !DRAFT.equals(template.getBizStatus()) || !ENABLED.equals(template.getBizEnabled())) { throw ElnTemplateOperationException.conflict("只有启用的草稿模板可以修改绑定"); } return template; } private ElnTemplateVersionEntity requireEditableTemplateForUpdate( TemplateAnnotationAuthorization authorization) { if (authorization == null || !StringUtils.hasText(authorization.getTenantId()) || !StringUtils.hasText(authorization.getTemplateId())) { throw new DataException("模板标注授权无效"); } return requireEditableTemplateForUpdate( authorization.getTenantId(), authorization.getTemplateId()); } private ElnTemplateVersionEntity requireEditableTemplateForUpdate( String tenantId, String templateId) { ElnTemplateVersionEntity template = store.findTemplateForUpdate( tenantId, trim(templateId)); if (template == null || !DRAFT.equals(template.getBizStatus()) || !ENABLED.equals(template.getBizEnabled())) { throw ElnTemplateOperationException.conflict("只有启用的草稿模板可以修改绑定"); } return template; } private ElnTemplateVersionEntity requireTemplate(TemplateAnnotationAuthorization authorization) { ElnTemplateVersionEntity template = store.findTemplate( authorization.getTenantId(), authorization.getTemplateId()); if (template == null) { throw new DataException("模板不存在或不属于当前租户"); } return template; } private ElnFieldDefEntity requireField(String tenantId, String fieldId) { ElnFieldDefEntity field = store.findField(tenantId, trim(fieldId)); if (field == null || !ENABLED.equals(field.getBizEnabled())) { throw new DataException("字段不存在、已停用或不属于当前租户"); } return field; } private ElnFieldVersionEntity requireFieldVersion(String tenantId, String versionId) { ElnFieldVersionEntity version = store.findFieldVersion(tenantId, trim(versionId)); if (version == null) { throw new DataException("字段版本不存在或不属于当前租户"); } return version; } private ElnFieldVersionEntity requireSelectableVersion(String tenantId, ElnFieldDefEntity field, String versionId, ElnTemplateVersionEntity template) { ElnFieldVersionEntity version = requireFieldVersion(tenantId, versionId); if (!field.getId().equals(version.getZiduanDingyiId())) { throw new DataException("字段版本不属于所选字段"); } if (ElnFieldEnums.Scope.TEMPLATE.name().equals(field.getZuoyongFanwei()) && !template.getMubanBianhao().equals(field.getMubanBianhao())) { throw new DataException("模板私有字段不属于当前模板"); } if (!ElnFieldEnums.Scope.GLOBAL.name().equals(field.getZuoyongFanwei()) && !ElnFieldEnums.Scope.TEMPLATE.name().equals(field.getZuoyongFanwei())) { throw new DataException("字段作用范围无效"); } boolean preRelease = ElnFieldEnums.VersionStatus.PRE_RELEASE.name() .equals(version.getBizStatus()); boolean currentPublished = ElnFieldEnums.VersionStatus.PUBLISHED.name() .equals(version.getBizStatus()) && version.getId().equals(field.getDangqianZiduanBanbenId()); if (!preRelease && !currentPublished) { throw new DataException("只能选择预发布或当前已发布字段版本"); } return version; } private ElnTemplateBindingEntity requireBinding( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireBinding(authorization.getTenantId(), bindingId); if (!authorization.getTemplateId().equals(binding.getMubanId())) { throw ElnTemplateOperationException.conflict("绑定不属于授权模板"); } return binding; } private ElnTemplateBindingEntity requireBindingForUpdate( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireBindingForUpdate( authorization.getTenantId(), bindingId); if (!authorization.getTemplateId().equals(binding.getMubanId())) { throw ElnTemplateOperationException.conflict("绑定不属于授权模板"); } return binding; } private ElnTemplateBindingEntity requireBinding(String tenantId, String bindingId) { ElnTemplateBindingEntity binding = store.findBinding(tenantId, trim(bindingId)); if (binding == null) { throw ElnTemplateOperationException.conflict("绑定不存在或不属于当前租户"); } return binding; } private ElnTemplateBindingEntity requireBindingForUpdate(String tenantId, String bindingId) { ElnTemplateBindingEntity binding = store.findBindingForUpdate(tenantId, trim(bindingId)); if (binding == null) { throw ElnTemplateOperationException.conflict("绑定不存在或不属于当前租户"); } return binding; } private ElnTemplateBindingEntity requireActiveBinding( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireBinding(authorization, bindingId); if (!ACTIVE.equals(binding.getBangdingZhuangtai())) { throw ElnTemplateOperationException.conflict("绑定不是有效状态"); } return binding; } private ElnTemplateBindingEntity requireActiveBindingForUpdate( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireBindingForUpdate(authorization, bindingId); if (!ACTIVE.equals(binding.getBangdingZhuangtai())) { throw ElnTemplateOperationException.conflict("绑定不是有效状态"); } return binding; } private ElnTemplateBindingEntity requireActivePrimary( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireActiveBinding(authorization, bindingId); if (!PRIMARY.equals(binding.getKongjianJuese())) { throw new DataException("操作目标必须是主控件"); } return binding; } private ElnTemplateBindingEntity requireActivePrimaryForUpdate( TemplateAnnotationAuthorization authorization, String bindingId) { ElnTemplateBindingEntity binding = requireActiveBindingForUpdate( authorization, bindingId); if (!PRIMARY.equals(binding.getKongjianJuese())) { throw new DataException("操作目标必须是主控件"); } return binding; } private UserContext requireUserContext() { UserContext user = userContextSupplier.get(); if (user == null || !StringUtils.hasText(user.getTenantId()) || !StringUtils.hasText(user.getUserId())) { throw new DataException("登录态无效"); } return user; } private void insert(ElnTemplateBindingEntity binding) { try { store.insert(binding); } catch (DuplicateKeyException e) { throw ElnTemplateOperationException.conflict("绑定已存在或发生并发冲突"); } } private void cas(ElnTemplateBindingEntity before, ElnTemplateBindingEntity after, String expectedStatus) { if (before.getFVersion() == null || !store.compareAndSet(after, before.getFVersion(), expectedStatus)) { throw ElnTemplateOperationException.conflict("绑定状态已变化,请重新预检"); } } private void casExpected(ElnTemplateBindingEntity before, ElnTemplateBindingEntity after, String expectedStatus, Integer expectedVersion) { if (expectedVersion == null || before.getFVersion() == null || !expectedVersion.equals(before.getFVersion()) || !store.compareAndSet(after, expectedVersion, expectedStatus)) { throw ElnTemplateOperationException.conflict("绑定状态已变化,请重新预检"); } } private List activeSnapshots( TemplateAnnotationAuthorization authorization) { List result = new ArrayList<>(); for (ElnTemplateBindingEntity binding : store.listBindings(authorization.getTenantId(), authorization.getTemplateId())) { if (ACTIVE.equals(binding.getBangdingZhuangtai())) { result.add(snapshotWithConfig(binding)); } } return sortedSnapshots(result); } private List snapshots( List bindings) { List result = new ArrayList<>(); for (ElnTemplateBindingEntity binding : bindings) { result.add(snapshotWithConfig(binding)); } return sortedSnapshots(result); } private static List sortedSnapshots( List controls) { List result = controls == null ? new ArrayList<>() : new ArrayList<>(controls); result.sort(Comparator.comparing(ElnTemplateVos.ControlSnapshot::getBindingId)); return result; } public static ElnTemplateVos.ControlSnapshot snapshot(ElnTemplateBindingEntity binding) { ElnTemplateVos.ControlSnapshot result = new ElnTemplateVos.ControlSnapshot(); result.setBindingId(binding.getId()); result.setControlTag(binding.getKongjianBiaoqian()); result.setControlAlias(binding.getKongjianBieming()); result.setControlType(binding.getKongjianLeixing()); result.setControlRole(binding.getKongjianJuese()); result.setFieldVersionId(binding.getZiduanBanbenId()); result.setPrimaryBindingId(binding.getYinyongShiliId()); result.setRepeatGroupCode(binding.getRepeatGroupCode()); result.setRepeatPrototypeRowId(binding.getRepeatPrototypeRowId()); result.setStepId(binding.getStepId()); return result; } private ElnTemplateVos.ControlSnapshot snapshotWithConfig( ElnTemplateBindingEntity binding) { ElnTemplateVos.ControlSnapshot result = snapshot(binding); result.setControlConfig(bindingControlConfig(binding)); return result; } private ElnTemplateVos.Binding toBinding(ElnTemplateBindingEntity source) { ElnTemplateVos.Binding result = new ElnTemplateVos.Binding(); result.setId(source.getId()); result.setTemplateId(source.getMubanId()); result.setFieldId(source.getZiduanDingyiId()); result.setFieldCode(source.getZiduanBianma()); result.setFieldVersionId(source.getZiduanBanbenId()); result.setControlTag(source.getKongjianBiaoqian()); result.setControlAlias(source.getKongjianBieming()); result.setControlType(source.getKongjianLeixing()); result.setControlRole(source.getKongjianJuese()); result.setPrimaryBindingId(source.getYinyongShiliId()); result.setRepeatGroupCode(source.getRepeatGroupCode()); result.setRepeatPrototypeRowId(source.getRepeatPrototypeRowId()); result.setCheckboxGroupCode(source.getCheckboxGroupCode()); result.setStepId(source.getStepId()); result.setControlConfig(bindingControlConfig(source)); result.setStatus(source.getBangdingZhuangtai()); result.setVersion(source.getFVersion()); return result; } private Map bindingControlConfig(ElnTemplateBindingEntity binding) { ElnFieldVersionEntity version = requireFieldVersion( binding.getFTenantId(), binding.getZiduanBanbenId()); if (!binding.getZiduanDingyiId().equals(version.getZiduanDingyiId())) { throw new DataException("绑定字段版本关系无效"); } return DocxExpectedControlConfig.from( version.getShujuLeixing(), version.getKongjianPeizhi()); } private static boolean fieldVisibleToTemplate(ElnFieldDefEntity field, ElnTemplateVersionEntity template) { return ElnFieldEnums.Scope.GLOBAL.name().equals(field.getZuoyongFanwei()) || (ElnFieldEnums.Scope.TEMPLATE.name().equals(field.getZuoyongFanwei()) && template.getMubanBianhao().equals(field.getMubanBianhao())); } private static ElnFieldVos.Detail toFieldDetail(ElnFieldDefEntity source) { ElnFieldVos.Detail result = new ElnFieldVos.Detail(); result.setId(source.getId()); result.setZiduanBianma(source.getZiduanBianma()); result.setZuoyongFanwei(source.getZuoyongFanwei()); result.setMubanBianhao(source.getMubanBianhao()); result.setDangqianZiduanBanbenId(source.getDangqianZiduanBanbenId()); result.setBizEnabled(source.getBizEnabled()); result.setBeizhu(source.getBeizhu()); return result; } private static ElnFieldVos.Version toFieldVersion(ElnFieldVersionEntity source) { ElnFieldVos.Version result = new ElnFieldVos.Version(); result.setId(source.getId()); result.setZiduanDingyiId(source.getZiduanDingyiId()); result.setBanbenHao(source.getBanbenHao()); result.setZiduanMingcheng(source.getZiduanMingcheng()); result.setShujuLeixing(source.getShujuLeixing()); result.setDanwei(source.getDanwei()); result.setShifouBitian(source.getShifouBitian()); result.setPiciMoshi(source.getPiciMoshi()); result.setBinding(StringUtils.hasText(source.getBangdingPeizhi()) ? JsonUtil.getJsonToBean(source.getBangdingPeizhi(), ElnFieldForms.Binding.class) : null); result.setKongjianPeizhi(parseMap(source.getKongjianPeizhi())); result.setRecommendedCheckboxGroupCode(source.getRecommendedCheckboxGroupCode()); result.setRecommendedCheckboxMinSelected(source.getRecommendedCheckboxMinSelected()); result.setRecommendedCheckboxMaxSelected(source.getRecommendedCheckboxMaxSelected()); result.setBizStatus(source.getBizStatus()); result.setZuofeiYuanyin(source.getZuofeiYuanyin()); result.setChuangjianShijian(source.getFCreatorTime()); result.setGengxinShijian(source.getFLastModifyTime()); return result; } private static void copyCheck(ElnTemplateVos.ChangeCheck source, ElnTemplateVos.ChangeCheck target) { target.setOperationCode(source.getOperationCode()); target.setExpectedVersions(source.getExpectedVersions()); target.setControls(source.getControls()); target.setDependencies(source.getDependencies()); } private static Map versions(List bindings) { List sorted = new ArrayList<>(bindings); sorted.sort(Comparator.comparing(ElnTemplateBindingEntity::getId)); Map result = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : sorted) { result.put(binding.getId(), binding.getFVersion()); } return result; } private static Set ids(List bindings) { Set result = new LinkedHashSet<>(); List sorted = new ArrayList<>(bindings); sorted.sort(Comparator.comparing(ElnTemplateBindingEntity::getId)); for (ElnTemplateBindingEntity binding : sorted) { result.add(binding.getId()); } return result; } private static List asList( ElnTemplateBindingEntity first, ElnTemplateBindingEntity second) { List result = new ArrayList<>(); result.add(first); result.add(second); return result; } private static List canonicalControls(List controls) { if (controls == null) { return Collections.singletonList(""); } List result = new ArrayList<>(); for (ElnTemplateVos.ControlSnapshot item : controls) { if (item == null) { result.add(""); } else { result.add(value(item.getBindingId()) + "\u0000" + value(item.getControlTag()) + "\u0000" + value(item.getControlAlias()) + "\u0000" + value(item.getControlType()) + "\u0000" + value(item.getControlRole()) + "\u0000" + value(item.getFieldVersionId()) + "\u0000" + value(item.getPrimaryBindingId()) + "\u0000" + value(item.getRepeatGroupCode()) + "\u0000" + value(item.getRepeatPrototypeRowId()) + "\u0000" + value(item.getStepId())); } } Collections.sort(result); return result; } private static String operationCode(String action, TemplateAnnotationAuthorization authorization, List affected, String targetVersionId) { StringBuilder source = new StringBuilder(action).append('\u0000') .append(authorization.getTenantId()).append('\u0000') .append(authorization.getTemplateId()).append('\u0000'); for (Map.Entry item : versions(affected).entrySet()) { source.append(item.getKey()).append(':').append(item.getValue()).append(';'); } source.append('\u0000').append(value(targetVersionId)); return "eln-binding-" + action.substring(0, action.indexOf(':') < 0 ? action.length() : action.indexOf(':')) + "-" + sha256(source.toString()); } private static String bindingOperationCode(String operation, String bindingId) { String result = requireText(operation, "操作码不能为空") + ":" + requireText(bindingId, "绑定 ID 不能为空"); if (result.length() > 160) { throw new DataException("绑定操作码超过 160 个字符"); } return result; } private static String sha256(String source) { try { byte[] digest = MessageDigest.getInstance("SHA-256") .digest(source.getBytes(StandardCharsets.UTF_8)); StringBuilder result = new StringBuilder(); for (byte item : digest) { result.append(String.format("%02x", item & 0xff)); } return result.toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException("JVM 不支持 SHA-256", e); } } private void audit(String actionCode, String tenantId, String userId, String operation, String templateId, String bindingId, Object before, Object after, String reason, String result) { Map snapshot = new LinkedHashMap<>(); snapshot.put("operationCode", operation); snapshot.put("templateId", templateId); snapshot.put("bindingId", bindingId); snapshot.put("reason", reason); snapshot.put("result", result); snapshot.put("before", before); snapshot.put("after", after); auditRecorder.accept(AuditEventDTO.builder() .operationId(operation) .tenantId(tenantId) .operatorId(userId) .eventTime(new Date()) .eventType("DATA_CHANGE") .actionCode(actionCode) .actionLabel(actionCode) .bizModule("eln") .bizType("eln_template_binding") .targetTable("eln_template_binding") .targetId(bindingId == null ? templateId : bindingId) .reason(reason) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private void failureAudit(String action, String tenantId, String userId, String operation, String templateId, String bindingId, RuntimeException error) { audit("ELN_TEMPLATE_BINDING_CONFIRM_FAILED", tenantId, userId, operation, templateId, bindingId, null, null, action + ": " + error.getMessage(), "FAILED"); } private String templateIdForFailure(String tenantId, String bindingId) { ElnTemplateBindingEntity binding = store.findBinding(tenantId, trim(bindingId)); return binding == null ? null : binding.getMubanId(); } private void requireSamePrivateFieldRequest(TemplateAnnotationAuthorization authorization, ElnTemplateVersionEntity template, ElnTemplateBindingEntity binding, ElnTemplateForms.PrivateField form) { ElnFieldDefEntity field = requireField( authorization.getTenantId(), binding.getZiduanDingyiId()); ElnFieldVersionEntity version = requireFieldVersion( authorization.getTenantId(), binding.getZiduanBanbenId()); Integer required = form.getShifouBitian() == null ? 0 : form.getShifouBitian(); Map expectedBinding = parseMap( JsonUtil.getObjectToString(manualBinding())); boolean same = PRIMARY.equals(binding.getKongjianJuese()) && field.getId().equals(version.getZiduanDingyiId()) && field.getId().equals(binding.getZiduanDingyiId()) && version.getId().equals(binding.getZiduanBanbenId()) && ElnFieldEnums.Scope.TEMPLATE.name().equals(field.getZuoyongFanwei()) && template.getMubanBianhao().equals(field.getMubanBianhao()) && Objects.equals(trimToNull(form.getBeizhu()), trimToNull(field.getBeizhu())) && ElnFieldEnums.VersionStatus.PRE_RELEASE.name().equals(version.getBizStatus()) && Objects.equals(trimToNull(form.getZiduanMingcheng()), trimToNull(version.getZiduanMingcheng())) && Objects.equals(trimToNull(form.getShujuLeixing()), trimToNull(version.getShujuLeixing())) && Objects.equals(trimToNull(form.getDanwei()), trimToNull(version.getDanwei())) && Objects.equals(required, version.getShifouBitian()) && ElnFieldEnums.BatchMode.RECORD_PRIVATE.name().equals(version.getPiciMoshi()) && Objects.equals(expectedBinding, parseMap(version.getBangdingPeizhi())) && Objects.equals(form.getKongjianPeizhi(), parseMap(version.getKongjianPeizhi())) && Objects.equals(normalizeCheckboxGroupCode(form.getCheckboxGroupCode()), binding.getCheckboxGroupCode()) && Objects.equals(stepAssignmentResolver.resolve(authorization.getTenantId(), template.getId(), form.getStepId()), binding.getStepId()); if (!same) { throw ElnTemplateOperationException.conflict("私有字段操作码已用于其他请求"); } } private static void requireValidReferencePrimary(ElnTemplateBindingEntity reference, ElnTemplateBindingEntity primary) { if (primary == null || !REFERENCE.equals(reference.getKongjianJuese()) || !ACTIVE.equals(primary.getBangdingZhuangtai()) || !PRIMARY.equals(primary.getKongjianJuese()) || !primary.getId().equals(reference.getYinyongShiliId()) || !primary.getMubanId().equals(reference.getMubanId()) || !primary.getZiduanDingyiId().equals(reference.getZiduanDingyiId()) || !primary.getZiduanBanbenId().equals(reference.getZiduanBanbenId())) { throw ElnTemplateOperationException.conflict( "引用控件必须指向同模板、同字段版本的有效主控件"); } } private static ElnFieldForms.Binding manualBinding() { ElnFieldForms.Read read = new ElnFieldForms.Read(); read.setSource(ElnFieldEnums.ReadSource.NONE.name()); ElnFieldForms.Write write = new ElnFieldForms.Write(); write.setTarget("ELN_RECORD"); ElnFieldForms.Binding binding = new ElnFieldForms.Binding(); binding.setRead(read); binding.setWrite(write); return binding; } @SuppressWarnings("unchecked") private static Map parseMap(String json) { return StringUtils.hasText(json) ? JsonUtil.getJsonToBean(json, Map.class) : null; } private static String controlType(String dataType) { if (ElnFieldEnums.DataType.TEXT.name().equals(dataType) || ElnFieldEnums.DataType.DECIMAL.name().equals(dataType)) { return ElnTemplateEnums.ControlType.TEXT.name(); } if (ElnFieldEnums.DataType.TEXTAREA.name().equals(dataType)) { return ElnTemplateEnums.ControlType.MULTILINE.name(); } if (ElnFieldEnums.DataType.SELECT.name().equals(dataType)) { return ElnTemplateEnums.ControlType.DROPDOWN.name(); } if (ElnFieldEnums.DataType.CHECKBOX.name().equals(dataType)) { return ElnTemplateEnums.ControlType.CHECKBOX.name(); } if (ElnFieldEnums.DataType.DATE.name().equals(dataType) || ElnFieldEnums.DataType.DATETIME.name().equals(dataType)) { return ElnTemplateEnums.ControlType.DATE.name(); } throw new DataException("字段数据类型不支持 Word 控件绑定"); } private static String uniqueDocxFileId(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); String result = null; if (files != null) { for (ElnDocumentFileVo file : files) { if (file != null && StringUtils.hasText(file.getFileId())) { String id = file.getFileId().trim(); boolean docx = "docx".equalsIgnoreCase(file.getFileExtension()) || id.toLowerCase().endsWith(".docx"); if (docx) { if (result != null) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } result = id; } } } } if (result == null) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } return result; } catch (DataException e) { throw e; } catch (RuntimeException e) { throw new DataException("模板附件无效"); } } private static boolean isLive(ElnTemplateBindingEntity binding) { return PENDING.equals(binding.getBangdingZhuangtai()) || ACTIVE.equals(binding.getBangdingZhuangtai()); } private static void requireReserveForm(ElnTemplateForms.ReserveBinding form) { if (form == null || !StringUtils.hasText(form.getFieldId()) || !StringUtils.hasText(form.getFieldVersionId()) || !StringUtils.hasText(form.getOperationCode())) { throw new DataException("字段、字段版本和操作码不能为空"); } } private static String requireRepeatToken(String value, String message) { String token = requireText(value, message); if (!token.matches("[A-Za-z0-9_-]{1,80}")) { throw new DataException(message); } return token; } private static String normalizeCheckboxGroupCode(String value) { String code = trimToNull(value); if (code != null && !code.matches("[A-Za-z0-9_-]{1,80}")) { throw new DataException("选择组编码只能包含字母、数字、下划线和短横线,且不能超过80个字符"); } return code; } private void validateCheckboxGroupAssignment( TemplateAnnotationAuthorization authorization, String excludedBindingId, String checkboxGroupCode, String role, String repeatGroupCode, String stepId, ElnFieldVersionEntity version) { if (checkboxGroupCode == null) { return; } if ((!PRIMARY.equals(role) && !REPEAT.equals(role)) || !ElnTemplateEnums.ControlType.CHECKBOX.name().equals( controlType(version.getShujuLeixing()))) { throw new DataException("只有可编辑复选框可以加入选择组"); } ElnFieldForms.Binding bindingConfig = StringUtils.hasText(version.getBangdingPeizhi()) ? JsonUtil.getJsonToBean(version.getBangdingPeizhi(), ElnFieldForms.Binding.class) : null; String readSource = bindingConfig == null || bindingConfig.getRead() == null ? null : trimToNull(bindingConfig.getRead().getSource()); if (readSource != null && !ElnFieldEnums.ReadSource.NONE.name().equals(readSource)) { throw new DataException("只读来源复选框不能加入选择组"); } if (excludedBindingId != null) { List diagnostics = new ArrayList<>(); for (ElnTemplateBindingDependencyValidator validator : dependencyValidators) { addDiagnostics(diagnostics, validator.validateCheckboxGroup( authorization.getTenantId(), authorization.getTemplateId(), excludedBindingId)); } rejectDependencyErrors(diagnostics); } boolean defaultChecked = isDefaultChecked(version); int defaultCheckedMembers = 0; for (ElnTemplateBindingEntity member : store.listBindings(authorization.getTenantId(), authorization.getTemplateId())) { if (!isLive(member) || Objects.equals(excludedBindingId, member.getId()) || !checkboxGroupCode.equals(member.getCheckboxGroupCode())) { continue; } if (!role.equals(member.getKongjianJuese()) || (PRIMARY.equals(role) && !Objects.equals(stepId, member.getStepId())) || (REPEAT.equals(role) && !Objects.equals(repeatGroupCode, member.getRepeatGroupCode()))) { throw new DataException("同一选择组必须位于同一步骤和同一普通或重复作用域"); } if (isDefaultChecked(requireFieldVersion( authorization.getTenantId(), member.getZiduanBanbenId()))) { defaultCheckedMembers++; } } Integer maxSelected = checkboxGroupMaxSelected(authorization, checkboxGroupCode); if (defaultChecked && maxSelected != null && defaultCheckedMembers >= maxSelected) { throw new DataException("选择组默认勾选数不能超过最多选择数 " + maxSelected); } } private Integer checkboxGroupMaxSelected( TemplateAnnotationAuthorization authorization, String groupCode) { if (checkboxGroupService != null) { for (ElnTemplateVos.CheckboxGroup group : checkboxGroupService.list( authorization.getTenantId(), authorization.getTemplateId())) { if (groupCode.equals(group.getGroupCode())) { return group.getMaxSelected(); } } } return 1; } private static boolean isDefaultChecked(ElnFieldVersionEntity version) { Map config = parseMap(version.getKongjianPeizhi()); return config != null && Boolean.TRUE.equals(config.get("defaultChecked")); } private static String operation(ElnTemplateForms.ChangeConfirm form) { return form == null ? null : form.getOperationCode(); } private static String requireText(String text, String message) { if (!StringUtils.hasText(text)) { throw new DataException(message); } return text.trim(); } private static String trim(String text) { return text == null ? null : text.trim(); } private static String trimToNull(String text) { return StringUtils.hasText(text) ? text.trim() : null; } private static String value(String text) { return text == null ? "" : text; } private static ElnTemplateVos.StepConfiguration singleStepConfiguration(String stepId) { ElnTemplateVos.Step step = new ElnTemplateVos.Step(); step.setId(stepId); step.setName("实验步骤"); step.setOrder(1); ElnTemplateVos.StepConfiguration result = new ElnTemplateVos.StepConfiguration(); result.setSingleStep(true); result.setSteps(Collections.singletonList(step)); return result; } private static ElnTemplateBindingEntity copy(ElnTemplateBindingEntity source) { ElnTemplateBindingEntity copy = new ElnTemplateBindingEntity(); copy.setId(source.getId()); copy.setMubanId(source.getMubanId()); copy.setMubanBianhao(source.getMubanBianhao()); copy.setZiduanDingyiId(source.getZiduanDingyiId()); copy.setZiduanBianma(source.getZiduanBianma()); copy.setZiduanBanbenId(source.getZiduanBanbenId()); copy.setKongjianBiaoqian(source.getKongjianBiaoqian()); copy.setKongjianBieming(source.getKongjianBieming()); copy.setKongjianLeixing(source.getKongjianLeixing()); copy.setKongjianJuese(source.getKongjianJuese()); copy.setYinyongShiliId(source.getYinyongShiliId()); copy.setRepeatGroupCode(source.getRepeatGroupCode()); copy.setRepeatPrototypeRowId(source.getRepeatPrototypeRowId()); copy.setCheckboxGroupCode(source.getCheckboxGroupCode()); copy.setStepId(source.getStepId()); copy.setBangdingZhuangtai(source.getBangdingZhuangtai()); copy.setCaozuoBianma(source.getCaozuoBianma()); copy.setLaiyuanBangdingId(source.getLaiyuanBangdingId()); copy.setDaoqiShijian(source.getDaoqiShijian()); copy.setZhuangtaiYuanyin(source.getZhuangtaiYuanyin()); copy.setFTenantId(source.getFTenantId()); copy.setFVersion(source.getFVersion()); copy.setFCreatorUserId(source.getFCreatorUserId()); copy.setFLastModifyUserId(source.getFLastModifyUserId()); copy.setFDeleteMark(source.getFDeleteMark()); return copy; } private List prepareDocumentEvidence( TemplateAnnotationAuthorization authorization) { if (authorization == null || !StringUtils.hasText(authorization.getTenantId()) || !StringUtils.hasText(authorization.getTemplateId()) || !StringUtils.hasText(authorization.getFileId())) { throw new DataException("模板标注授权无效"); } List controls = documentEvidence.readControls( authorization.getTenantId(), authorization.getFileId()); if (controls == null) { throw new DataException("DOCX 控件证据无效"); } if (pendingCleanupService != null) { pendingCleanupService.reconcileExpired(authorization.getTenantId(), authorization.getTemplateId(), new Date(), controls); } return controls; } private static ElnTemplateDocumentEvidence evidence( ObjectProvider provider) { ElnTemplateDocumentEvidence evidence = provider.getIfAvailable(); return evidence == null ? (tenantId, fileId) -> { throw new DataException("DOCX 控件证据服务不可用"); } : evidence; } private static UserContext currentUserContext() { UserInfo user = UserProvider.getUser(); TenantVO routed = TenantDataSourceUtil.getTenantInfo(); String tenantId = routed != null && StringUtils.hasText(routed.getEnCode()) ? routed.getEnCode().trim() : user != null && StringUtils.hasText(user.getTenantId()) ? user.getTenantId().trim() : null; String userId = user == null || !StringUtils.hasText(user.getUserId()) ? null : user.getUserId().trim(); return new UserContext(tenantId, userId); } public interface BindingStore { ElnTemplateVersionEntity findTemplate(String tenantId, String templateId); ElnTemplateVersionEntity findTemplateForUpdate(String tenantId, String templateId); List listFields(String tenantId); List listFieldVersions(String tenantId, String fieldId); ElnFieldDefEntity findField(String tenantId, String fieldId); ElnFieldVersionEntity findFieldVersion(String tenantId, String versionId); ElnTemplateBindingEntity findBinding(String tenantId, String bindingId); ElnTemplateBindingEntity findBindingForUpdate(String tenantId, String bindingId); ElnTemplateBindingEntity findByOperation(String tenantId, String templateId, String operationCode); List listBindings(String tenantId, String templateId); void insert(ElnTemplateBindingEntity binding); boolean compareAndSet(ElnTemplateBindingEntity replacement, int expectedVersion, String expectedStatus); } private static final class MyBatisBindingStore implements BindingStore { private final ElnTemplateMapper templateMapper; private final ElnFieldDefMapper fieldMapper; private final ElnFieldVersionMapper fieldVersionMapper; private final ElnTemplateBindingMapper bindingMapper; private MyBatisBindingStore(ElnTemplateMapper templateMapper, ElnFieldDefMapper fieldMapper, ElnFieldVersionMapper fieldVersionMapper, ElnTemplateBindingMapper bindingMapper) { this.templateMapper = templateMapper; this.fieldMapper = fieldMapper; this.fieldVersionMapper = fieldVersionMapper; this.bindingMapper = bindingMapper; } @Override public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) { return templateMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateVersionEntity::getFTenantId, tenantId) .eq(ElnTemplateVersionEntity::getId, templateId) .and(q -> q.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0))); } @Override public ElnTemplateVersionEntity findTemplateForUpdate( String tenantId, String templateId) { return templateMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateVersionEntity::getFTenantId, tenantId) .eq(ElnTemplateVersionEntity::getId, templateId) .and(q -> q.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0)) .last("FOR UPDATE")); } @Override public ElnFieldDefEntity findField(String tenantId, String fieldId) { return fieldMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnFieldDefEntity::getFTenantId, tenantId) .eq(ElnFieldDefEntity::getId, fieldId) .and(q -> q.isNull(ElnFieldDefEntity::getFDeleteMark) .or().eq(ElnFieldDefEntity::getFDeleteMark, 0))); } @Override public List listFields(String tenantId) { return fieldMapper.selectList(Wrappers.lambdaQuery() .eq(ElnFieldDefEntity::getFTenantId, tenantId) .and(q -> q.isNull(ElnFieldDefEntity::getFDeleteMark) .or().eq(ElnFieldDefEntity::getFDeleteMark, 0)) .orderByAsc(ElnFieldDefEntity::getZiduanBianma)); } @Override public List listFieldVersions(String tenantId, String fieldId) { return fieldVersionMapper.selectList(Wrappers.lambdaQuery() .eq(ElnFieldVersionEntity::getFTenantId, tenantId) .eq(ElnFieldVersionEntity::getZiduanDingyiId, fieldId) .and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark) .or().eq(ElnFieldVersionEntity::getFDeleteMark, 0)) .orderByAsc(ElnFieldVersionEntity::getBanbenHao) .orderByAsc(ElnFieldVersionEntity::getId)); } @Override public ElnFieldVersionEntity findFieldVersion(String tenantId, String versionId) { return fieldVersionMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnFieldVersionEntity::getFTenantId, tenantId) .eq(ElnFieldVersionEntity::getId, versionId) .and(q -> q.isNull(ElnFieldVersionEntity::getFDeleteMark) .or().eq(ElnFieldVersionEntity::getFDeleteMark, 0))); } @Override public ElnTemplateBindingEntity findBinding(String tenantId, String bindingId) { return bindingMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getId, bindingId) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))); } @Override public ElnTemplateBindingEntity findBindingForUpdate(String tenantId, String bindingId) { return bindingMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getId, bindingId) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)) .last("FOR UPDATE")); } @Override public ElnTemplateBindingEntity findByOperation(String tenantId, String templateId, String operationCode) { return bindingMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getMubanId, templateId) .eq(ElnTemplateBindingEntity::getCaozuoBianma, operationCode) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))); } @Override public List listBindings(String tenantId, String templateId) { return bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getMubanId, templateId) .and(q -> q.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0)) .orderByAsc(ElnTemplateBindingEntity::getId)); } @Override public void insert(ElnTemplateBindingEntity binding) { bindingMapper.insert(binding); } @Override public boolean compareAndSet(ElnTemplateBindingEntity replacement, int expectedVersion, String expectedStatus) { return bindingMapper.update(null, Wrappers.lambdaUpdate() .eq(ElnTemplateBindingEntity::getFTenantId, replacement.getFTenantId()) .eq(ElnTemplateBindingEntity::getId, replacement.getId()) .eq(ElnTemplateBindingEntity::getFVersion, expectedVersion) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, expectedStatus) .set(ElnTemplateBindingEntity::getZiduanBanbenId, replacement.getZiduanBanbenId()) .set(ElnTemplateBindingEntity::getKongjianBiaoqian, replacement.getKongjianBiaoqian()) .set(ElnTemplateBindingEntity::getKongjianBieming, replacement.getKongjianBieming()) .set(ElnTemplateBindingEntity::getKongjianLeixing, replacement.getKongjianLeixing()) .set(ElnTemplateBindingEntity::getKongjianJuese, replacement.getKongjianJuese()) .set(ElnTemplateBindingEntity::getYinyongShiliId, replacement.getYinyongShiliId()) .set(ElnTemplateBindingEntity::getRepeatGroupCode, replacement.getRepeatGroupCode()) .set(ElnTemplateBindingEntity::getRepeatPrototypeRowId, replacement.getRepeatPrototypeRowId()) .set(ElnTemplateBindingEntity::getCheckboxGroupCode, replacement.getCheckboxGroupCode()) .set(ElnTemplateBindingEntity::getStepId, replacement.getStepId()) .set(ElnTemplateBindingEntity::getBangdingZhuangtai, replacement.getBangdingZhuangtai()) .set(ElnTemplateBindingEntity::getCaozuoBianma, replacement.getCaozuoBianma()) .set(ElnTemplateBindingEntity::getZhuangtaiYuanyin, replacement.getZhuangtaiYuanyin()) .set(ElnTemplateBindingEntity::getFVersion, expectedVersion + 1)) == 1; } } public static final class UserContext { private final String tenantId; private final String userId; public UserContext(String tenantId, String userId) { this.tenantId = tenantId; this.userId = userId; } public String getTenantId() { return tenantId; } public String getUserId() { return userId; } } @FunctionalInterface interface StepAssignmentResolver { String resolve(String tenantId, String templateId, String requestedStepId); } @FunctionalInterface interface StepConfigurationReader { ElnTemplateVos.StepConfiguration read(String templateId); } }