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<UserContext> userContextSupplier;
|
private final TargetIdFactory targetIdFactory;
|
private final Supplier<String> bindingIdSupplier;
|
private final TemplateFileCopier fileCopier;
|
private final Consumer<AuditEventDTO> auditRecorder;
|
private final TransactionRunner transactionRunner;
|
private final List<ElnTemplatePublishValidator> 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<ElnTemplatePublishValidator> publishValidators,
|
ObjectProvider<AuditClient> 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<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder) {
|
this(store, userContextSupplier, targetIdFactory, bindingIdSupplier,
|
fileCopier, auditRecorder, callback -> callback.get(), Collections.emptyList(),
|
TemplatePublishFence.noop(), userId -> userId);
|
}
|
|
ElnTemplateCatalogServiceImpl(TemplateCatalogStore store,
|
Supplier<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder,
|
TransactionRunner transactionRunner) {
|
this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier,
|
auditRecorder, transactionRunner, Collections.emptyList(),
|
TemplatePublishFence.noop(), userId -> userId);
|
}
|
|
ElnTemplateCatalogServiceImpl(TemplateCatalogStore store,
|
Supplier<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder,
|
TransactionRunner transactionRunner,
|
List<ElnTemplatePublishValidator> publishValidators) {
|
this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier,
|
auditRecorder, transactionRunner, publishValidators,
|
TemplatePublishFence.noop(), userId -> userId);
|
}
|
|
ElnTemplateCatalogServiceImpl(TemplateCatalogStore store,
|
Supplier<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder,
|
TransactionRunner transactionRunner,
|
List<ElnTemplatePublishValidator> publishValidators,
|
TemplatePublishFence publishFence) {
|
this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier,
|
auditRecorder, transactionRunner, publishValidators, publishFence,
|
userId -> userId);
|
}
|
|
ElnTemplateCatalogServiceImpl(TemplateCatalogStore store,
|
Supplier<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder,
|
TransactionRunner transactionRunner,
|
List<ElnTemplatePublishValidator> publishValidators,
|
TemplatePublishFence publishFence,
|
UserNameResolver userNameResolver) {
|
this(store, userContextSupplier, targetIdFactory, bindingIdSupplier, fileCopier,
|
auditRecorder, transactionRunner, publishValidators, publishFence,
|
userNameResolver, (tenantId, sourceTemplateId, targetTemplateId) -> { });
|
}
|
|
ElnTemplateCatalogServiceImpl(TemplateCatalogStore store,
|
Supplier<UserContext> userContextSupplier,
|
TargetIdFactory targetIdFactory,
|
Supplier<String> bindingIdSupplier,
|
TemplateFileCopier fileCopier,
|
Consumer<AuditEventDTO> auditRecorder,
|
TransactionRunner transactionRunner,
|
List<ElnTemplatePublishValidator> 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<ElnTemplateVos.ListItem> 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<ElnTemplateVersionEntity> entities = store.page(context.getTenantId(), actual);
|
Page<ElnTemplateVos.ListItem> result = new Page<>(entities.getCurrent(), entities.getSize());
|
result.setTotal(entities.getTotal());
|
List<String> templateCodes = entities.getRecords().stream()
|
.map(ElnTemplateVersionEntity::getMubanBianhao)
|
.filter(StringUtils::hasText).distinct().collect(Collectors.toList());
|
Map<String, String> currentTemplateIds = store.listCurrentVersions(
|
context.getTenantId(), templateCodes).stream()
|
.collect(Collectors.toMap(ElnTemplateCurrentVersionEntity::getMubanBianhao,
|
ElnTemplateCurrentVersionEntity::getDangqianMubanId,
|
(first, second) -> first, LinkedHashMap::new));
|
List<String> templateIds = entities.getRecords().stream()
|
.map(ElnTemplateVersionEntity::getId).collect(Collectors.toList());
|
Map<String, Set<String>> 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<ElnTemplateVos.ListItem> 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<ElnTemplateVos.CatalogItem> 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<ElnTemplateVos.CatalogItem> result = store.pageDirectory(
|
context.getTenantId(), actual);
|
List<String> effectiveTemplateIds = result.getRecords().stream()
|
.map(ElnTemplateVos.CatalogItem::getShengxiaoMubanId)
|
.filter(StringUtils::hasText).collect(Collectors.toList());
|
Map<String, Set<String>> 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<ElnTemplateBindingEntity> sourceBindings =
|
store.listActiveBindings(tenantId, sourceId);
|
Map<String, ElnTemplateBindingEntity> sourceBindingsById = new LinkedHashMap<>();
|
Map<String, String> 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<ElnDocumentFileVo> files = JsonUtil.getJsonToList(attachments, ElnDocumentFileVo.class);
|
List<String> 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<ElnTemplateVos.Diagnostic> 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<ElnTemplateVos.Diagnostic> 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<ElnTemplateVos.Diagnostic> 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<ElnDocumentFileVo> 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<ElnTemplateVos.Diagnostic> validate(String tenantId, String templateId) {
|
return validate(tenantId, templateId, null);
|
}
|
|
@Override
|
public List<ElnTemplateVos.Diagnostic> validate(
|
String tenantId, String templateId, String attachments) {
|
ElnTemplateHealthService.HealthReport report = healthService.inspect(templateId, attachments);
|
List<ElnTemplateVos.Diagnostic> 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<ElnTemplatePublishValidator> withHealthValidator(
|
ElnTemplateHealthService healthService,
|
List<ElnTemplatePublishValidator> injectedValidators) {
|
List<ElnTemplatePublishValidator> 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<String, ElnTemplateBindingEntity> sourceBindings,
|
Map<String, String> 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<String, ElnTemplateBindingEntity> sourceBindings,
|
Map<String, String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String> 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<UserEntity> 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<String> 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<Boolean> 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<ElnTemplateVos.Detail> 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<ElnTemplateVos.Detail> 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<ElnTemplateCurrentVersionEntity> listCurrentVersions(
|
String tenantId, List<String> templateCodes);
|
|
boolean insertCurrent(ElnTemplateCurrentVersionEntity current);
|
|
void updateCurrent(ElnTemplateCurrentVersionEntity current);
|
|
List<ElnTemplateBindingEntity> listActiveBindings(String tenantId, String templateId);
|
|
List<ElnTemplateBindingEntity> listActiveBindingsByTemplateIds(
|
String tenantId, List<String> templateIds);
|
|
default List<ElnTemplateVos.TemplateCategory> listTemplateCategories(
|
String tenantId, List<String> templateCategoryIds) {
|
return Collections.emptyList();
|
}
|
|
void insertBinding(ElnTemplateBindingEntity binding);
|
|
IPage<ElnTemplateVersionEntity> page(String tenantId, ElnTemplateForms.Query query);
|
|
IPage<ElnTemplateVos.CatalogItem> 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<ElnTemplateVersionEntity> update =
|
Wrappers.<ElnTemplateVersionEntity>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<ElnTemplateVersionEntity> update =
|
Wrappers.<ElnTemplateVersionEntity>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.<ElnTemplateCurrentVersionEntity>lambdaQuery()
|
.eq(ElnTemplateCurrentVersionEntity::getFTenantId, tenantId)
|
.eq(ElnTemplateCurrentVersionEntity::getMubanBianhao, templateCode)
|
.and(item -> item.isNull(ElnTemplateCurrentVersionEntity::getFDeleteMark)
|
.or().eq(ElnTemplateCurrentVersionEntity::getFDeleteMark, 0)));
|
}
|
|
@Override
|
public List<ElnTemplateCurrentVersionEntity> listCurrentVersions(
|
String tenantId, List<String> templateCodes) {
|
if (templateCodes.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return currentMapper.selectList(
|
Wrappers.<ElnTemplateCurrentVersionEntity>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<ElnTemplateCurrentVersionEntity> update =
|
Wrappers.<ElnTemplateCurrentVersionEntity>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<ElnTemplateBindingEntity> listActiveBindings(String tenantId, String templateId) {
|
return bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>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<ElnTemplateBindingEntity> listActiveBindingsByTemplateIds(
|
String tenantId, List<String> templateIds) {
|
if (templateIds.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return bindingMapper.selectList(Wrappers.<ElnTemplateBindingEntity>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<ElnTemplateVos.TemplateCategory> listTemplateCategories(
|
String tenantId, List<String> templateCategoryIds) {
|
if (templateCategoryIds.isEmpty()) {
|
return Collections.emptyList();
|
}
|
return templateMapper.selectTemplateCategories(tenantId, templateCategoryIds);
|
}
|
|
@Override
|
public void insertBinding(ElnTemplateBindingEntity binding) {
|
bindingMapper.insert(binding);
|
}
|
|
@Override
|
public IPage<ElnTemplateVersionEntity> page(String tenantId, ElnTemplateForms.Query form) {
|
if (ElnTemplateForms.VersionScope.LATEST.equals(form.getVersionScope())) {
|
return pageLatestVersions(tenantId, form);
|
}
|
LambdaQueryWrapper<ElnTemplateVersionEntity> 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<ElnTemplateVos.CatalogItem> 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<ElnTemplateVersionEntity> pageLatestVersions(
|
String tenantId, ElnTemplateForms.Query form) {
|
String keyword = trimToNull(form.getKeyword());
|
IPage<String> 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<ElnTemplateVersionEntity> result =
|
new Page<>(codePage.getCurrent(), codePage.getSize());
|
result.setTotal(codePage.getTotal());
|
if (codePage.getRecords().isEmpty()) {
|
return result;
|
}
|
|
LambdaQueryWrapper<ElnTemplateVersionEntity> query = templateQuery(tenantId)
|
.in(ElnTemplateVersionEntity::getMubanBianhao, codePage.getRecords());
|
applyPageFilters(query, form);
|
List<ElnTemplateVersionEntity> candidates = templateMapper.selectList(query);
|
candidates.sort(MyBatisTemplateCatalogStore::compareLatestFirst);
|
Map<String, ElnTemplateVersionEntity> latestByCode = new LinkedHashMap<>();
|
for (ElnTemplateVersionEntity candidate : candidates) {
|
latestByCode.putIfAbsent(candidate.getMubanBianhao(), candidate);
|
}
|
List<ElnTemplateVersionEntity> records = codePage.getRecords().stream()
|
.map(latestByCode::get)
|
.filter(Objects::nonNull)
|
.collect(Collectors.toList());
|
result.setRecords(records);
|
return result;
|
}
|
|
private static void applyPageFilters(
|
LambdaQueryWrapper<ElnTemplateVersionEntity> 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.<Date>reverseOrder())
|
.compare(versionTimestamp(left), versionTimestamp(right));
|
if (byTime != 0) {
|
return byTime;
|
}
|
return Comparator.nullsLast(Comparator.<String>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<ElnTemplateVersionEntity> templateQuery(String tenantId) {
|
return Wrappers.<ElnTemplateVersionEntity>lambdaQuery()
|
.eq(ElnTemplateVersionEntity::getFTenantId, tenantId)
|
.and(item -> item.isNull(ElnTemplateVersionEntity::getFDeleteMark)
|
.or().eq(ElnTemplateVersionEntity::getFDeleteMark, 0));
|
}
|
}
|
}
|