package jnpf.elnService.template.impl; import cn.hutool.core.util.IdUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.audit.model.AuditEventDTO; import jnpf.audit.sdk.AuditClient; import jnpf.base.ActionResult; import jnpf.base.ActionResultCode; import jnpf.base.UserInfo; import jnpf.database.util.TenantDataSourceUtil; import jnpf.elnEntity.ElnDocumentFileVo; import jnpf.elnEntity.template.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateCurrentVersionEntity; 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.ElnTemplateBindingMapper; import jnpf.elnMapper.ElnTemplateCurrentVersionMapper; import jnpf.elnService.template.ElnTemplateCatalogService; import jnpf.elnService.template.ElnTemplateFileService; import jnpf.elnService.template.ElnTemplateHealthService; import jnpf.elnService.template.ElnTemplatePublishValidator; import jnpf.elnService.template.ElnTemplateRepeatGroupService; import jnpf.elnService.template.ElnTemplateStepService; import jnpf.exception.DataException; import jnpf.model.tenant.TenantVO; import jnpf.onlyoffice.TemplatePublishFenceApi; import jnpf.permission.UserApi; import jnpf.permission.entity.UserEntity; import jnpf.util.JsonUtil; import jnpf.util.UserProvider; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.StringUtils; import java.nio.charset.StandardCharsets; 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; import java.util.stream.Collectors; @Service public class ElnTemplateCatalogServiceImpl implements ElnTemplateCatalogService { private static final String ENABLED = "enabled"; private static final int MAX_SOURCE_CHAIN_DEPTH = 32; private final TemplateCatalogStore store; private final Supplier userContextSupplier; private final TargetIdFactory targetIdFactory; private final Supplier bindingIdSupplier; private final TemplateFileCopier fileCopier; private final Consumer auditRecorder; private final TransactionRunner transactionRunner; private final List publishValidators; private final TemplatePublishFence publishFence; private final UserNameResolver userNameResolver; private final VersionGroupCopier versionGroupCopier; @Autowired public ElnTemplateCatalogServiceImpl(ElnTemplateMapper templateMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateCurrentVersionMapper currentMapper, ElnTemplateFileService fileService, ElnTemplateHealthService healthService, ElnTemplateRepeatGroupService repeatGroupService, ElnTemplateStepService stepService, List publishValidators, ObjectProvider auditClientProvider, PlatformTransactionManager transactionManager, TemplatePublishFenceApi publishFenceApi, UserApi userApi) { this(new MyBatisTemplateCatalogStore(templateMapper, bindingMapper, currentMapper), ElnTemplateCatalogServiceImpl::currentUserContext, ElnTemplateCatalogServiceImpl::deterministicTargetId, IdUtil::getSnowflakeNextIdStr, fileService::copyDocx, event -> { AuditClient client = auditClientProvider.getIfAvailable(); if (client != null) { client.record(event); } }, new SpringTransactionRunner(transactionManager), withHealthValidator(healthService, publishValidators), remotePublishFence(publishFenceApi), remoteUserNameResolver(userApi), (tenantId, sourceTemplateId, targetTemplateId) -> { repeatGroupService.copyForVersion( tenantId, sourceTemplateId, targetTemplateId); stepService.copyForVersion(tenantId, sourceTemplateId, targetTemplateId); }); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder) { this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier, auditRecorder, callback -> callback.get(), Collections.emptyList(), TemplatePublishFence.noop(), userId -> userId); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder, TransactionRunner transactionRunner) { this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier, auditRecorder, transactionRunner, Collections.emptyList(), TemplatePublishFence.noop(), userId -> userId); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder, TransactionRunner transactionRunner, List publishValidators) { this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier, auditRecorder, transactionRunner, publishValidators, TemplatePublishFence.noop(), userId -> userId); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder, TransactionRunner transactionRunner, List publishValidators, TemplatePublishFence publishFence) { this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier, auditRecorder, transactionRunner, publishValidators, publishFence, userId -> userId); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder, TransactionRunner transactionRunner, List publishValidators, TemplatePublishFence publishFence, UserNameResolver userNameResolver) { this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier, auditRecorder, transactionRunner, publishValidators, publishFence, userNameResolver, (tenantId, sourceTemplateId, targetTemplateId) -> { }); } ElnTemplateCatalogServiceImpl(TemplateCatalogStore store, Supplier userContextSupplier, TargetIdFactory targetIdFactory, Supplier bindingIdSupplier, TemplateFileCopier fileCopier, Consumer auditRecorder, TransactionRunner transactionRunner, List publishValidators, TemplatePublishFence publishFence, UserNameResolver userNameResolver, VersionGroupCopier versionGroupCopier) { this.store = store; this.userContextSupplier = userContextSupplier; this.targetIdFactory = targetIdFactory; this.bindingIdSupplier = bindingIdSupplier; this.fileCopier = fileCopier; this.auditRecorder = auditRecorder; this.transactionRunner = transactionRunner; this.publishValidators = publishValidators == null ? Collections.emptyList() : new ArrayList<>(publishValidators); this.publishFence = publishFence; this.userNameResolver = userNameResolver; this.versionGroupCopier = versionGroupCopier; } @Override @Transactional(readOnly = true) public IPage page(ElnTemplateForms.Query query) { UserContext context = requireContext(); ElnTemplateForms.Query actual = query == null ? new ElnTemplateForms.Query() : query; if (actual.getCurrentPage() < 1 || actual.getPageSize() < 1) { throw new DataException("分页参数无效"); } IPage entities = store.page(context.getTenantId(), actual); Page result = new Page<>(entities.getCurrent(), entities.getSize()); result.setTotal(entities.getTotal()); List templateCodes = entities.getRecords().stream() .map(ElnTemplateVersionEntity::getMubanBianhao) .filter(StringUtils::hasText).distinct().collect(Collectors.toList()); Map currentTemplateIds = store.listCurrentVersions( context.getTenantId(), templateCodes).stream() .collect(Collectors.toMap(ElnTemplateCurrentVersionEntity::getMubanBianhao, ElnTemplateCurrentVersionEntity::getDangqianMubanId, (first, second) -> first, LinkedHashMap::new)); List templateIds = entities.getRecords().stream() .map(ElnTemplateVersionEntity::getId).collect(Collectors.toList()); Map> fieldsByTemplate = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : store.listActiveBindingsByTemplateIds( context.getTenantId(), templateIds)) { if (StringUtils.hasText(binding.getZiduanDingyiId())) { fieldsByTemplate.computeIfAbsent( binding.getMubanId(), key -> new LinkedHashSet<>()) .add(binding.getZiduanDingyiId()); } } List records = new ArrayList<>(); for (ElnTemplateVersionEntity entity : entities.getRecords()) { ElnTemplateVos.ListItem item = toListItem(entity); item.setShifouDangqian(Objects.equals(entity.getId(), currentTemplateIds.get(entity.getMubanBianhao()))); item.setZiduanShu((long) fieldsByTemplate .getOrDefault(entity.getId(), Collections.emptySet()).size()); records.add(item); } result.setRecords(records); return result; } @Override @Transactional(readOnly = true) public IPage directory(ElnTemplateForms.Query query) { UserContext context = requireContext(); ElnTemplateForms.Query actual = query == null ? new ElnTemplateForms.Query() : query; if (actual.getCurrentPage() < 1 || actual.getPageSize() < 1) { throw new DataException("分页参数无效"); } IPage result = store.pageDirectory( context.getTenantId(), actual); List effectiveTemplateIds = result.getRecords().stream() .map(ElnTemplateVos.CatalogItem::getShengxiaoMubanId) .filter(StringUtils::hasText).collect(Collectors.toList()); Map> fieldsByTemplate = new LinkedHashMap<>(); for (ElnTemplateBindingEntity binding : store.listActiveBindingsByTemplateIds( context.getTenantId(), effectiveTemplateIds)) { if (StringUtils.hasText(binding.getZiduanDingyiId())) { fieldsByTemplate.computeIfAbsent( binding.getMubanId(), key -> new LinkedHashSet<>()) .add(binding.getZiduanDingyiId()); } } for (ElnTemplateVos.CatalogItem item : result.getRecords()) { if (StringUtils.hasText(item.getShengxiaoMubanId())) { item.setZiduanShu((long) fieldsByTemplate.getOrDefault( item.getShengxiaoMubanId(), Collections.emptySet()).size()); } } return result; } @Override @Transactional(readOnly = true) public ElnTemplateVos.Detail detail(String templateId) { UserContext context = requireContext(); ElnTemplateVersionEntity current = requireTemplate(context.getTenantId(), templateId); ElnTemplateVos.Detail result = toDetail(current); mergeSourceChain(context.getTenantId(), current, result); enrichDetailNames(result); return result; } @Override public ElnTemplateVos.Detail createVersion(String sourceTemplateId, ElnTemplateForms.CreateVersion form) { UserContext context = requireContext(); validateCreateVersion(sourceTemplateId, form); String sourceId = sourceTemplateId.trim(); String operation = form.getShengbanCaozuoBianma().trim(); String businessVersion = form.getMubanBanben().trim(); Date effectiveDate = form.getShengxiaoRiqi(); try { return transactionRunner.execute(() -> createVersionInTransaction( context, sourceId, businessVersion, operation, effectiveDate)); } catch (DuplicateKeyException e) { return transactionRunner.execute(() -> resolveConcurrentConflict( context, sourceId, businessVersion, operation, effectiveDate)); } } private ElnTemplateVos.Detail createVersionInTransaction( UserContext context, String sourceId, String businessVersion, String operation, Date effectiveDate) { String tenantId = context.getTenantId(); ElnTemplateVersionEntity existing = store.findByOperation(tenantId, operation); if (existing != null) { return completeIdempotentRetry( context, sourceId, businessVersion, operation, effectiveDate, existing); } ElnTemplateVersionEntity source = requireTemplateForUpdate(tenantId, sourceId); if (store.findByCodeAndVersion(tenantId, source.getMubanBianhao(), businessVersion) != null) { throw new DataException("同名模板业务版本已存在"); } String targetId = targetIdFactory.create(tenantId, sourceId, operation); ElnTemplateVersionEntity target = new ElnTemplateVersionEntity(); target.setId(targetId); target.setFTenantId(tenantId); target.setMubanBianhao(source.getMubanBianhao()); target.setMubanBanben(businessVersion); target.setMubanMingcheng(source.getMubanMingcheng()); target.setShiyanBuzhou(source.getShiyanBuzhou()); target.setJianyanXiangmu(source.getJianyanXiangmu()); target.setJianyanXiangmuBianma(source.getJianyanXiangmuBianma()); target.setJianyanXiangmuMingcheng(source.getJianyanXiangmuMingcheng()); target.setMubanFenlei(source.getMubanFenlei()); target.setMubanFenleiMingcheng(source.getMubanFenleiMingcheng()); target.setShengxiaoRiqi(effectiveDate); target.setBizStatus(ElnTemplateEnums.TemplateStatus.DRAFT.name()); target.setBizEnabled(ENABLED); target.setLaiyuanMubanId(sourceId); target.setShengbanCaozuoBianma(operation); Date createdAt = new Date(); target.setChuangjianRenyuan(context.getUserId()); target.setChuangjianShijian(createdAt); target.setFCreatorUserId(context.getUserId()); target.setFCreatorTime(createdAt); target.setFVersion(0); target.setFDeleteMark(0); store.insertTemplate(target); String attachmentJson = fileCopier.copy(source.getMubanFile(), targetId); target.setMubanFile(attachmentJson); store.updateTemplateFile(target); List sourceBindings = store.listActiveBindings(tenantId, sourceId); Map sourceBindingsById = new LinkedHashMap<>(); Map targetBindingIds = new LinkedHashMap<>(); for (ElnTemplateBindingEntity sourceBinding : sourceBindings) { if (!StringUtils.hasText(sourceBinding.getId()) || sourceBindingsById.put(sourceBinding.getId(), sourceBinding) != null) { throw new DataException("源模板绑定集合无效"); } String targetBindingId = bindingIdSupplier.get(); if (!StringUtils.hasText(targetBindingId) || targetBindingIds.containsValue(targetBindingId)) { throw new DataException("目标模板绑定 ID 生成失败"); } targetBindingIds.put(sourceBinding.getId(), targetBindingId); } for (ElnTemplateBindingEntity sourceBinding : sourceBindings) { store.insertBinding(copyBinding(sourceBinding, target, operation, sourceBindingsById, targetBindingIds)); } versionGroupCopier.copy(tenantId, sourceId, targetId); auditVersionCreate(context, operation, sourceId, targetId, "SUCCESS"); return toDetail(target); } private ElnTemplateVos.Detail resolveConcurrentConflict( UserContext context, String sourceId, String businessVersion, String operation, Date effectiveDate) { String tenantId = context.getTenantId(); ElnTemplateVersionEntity existing = store.findByOperation(tenantId, operation); if (existing != null) { return completeIdempotentRetry( context, sourceId, businessVersion, operation, effectiveDate, existing); } ElnTemplateVersionEntity source = requireTemplate(tenantId, sourceId); if (store.findByCodeAndVersion(tenantId, source.getMubanBianhao(), businessVersion) != null) { throw new DataException("同名模板业务版本已存在"); } throw new DataException("模板升版并发冲突,请重试"); } private ElnTemplateVos.Detail completeIdempotentRetry( UserContext context, String sourceId, String businessVersion, String operation, Date effectiveDate, ElnTemplateVersionEntity existing) { if (!sourceId.equals(existing.getLaiyuanMubanId()) || !businessVersion.equals(existing.getMubanBanben()) || !sameInstant(effectiveDate, existing.getShengxiaoRiqi())) { throw new DataException("升版操作码已用于其他模板版本"); } ElnTemplateVersionEntity source = requireTemplate(context.getTenantId(), sourceId); String attachmentJson = fileCopier.copy(source.getMubanFile(), existing.getId()); existing.setMubanFile(attachmentJson); store.updateTemplateFile(existing); versionGroupCopier.copy(context.getTenantId(), sourceId, existing.getId()); auditVersionCreate(context, operation, sourceId, existing.getId(), "IDEMPOTENT"); return toDetail(existing); } private static boolean sameInstant(Date left, Date right) { return left == null ? right == null : right != null && left.getTime() == right.getTime(); } @Override public void publish(String templateId, String reason) { UserContext context = requireContext(); ElnTemplateVersionEntity snapshot = requireTemplate(context.getTenantId(), templateId); if (!ElnTemplateEnums.TemplateStatus.DRAFT.name().equals(snapshot.getBizStatus())) { throw new DataException("只有草稿模板可以发布"); } String fileId = uniqueDocxFileId(snapshot.getMubanFile()); String fileType = "annex"; String token = publishFence.acquire( context.getTenantId(), snapshot.getId(), fileId, fileType); try { transactionRunner.execute(() -> { publishInTransaction(context, snapshot.getId(), fileId, reason); return null; }); } catch (RuntimeException publishError) { try { publishFence.release(context.getTenantId(), snapshot.getId(), fileId, fileType, token); } catch (RuntimeException releaseError) { publishError.addSuppressed(releaseError); } throw publishError; } try { publishFence.release(context.getTenantId(), snapshot.getId(), fileId, fileType, token); } catch (RuntimeException releaseError) { DataException result = new DataException( "模板已发布成功,但 OnlyOffice 发布锁释放失败,需人工恢复并刷新页面"); result.addSuppressed(releaseError); throw result; } } private void publishInTransaction(UserContext context, String templateId, String fencedFileId, String reason) { ElnTemplateVersionEntity template = requireTemplateForUpdate( context.getTenantId(), templateId); if (!ElnTemplateEnums.TemplateStatus.DRAFT.name().equals(template.getBizStatus())) { throw new DataException("只有草稿模板可以发布"); } if (!fencedFileId.equals(uniqueDocxFileId(template.getMubanFile()))) { throw new DataException("模板附件已变化,请刷新后重试"); } ElnTemplateVersionEntity changed = copyTemplate(template); changed.setMubanFile(fileCopier.copy(template.getMubanFile(), template.getId() + "-published")); validateForPublish(changed); changed.setBizStatus(ElnTemplateEnums.TemplateStatus.PUBLISHED.name()); touch(changed, context); store.updateTemplateLifecycle(changed); makeFirstRunnableVersionCurrent(context, changed); auditLifecycle("ELN_TEMPLATE_PUBLISH", "发布 ELN 模板版本", changed.getId(), context, template, changed, trimToNull(reason)); } private void makeFirstRunnableVersionCurrent(UserContext context, ElnTemplateVersionEntity published) { ElnTemplateCurrentVersionEntity current = store.findCurrent( context.getTenantId(), published.getMubanBianhao()); if (current != null || !ENABLED.equals(published.getBizEnabled()) || published.getShengxiaoRiqi() == null || published.getShengxiaoRiqi().after(new Date())) { return; } insertCurrent(context, published); } private static String uniqueDocxFileId(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); List docxFileIds = files == null ? Collections.emptyList() : files.stream() .filter(Objects::nonNull) .filter(file -> "docx".equalsIgnoreCase(file.getFileExtension()) || StringUtils.hasText(file.getFileId()) && file.getFileId().toLowerCase().endsWith(".docx")) .map(ElnDocumentFileVo::getFileId) .filter(StringUtils::hasText) .collect(Collectors.toList()); if (docxFileIds.size() != 1) { throw new DataException("模板必须且只能包含一个 DOCX 附件"); } return docxFileIds.get(0); } catch (DataException e) { throw e; } catch (RuntimeException e) { throw new DataException("模板附件无效"); } } @Override @Transactional public void enable(String templateId, String reason) { changeEnabled(templateId, ENABLED, requireReason(reason), "ELN_TEMPLATE_ENABLE", "启用 ELN 模板版本"); } @Override @Transactional public void disable(String templateId, String reason) { changeEnabled(templateId, "disabled", requireReason(reason), "ELN_TEMPLATE_DISABLE", "停用 ELN 模板版本"); } @Override @Transactional public void switchCurrent(String templateCode, String templateId, String reason) { UserContext context = requireContext(); String actualReason = requireReason(reason); if (!StringUtils.hasText(templateCode)) { throw new DataException("模板编号不能为空"); } String actualCode = templateCode.trim(); ElnTemplateVersionEntity target = requireTemplateForUpdate( context.getTenantId(), templateId); if (!actualCode.equals(target.getMubanBianhao())) { throw new DataException("模板编号与目标版本不一致"); } if (!ElnTemplateEnums.TemplateStatus.PUBLISHED.name().equals(target.getBizStatus()) || !ENABLED.equals(target.getBizEnabled()) || target.getShengxiaoRiqi() == null || target.getShengxiaoRiqi().after(new Date())) { throw new DataException("目标模板不具备设为当前版本的资格"); } validateForPublish(target); ElnTemplateCurrentVersionEntity current = store.findCurrent(context.getTenantId(), actualCode); String beforeTemplateId = current == null ? null : current.getDangqianMubanId(); if (current == null) { insertCurrent(context, target); } else { ElnTemplateCurrentVersionEntity changed = copyCurrent(current); changed.setDangqianMubanId(target.getId()); changed.setJianyanXiangmuBianma(target.getJianyanXiangmuBianma()); touch(changed, context); store.updateCurrent(changed); } auditSwitch(context, actualReason, beforeTemplateId, target.getId()); } private void insertCurrent(UserContext context, ElnTemplateVersionEntity target) { ElnTemplateCurrentVersionEntity current = new ElnTemplateCurrentVersionEntity(); current.setId(IdUtil.getSnowflakeNextIdStr()); current.setFTenantId(context.getTenantId()); current.setMubanBianhao(target.getMubanBianhao()); current.setDangqianMubanId(target.getId()); current.setJianyanXiangmuBianma(target.getJianyanXiangmuBianma()); current.setFVersion(0); current.setFDeleteMark(0); current.setFCreatorUserId(context.getUserId()); touch(current, context); try { if (!store.insertCurrent(current)) { throw new DataException("当前模板设置失败,请重试"); } } catch (DuplicateKeyException e) { throw new DataException("当前模板已被其他操作设置,请刷新后重试"); } } private void changeEnabled(String templateId, String targetEnabled, String reason, String actionCode, String actionLabel) { UserContext context = requireContext(); ElnTemplateVersionEntity template = requireTemplateForUpdate( context.getTenantId(), templateId); if (targetEnabled.equals(template.getBizEnabled())) { throw new DataException(ENABLED.equals(targetEnabled) ? "模板已启用" : "模板已停用"); } ElnTemplateVersionEntity changed = copyTemplate(template); changed.setBizEnabled(targetEnabled); touch(changed, context); store.updateTemplateLifecycle(changed); auditLifecycle(actionCode, actionLabel, changed.getId(), context, template, changed, reason); } private void validateForPublish(ElnTemplateVersionEntity template) { List diagnostics = new ArrayList<>(); ElnTemplateVos.Diagnostic attachment = attachmentDiagnostic(template.getMubanFile()); if (attachment != null) { diagnostics.add(attachment); } for (ElnTemplatePublishValidator validator : publishValidators) { if (validator == null) { continue; } try { List items = validator.validate( template.getFTenantId(), template.getId(), template.getMubanFile()); if (items != null) { diagnostics.addAll(items); } } catch (DataException e) { diagnostics.add(diagnostic("ERROR", "TEMPLATE_VALIDATION_FAILED", e.getMessage())); } } List errors = diagnostics.stream() .filter(item -> item != null && "ERROR".equalsIgnoreCase(item.getSeverity())) .collect(Collectors.toList()); if (!errors.isEmpty()) { String message = errors.stream() .map(item -> StringUtils.hasText(item.getMessage()) ? item.getMessage() : "模板存在未说明的校验错误") .collect(Collectors.joining(";")); throw new DataException("模板发布校验失败:" + message); } } private static ElnTemplateVos.Diagnostic attachmentDiagnostic(String attachments) { try { List files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class); long docxCount = files == null ? 0 : files.stream() .filter(item -> item != null && StringUtils.hasText(item.getFileId())) .filter(item -> "docx".equalsIgnoreCase(item.getFileExtension()) || item.getFileId().toLowerCase().endsWith(".docx")) .count(); return docxCount == 1 ? null : diagnostic("ERROR", "INVALID_DOCX_ATTACHMENT", "模板必须且只能包含一个 DOCX 附件"); } catch (RuntimeException e) { return diagnostic("ERROR", "INVALID_DOCX_ATTACHMENT", "模板附件无效"); } } static ElnTemplatePublishValidator healthValidator(ElnTemplateHealthService healthService) { return new ElnTemplatePublishValidator() { @Override public List validate(String tenantId, String templateId) { return validate(tenantId, templateId, null); } @Override public List validate( String tenantId, String templateId, String attachments) { ElnTemplateHealthService.HealthReport report = healthService.inspect(templateId, attachments); List result = new ArrayList<>(); if (report == null || report.getDiagnostics() == null) { return result; } for (ElnTemplateHealthService.HealthDiagnostic source : report.getDiagnostics()) { if (source == null) { continue; } String severity = source.getSeverity(); if ("PENDING_BINDING".equals(source.getCode()) || "PENDING_CONFIRMATION_REQUIRED".equals(source.getCode()) || "FIELD_VERSION_NOT_PUBLISHED".equals(source.getCode()) || "CHECKBOX_GROUP_SINGLE_MEMBER".equals(source.getCode())) { severity = "ERROR"; } ElnTemplateVos.Diagnostic target = diagnostic( severity, source.getCode(), source.getMessage()); target.setBindingId(source.getBindingId()); target.setControlTag(source.getControlTag()); result.add(target); } return result; } }; } private static List withHealthValidator( ElnTemplateHealthService healthService, List injectedValidators) { List result = new ArrayList<>(); result.add(healthValidator(healthService)); if (injectedValidators != null) { result.addAll(injectedValidators); } return result; } private static ElnTemplateVos.Diagnostic diagnostic( String severity, String code, String message) { ElnTemplateVos.Diagnostic result = new ElnTemplateVos.Diagnostic(); result.setSeverity(severity); result.setCode(code); result.setMessage(message); return result; } private ElnTemplateBindingEntity copyBinding(ElnTemplateBindingEntity source, ElnTemplateVersionEntity target, String operation, Map sourceBindings, Map targetBindingIds) { ElnTemplateBindingEntity copy = new ElnTemplateBindingEntity(); copy.setId(targetBindingIds.get(source.getId())); copy.setMubanId(target.getId()); copy.setMubanBianhao(target.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(remapPrimaryBindingId( source, sourceBindings, targetBindingIds)); copy.setRepeatGroupCode(source.getRepeatGroupCode()); copy.setRepeatPrototypeRowId(source.getRepeatPrototypeRowId()); copy.setCheckboxGroupCode(source.getCheckboxGroupCode()); copy.setBangdingZhuangtai(ElnTemplateEnums.BindingStatus.PENDING.name()); copy.setCaozuoBianma(operation + ":" + source.getId()); copy.setLaiyuanBangdingId(source.getId()); copy.setDaoqiShijian(null); copy.setZhuangtaiYuanyin(null); copy.setFTenantId(target.getFTenantId()); copy.setFVersion(0); copy.setFDeleteMark(0); return copy; } private static String remapPrimaryBindingId(ElnTemplateBindingEntity source, Map sourceBindings, Map targetBindingIds) { if (ElnTemplateEnums.ControlRole.PRIMARY.name().equals(source.getKongjianJuese())) { if (StringUtils.hasText(source.getYinyongShiliId())) { throw new DataException("源模板主控件引用关系无效"); } return null; } if (ElnTemplateEnums.ControlRole.REPEAT.name().equals(source.getKongjianJuese())) { if (StringUtils.hasText(source.getYinyongShiliId()) || !StringUtils.hasText(source.getRepeatGroupCode()) || !StringUtils.hasText(source.getRepeatPrototypeRowId())) { throw new DataException("源模板重复绑定关系无效"); } return null; } if (!ElnTemplateEnums.ControlRole.REFERENCE.name().equals(source.getKongjianJuese()) || !StringUtils.hasText(source.getYinyongShiliId())) { throw new DataException("源模板绑定角色无效"); } ElnTemplateBindingEntity primary = sourceBindings.get(source.getYinyongShiliId()); if (primary == null || !ElnTemplateEnums.ControlRole.PRIMARY.name().equals(primary.getKongjianJuese()) || !source.getZiduanDingyiId().equals(primary.getZiduanDingyiId()) || !source.getZiduanBanbenId().equals(primary.getZiduanBanbenId())) { throw new DataException("源模板引用控件未指向本次复制的有效主控件"); } String targetPrimaryId = targetBindingIds.get(primary.getId()); if (!StringUtils.hasText(targetPrimaryId)) { throw new DataException("目标模板主控件 ID 映射缺失"); } return targetPrimaryId; } private void validateCreateVersion(String sourceTemplateId, ElnTemplateForms.CreateVersion form) { if (!StringUtils.hasText(sourceTemplateId) || form == null || !StringUtils.hasText(form.getMubanBanben()) || !StringUtils.hasText(form.getShengbanCaozuoBianma())) { throw new DataException("源模板、业务版本和升版操作码不能为空"); } } private ElnTemplateVersionEntity requireTemplate(String tenantId, String templateId) { if (!StringUtils.hasText(templateId)) { throw new DataException("模板不存在"); } ElnTemplateVersionEntity template = store.findTemplate(tenantId, templateId.trim()); if (template == null || !tenantId.equals(template.getFTenantId())) { throw new DataException("模板不存在或不属于当前租户"); } return template; } private ElnTemplateVersionEntity requireTemplateForUpdate( String tenantId, String templateId) { if (!StringUtils.hasText(templateId)) { throw new DataException("模板不存在"); } ElnTemplateVersionEntity template = store.findTemplateForUpdate( tenantId, templateId.trim()); if (template == null || !tenantId.equals(template.getFTenantId())) { throw new DataException("模板不存在或不属于当前租户"); } return template; } private UserContext requireContext() { UserContext context = userContextSupplier.get(); if (context == null || !StringUtils.hasText(context.getTenantId())) { throw new DataException("当前租户无效"); } return context; } private static String requireReason(String reason) { if (!StringUtils.hasText(reason)) { throw new DataException("操作原因不能为空"); } return reason.trim(); } private static String trimToNull(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private static String value(String source) { return source == null ? "" : source; } private static ElnTemplateVersionEntity copyTemplate(ElnTemplateVersionEntity source) { return JsonUtil.getJsonToBean(source, ElnTemplateVersionEntity.class); } private static ElnTemplateCurrentVersionEntity copyCurrent( ElnTemplateCurrentVersionEntity source) { return JsonUtil.getJsonToBean(source, ElnTemplateCurrentVersionEntity.class); } private static void touch(ElnTemplateVersionEntity target, UserContext context) { target.setFLastModifyUserId(context.getUserId()); target.setFLastModifyTime(new Date()); } private static void touch(ElnTemplateCurrentVersionEntity target, UserContext context) { target.setFLastModifyUserId(context.getUserId()); target.setFLastModifyTime(new Date()); } private void auditVersionCreate(UserContext context, String operation, String sourceId, String targetId, String result) { Map snapshot = new LinkedHashMap<>(); snapshot.put("operation", operation); snapshot.put("sourceTemplateId", sourceId); snapshot.put("targetTemplateId", targetId); snapshot.put("result", result); auditRecorder.accept(AuditEventDTO.builder() .operationId(operation) .tenantId(context.getTenantId()) .operatorId(context.getUserId()) .eventTime(new Date()) .eventType("DATA_CHANGE") .actionCode("ELN_TEMPLATE_VERSION_CREATE") .actionLabel("ELN_TEMPLATE_VERSION_CREATE") .bizModule("eln") .bizType("eln_template_version") .targetTable("eln_template") .targetId(targetId) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private void auditLifecycle(String actionCode, String actionLabel, String targetId, UserContext context, Object before, Object after, String reason) { Map snapshot = new LinkedHashMap<>(); snapshot.put("before", before); snapshot.put("after", after); auditRecorder.accept(AuditEventDTO.builder() .tenantId(context.getTenantId()) .operatorId(context.getUserId()) .eventTime(new Date()) .eventType("BIZ_ACTION") .actionCode(actionCode) .actionLabel(actionLabel) .bizModule("eln") .bizType("eln_template") .targetTable("eln_template") .targetId(targetId) .reason(reason) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private void auditSwitch(UserContext context, String reason, String beforeTemplateId, String afterTemplateId) { Map snapshot = new LinkedHashMap<>(); snapshot.put("beforeTemplateId", beforeTemplateId); snapshot.put("afterTemplateId", afterTemplateId); auditRecorder.accept(AuditEventDTO.builder() .tenantId(context.getTenantId()) .operatorId(context.getUserId()) .eventTime(new Date()) .eventType("BIZ_ACTION") .actionCode("ELN_TEMPLATE_SWITCH_CURRENT") .actionLabel("切换 ELN 当前模板版本") .bizModule("eln") .bizType("eln_template") .targetTable("eln_template_current_version") .targetId(afterTemplateId) .reason(reason) .dataSnapshot(JsonUtil.getObjectToString(snapshot)) .build()); } private static ElnTemplateVos.ListItem toListItem(ElnTemplateVersionEntity source) { ElnTemplateVos.ListItem result = new ElnTemplateVos.ListItem(); result.setId(source.getId()); result.setMubanBianhao(source.getMubanBianhao()); result.setMubanBanben(source.getMubanBanben()); result.setJianyanXiangmu(source.getJianyanXiangmu()); result.setJianyanXiangmuBianma(source.getJianyanXiangmuBianma()); result.setJianyanXiangmuMingcheng(source.getJianyanXiangmuMingcheng()); result.setMubanFenlei(source.getMubanFenlei()); result.setMubanFenleiMingcheng(source.getMubanFenleiMingcheng()); result.setBizStatus(source.getBizStatus()); result.setBizEnabled(source.getBizEnabled()); result.setShengxiaoRiqi(source.getShengxiaoRiqi()); result.setGengxinShijian(source.getFLastModifyTime() == null ? source.getFCreatorTime() : source.getFLastModifyTime()); result.setChuangjianShijian(source.getChuangjianShijian() == null ? source.getFCreatorTime() : source.getChuangjianShijian()); return result; } private static ElnTemplateVos.Detail toDetail(ElnTemplateVersionEntity source) { ElnTemplateVos.Detail result = new ElnTemplateVos.Detail(); ElnTemplateVos.ListItem base = toListItem(source); result.setId(base.getId()); result.setMubanBianhao(base.getMubanBianhao()); result.setMubanBanben(base.getMubanBanben()); result.setJianyanXiangmu(base.getJianyanXiangmu()); result.setJianyanXiangmuBianma(base.getJianyanXiangmuBianma()); result.setJianyanXiangmuMingcheng(base.getJianyanXiangmuMingcheng()); result.setMubanFenlei(base.getMubanFenlei()); result.setMubanFenleiMingcheng(base.getMubanFenleiMingcheng()); result.setBizStatus(base.getBizStatus()); result.setBizEnabled(base.getBizEnabled()); result.setShengxiaoRiqi(base.getShengxiaoRiqi()); result.setGengxinShijian(base.getGengxinShijian()); result.setChuangjianShijian(base.getChuangjianShijian()); result.setMubanMingcheng(source.getMubanMingcheng()); result.setShiyanBuzhou(source.getShiyanBuzhou()); result.setChuangjianRenyuan(firstText( source.getChuangjianRenyuan(), source.getFCreatorUserId())); result.setMubanFile(source.getMubanFile()); result.setLaiyuanMubanId(source.getLaiyuanMubanId()); result.setShengbanCaozuoBianma(source.getShengbanCaozuoBianma()); return result; } private void mergeSourceChain(String tenantId, ElnTemplateVersionEntity current, ElnTemplateVos.Detail result) { Set visited = new LinkedHashSet<>(); visited.add(current.getId()); String sourceId = current.getLaiyuanMubanId(); int depth = 0; while (StringUtils.hasText(sourceId) && depth++ < MAX_SOURCE_CHAIN_DEPTH && visited.add(sourceId)) { ElnTemplateVersionEntity source = store.findTemplate(tenantId, sourceId); if (source == null) { break; } fillMissingDetailFields(result, source); sourceId = source.getLaiyuanMubanId(); } } private static void fillMissingDetailFields(ElnTemplateVos.Detail target, ElnTemplateVersionEntity source) { if (!StringUtils.hasText(target.getMubanMingcheng())) { target.setMubanMingcheng(source.getMubanMingcheng()); } if (!StringUtils.hasText(target.getShiyanBuzhou())) { target.setShiyanBuzhou(source.getShiyanBuzhou()); } if (!StringUtils.hasText(target.getJianyanXiangmu())) { target.setJianyanXiangmu(source.getJianyanXiangmu()); } if (!StringUtils.hasText(target.getJianyanXiangmuBianma())) { target.setJianyanXiangmuBianma(source.getJianyanXiangmuBianma()); } if (!StringUtils.hasText(target.getJianyanXiangmuMingcheng())) { target.setJianyanXiangmuMingcheng(source.getJianyanXiangmuMingcheng()); } if (!StringUtils.hasText(target.getMubanFenlei())) { target.setMubanFenlei(source.getMubanFenlei()); } if (!StringUtils.hasText(target.getMubanFenleiMingcheng())) { target.setMubanFenleiMingcheng(source.getMubanFenleiMingcheng()); } if (!StringUtils.hasText(target.getChuangjianRenyuan())) { target.setChuangjianRenyuan(firstText( source.getChuangjianRenyuan(), source.getFCreatorUserId())); } if (target.getChuangjianShijian() == null) { target.setChuangjianShijian(source.getChuangjianShijian() == null ? source.getFCreatorTime() : source.getChuangjianShijian()); } } private void enrichDetailNames(ElnTemplateVos.Detail result) { result.setChuangjianRenyuanMingcheng(resolveUserName( result.getChuangjianRenyuan())); } private String resolveUserName(String userId) { if (!StringUtils.hasText(userId)) { return userId; } try { String name = userNameResolver.resolve(userId); return StringUtils.hasText(name) ? name : userId; } catch (RuntimeException ignored) { return userId; } } private static String firstText(String first, String second) { return StringUtils.hasText(first) ? first : second; } private static UserNameResolver remoteUserNameResolver(UserApi userApi) { return userId -> { List users = userApi.getUserName(Collections.singletonList(userId)); if (users == null) { return null; } return users.stream() .filter(user -> userId.equals(user.getId())) .map(user -> firstText(user.getRealName(), user.getAccount())) .filter(StringUtils::hasText) .findFirst().orElse(null); }; } static String deterministicTargetId(String tenantId, String sourceTemplateId, String operationCode) { String seed = tenantId + "\u0000" + sourceTemplateId + "\u0000" + operationCode; return UUID.nameUUIDFromBytes(seed.getBytes(StandardCharsets.UTF_8)) .toString().replace("-", ""); } 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() : "0"; String userId = user == null || !StringUtils.hasText(user.getUserId()) ? null : user.getUserId().trim(); return new UserContext(tenantId, userId); } static final class UserContext { private final String tenantId; private final String userId; UserContext(String tenantId, String userId) { this.tenantId = tenantId; this.userId = userId; } String getTenantId() { return tenantId; } String getUserId() { return userId; } } interface TargetIdFactory { String create(String tenantId, String sourceTemplateId, String operationCode); } interface TemplateFileCopier { String copy(String sourceAttachments, String targetTemplateId); } interface TemplatePublishFence { String acquire(String tenantId, String templateId, String fileId, String fileType); void release(String tenantId, String templateId, String fileId, String fileType, String token); static TemplatePublishFence noop() { return new TemplatePublishFence() { @Override public String acquire(String tenantId, String templateId, String fileId, String fileType) { return "noop"; } @Override public void release(String tenantId, String templateId, String fileId, String fileType, String token) { } }; } } private static TemplatePublishFence remotePublishFence(TemplatePublishFenceApi api) { return new TemplatePublishFence() { @Override public String acquire(String tenantId, String templateId, String fileId, String fileType) { ActionResult result = api.acquire( tenantId, templateId, fileId, fileType); if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || !StringUtils.hasText(result.getData())) { throw new DataException(result == null || !StringUtils.hasText(result.getMsg()) ? "无法锁定 OnlyOffice 模板文件,请稍后重试" : result.getMsg()); } return result.getData(); } @Override public void release(String tenantId, String templateId, String fileId, String fileType, String token) { ActionResult result = api.release( tenantId, templateId, fileId, fileType, token); if (result == null || !ActionResultCode.SUCCESS.getCode().equals(result.getCode()) || !Boolean.TRUE.equals(result.getData())) { throw new DataException(result == null || !StringUtils.hasText(result.getMsg()) ? "OnlyOffice 发布锁释放失败,需人工恢复" : result.getMsg()); } } }; } interface TransactionRunner { ElnTemplateVos.Detail execute(Supplier callback); } private static final class SpringTransactionRunner implements TransactionRunner { private final TransactionTemplate transactionTemplate; private SpringTransactionRunner(PlatformTransactionManager transactionManager) { this.transactionTemplate = new TransactionTemplate(transactionManager); this.transactionTemplate.setPropagationBehavior( TransactionDefinition.PROPAGATION_REQUIRES_NEW); } @Override public ElnTemplateVos.Detail execute(Supplier callback) { return transactionTemplate.execute(status -> callback.get()); } } interface TemplateCatalogStore { ElnTemplateVersionEntity findTemplate(String tenantId, String templateId); ElnTemplateVersionEntity findTemplateForUpdate(String tenantId, String templateId); ElnTemplateVersionEntity findByOperation(String tenantId, String operationCode); ElnTemplateVersionEntity findByCodeAndVersion( String tenantId, String templateCode, String businessVersion); void insertTemplate(ElnTemplateVersionEntity template); void updateTemplateFile(ElnTemplateVersionEntity template); void updateTemplateLifecycle(ElnTemplateVersionEntity template); ElnTemplateCurrentVersionEntity findCurrent(String tenantId, String templateCode); List listCurrentVersions( String tenantId, List templateCodes); boolean insertCurrent(ElnTemplateCurrentVersionEntity current); void updateCurrent(ElnTemplateCurrentVersionEntity current); List listActiveBindings(String tenantId, String templateId); List listActiveBindingsByTemplateIds( String tenantId, List templateIds); default List listTemplateCategories( String tenantId, List templateCategoryIds) { return Collections.emptyList(); } void insertBinding(ElnTemplateBindingEntity binding); IPage page(String tenantId, ElnTemplateForms.Query query); IPage pageDirectory( String tenantId, ElnTemplateForms.Query query); } interface UserNameResolver { String resolve(String userId); } interface VersionGroupCopier { void copy(String tenantId, String sourceTemplateId, String targetTemplateId); } private static final class MyBatisTemplateCatalogStore implements TemplateCatalogStore { private final ElnTemplateMapper templateMapper; private final ElnTemplateBindingMapper bindingMapper; private final ElnTemplateCurrentVersionMapper currentMapper; private MyBatisTemplateCatalogStore(ElnTemplateMapper templateMapper, ElnTemplateBindingMapper bindingMapper, ElnTemplateCurrentVersionMapper currentMapper) { this.templateMapper = templateMapper; this.bindingMapper = bindingMapper; this.currentMapper = currentMapper; } @Override public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) { return templateMapper.selectOne(templateQuery(tenantId) .eq(ElnTemplateVersionEntity::getId, templateId)); } @Override public ElnTemplateVersionEntity findTemplateForUpdate( String tenantId, String templateId) { return templateMapper.selectOne(templateQuery(tenantId) .eq(ElnTemplateVersionEntity::getId, templateId) .last("FOR UPDATE")); } @Override public ElnTemplateVersionEntity findByOperation(String tenantId, String operationCode) { return templateMapper.selectOne(templateQuery(tenantId) .eq(ElnTemplateVersionEntity::getShengbanCaozuoBianma, operationCode)); } @Override public ElnTemplateVersionEntity findByCodeAndVersion( String tenantId, String templateCode, String businessVersion) { return templateMapper.selectOne(templateQuery(tenantId) .eq(ElnTemplateVersionEntity::getMubanBianhao, templateCode) .eq(ElnTemplateVersionEntity::getMubanBanben, businessVersion)); } @Override public void insertTemplate(ElnTemplateVersionEntity template) { templateMapper.insert(template); } @Override public void updateTemplateFile(ElnTemplateVersionEntity template) { Integer expectedVersion = template.getFVersion(); int nextVersion = expectedVersion == null ? 1 : expectedVersion + 1; LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .eq(ElnTemplateVersionEntity::getFTenantId, template.getFTenantId()) .eq(ElnTemplateVersionEntity::getId, template.getId()) .and(item -> item.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0)) .set(ElnTemplateVersionEntity::getMubanFile, template.getMubanFile()) .set(ElnTemplateVersionEntity::getFLastModifyUserId, template.getFLastModifyUserId()) .set(ElnTemplateVersionEntity::getFLastModifyTime, template.getFLastModifyTime()) .set(ElnTemplateVersionEntity::getFVersion, nextVersion); if (expectedVersion == null) { update.isNull(ElnTemplateVersionEntity::getFVersion); } else { update.eq(ElnTemplateVersionEntity::getFVersion, expectedVersion); } if (templateMapper.update(null, update) != 1) { throw new DataException("模板状态已变化,请刷新后重试"); } template.setFVersion(nextVersion); } @Override public void updateTemplateLifecycle(ElnTemplateVersionEntity template) { Integer expectedVersion = template.getFVersion(); int nextVersion = expectedVersion == null ? 1 : expectedVersion + 1; LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .eq(ElnTemplateVersionEntity::getFTenantId, template.getFTenantId()) .eq(ElnTemplateVersionEntity::getId, template.getId()) .and(item -> item.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0)) .set(ElnTemplateVersionEntity::getMubanFile, template.getMubanFile()) .set(ElnTemplateVersionEntity::getBizStatus, template.getBizStatus()) .set(ElnTemplateVersionEntity::getBizEnabled, template.getBizEnabled()) .set(ElnTemplateVersionEntity::getFLastModifyUserId, template.getFLastModifyUserId()) .set(ElnTemplateVersionEntity::getFLastModifyTime, template.getFLastModifyTime()) .set(ElnTemplateVersionEntity::getFVersion, nextVersion); if (expectedVersion == null) { update.isNull(ElnTemplateVersionEntity::getFVersion); } else { update.eq(ElnTemplateVersionEntity::getFVersion, expectedVersion); } if (templateMapper.update(null, update) != 1) { throw new DataException("模板状态已变化,请刷新后重试"); } template.setFVersion(nextVersion); } @Override public ElnTemplateCurrentVersionEntity findCurrent(String tenantId, String templateCode) { return currentMapper.selectOne(Wrappers.lambdaQuery() .eq(ElnTemplateCurrentVersionEntity::getFTenantId, tenantId) .eq(ElnTemplateCurrentVersionEntity::getMubanBianhao, templateCode) .and(item -> item.isNull(ElnTemplateCurrentVersionEntity::getFDeleteMark) .or().eq(ElnTemplateCurrentVersionEntity::getFDeleteMark, 0))); } @Override public List listCurrentVersions( String tenantId, List templateCodes) { if (templateCodes.isEmpty()) { return Collections.emptyList(); } return currentMapper.selectList( Wrappers.lambdaQuery() .eq(ElnTemplateCurrentVersionEntity::getFTenantId, tenantId) .in(ElnTemplateCurrentVersionEntity::getMubanBianhao, templateCodes) .and(item -> item.isNull(ElnTemplateCurrentVersionEntity::getFDeleteMark) .or().eq(ElnTemplateCurrentVersionEntity::getFDeleteMark, 0))); } @Override public boolean insertCurrent(ElnTemplateCurrentVersionEntity current) { return currentMapper.insert(current) == 1; } @Override public void updateCurrent(ElnTemplateCurrentVersionEntity current) { Integer expectedVersion = current.getFVersion(); int nextVersion = expectedVersion == null ? 1 : expectedVersion + 1; LambdaUpdateWrapper update = Wrappers.lambdaUpdate() .eq(ElnTemplateCurrentVersionEntity::getFTenantId, current.getFTenantId()) .eq(ElnTemplateCurrentVersionEntity::getMubanBianhao, current.getMubanBianhao()) .eq(ElnTemplateCurrentVersionEntity::getId, current.getId()) .and(item -> item.isNull(ElnTemplateCurrentVersionEntity::getFDeleteMark) .or().eq(ElnTemplateCurrentVersionEntity::getFDeleteMark, 0)) .set(ElnTemplateCurrentVersionEntity::getDangqianMubanId, current.getDangqianMubanId()) .set(ElnTemplateCurrentVersionEntity::getFLastModifyUserId, current.getFLastModifyUserId()) .set(ElnTemplateCurrentVersionEntity::getFLastModifyTime, current.getFLastModifyTime()) .set(ElnTemplateCurrentVersionEntity::getFVersion, nextVersion); if (expectedVersion == null) { update.isNull(ElnTemplateCurrentVersionEntity::getFVersion); } else { update.eq(ElnTemplateCurrentVersionEntity::getFVersion, expectedVersion); } if (currentMapper.update(null, update) != 1) { throw new DataException("当前模板已变化,请刷新后重试"); } current.setFVersion(nextVersion); } @Override public List listActiveBindings(String tenantId, String templateId) { return bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .eq(ElnTemplateBindingEntity::getMubanId, templateId) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, ElnTemplateEnums.BindingStatus.ACTIVE.name()) .and(item -> item.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))); } @Override public List listActiveBindingsByTemplateIds( String tenantId, List templateIds) { if (templateIds.isEmpty()) { return Collections.emptyList(); } return bindingMapper.selectList(Wrappers.lambdaQuery() .eq(ElnTemplateBindingEntity::getFTenantId, tenantId) .in(ElnTemplateBindingEntity::getMubanId, templateIds) .eq(ElnTemplateBindingEntity::getBangdingZhuangtai, ElnTemplateEnums.BindingStatus.ACTIVE.name()) .and(item -> item.isNull(ElnTemplateBindingEntity::getFDeleteMark) .or().eq(ElnTemplateBindingEntity::getFDeleteMark, 0))); } @Override public List listTemplateCategories( String tenantId, List templateCategoryIds) { if (templateCategoryIds.isEmpty()) { return Collections.emptyList(); } return templateMapper.selectTemplateCategories(tenantId, templateCategoryIds); } @Override public void insertBinding(ElnTemplateBindingEntity binding) { bindingMapper.insert(binding); } @Override public IPage page(String tenantId, ElnTemplateForms.Query form) { if (ElnTemplateForms.VersionScope.LATEST.equals(form.getVersionScope())) { return pageLatestVersions(tenantId, form); } LambdaQueryWrapper query = templateQuery(tenantId); applyPageFilters(query, form); query.orderByDesc(ElnTemplateVersionEntity::getFLastModifyTime) .orderByDesc(ElnTemplateVersionEntity::getFCreatorTime) .orderByDesc(ElnTemplateVersionEntity::getId); return templateMapper.selectPage(new Page<>(form.getCurrentPage(), form.getPageSize()), query); } @Override public IPage pageDirectory( String tenantId, ElnTemplateForms.Query form) { String keyword = trimToNull(form.getKeyword()); return templateMapper.selectTemplateDirectory( new Page<>(form.getCurrentPage(), form.getPageSize()), tenantId, trimToNull(form.getMubanFenlei()), trimToNull(form.getMubanBianhao()), trimToNull(form.getBizStatus()), trimToNull(form.getBizEnabled()), keyword == null ? null : "%" + keyword + "%"); } private IPage pageLatestVersions( String tenantId, ElnTemplateForms.Query form) { String keyword = trimToNull(form.getKeyword()); IPage codePage = templateMapper.selectLatestTemplateCodes( new Page<>(form.getCurrentPage(), form.getPageSize()), tenantId, trimToNull(form.getMubanFenlei()), trimToNull(form.getMubanBianhao()), trimToNull(form.getBizStatus()), trimToNull(form.getBizEnabled()), keyword == null ? null : "%" + keyword + "%"); Page result = new Page<>(codePage.getCurrent(), codePage.getSize()); result.setTotal(codePage.getTotal()); if (codePage.getRecords().isEmpty()) { return result; } LambdaQueryWrapper query = templateQuery(tenantId) .in(ElnTemplateVersionEntity::getMubanBianhao, codePage.getRecords()); applyPageFilters(query, form); List candidates = templateMapper.selectList(query); candidates.sort(MyBatisTemplateCatalogStore::compareLatestFirst); Map latestByCode = new LinkedHashMap<>(); for (ElnTemplateVersionEntity candidate : candidates) { latestByCode.putIfAbsent(candidate.getMubanBianhao(), candidate); } List records = codePage.getRecords().stream() .map(latestByCode::get) .filter(Objects::nonNull) .collect(Collectors.toList()); result.setRecords(records); return result; } private static void applyPageFilters( LambdaQueryWrapper query, ElnTemplateForms.Query form) { if (StringUtils.hasText(form.getMubanBianhao())) { query.eq(ElnTemplateVersionEntity::getMubanBianhao, form.getMubanBianhao().trim()); } if (StringUtils.hasText(form.getMubanFenlei())) { query.eq(ElnTemplateVersionEntity::getMubanFenlei, form.getMubanFenlei().trim()); } if (StringUtils.hasText(form.getBizStatus())) { query.eq(ElnTemplateVersionEntity::getBizStatus, form.getBizStatus().trim()); } if (StringUtils.hasText(form.getBizEnabled())) { query.eq(ElnTemplateVersionEntity::getBizEnabled, form.getBizEnabled().trim()); } if (StringUtils.hasText(form.getKeyword())) { String keyword = form.getKeyword().trim(); query.and(item -> item.like(ElnTemplateVersionEntity::getMubanBianhao, keyword) .or().like(ElnTemplateVersionEntity::getMubanBanben, keyword) .or().like(ElnTemplateVersionEntity::getJianyanXiangmu, keyword) .or().like(ElnTemplateVersionEntity::getJianyanXiangmuMingcheng, keyword)); } } private static int compareLatestFirst(ElnTemplateVersionEntity left, ElnTemplateVersionEntity right) { int byTime = Comparator.nullsLast(Comparator.reverseOrder()) .compare(versionTimestamp(left), versionTimestamp(right)); if (byTime != 0) { return byTime; } return Comparator.nullsLast(Comparator.reverseOrder()) .compare(left.getId(), right.getId()); } private static Date versionTimestamp(ElnTemplateVersionEntity version) { if (version.getFLastModifyTime() != null) { return version.getFLastModifyTime(); } if (version.getFCreatorTime() != null) { return version.getFCreatorTime(); } return version.getChuangjianShijian(); } private static String trimToNull(String value) { return StringUtils.hasText(value) ? value.trim() : null; } private static LambdaQueryWrapper templateQuery(String tenantId) { return Wrappers.lambdaQuery() .eq(ElnTemplateVersionEntity::getFTenantId, tenantId) .and(item -> item.isNull(ElnTemplateVersionEntity::getFDeleteMark) .or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0)); } } }