package jnpf.elnService.template.impl;
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.elnEntity.template.ElnTemplateBindingEntity;
|
import jnpf.elnEntity.template.ElnTemplateCurrentVersionEntity;
|
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.exception.DataException;
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
import org.junit.Test;
|
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.transaction.PlatformTransactionManager;
|
import org.springframework.transaction.TransactionDefinition;
|
import org.springframework.transaction.TransactionStatus;
|
import org.springframework.transaction.support.SimpleTransactionStatus;
|
import org.springframework.transaction.support.TransactionTemplate;
|
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Comparator;
|
import java.util.Date;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.concurrent.atomic.AtomicInteger;
|
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Proxy;
|
|
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertNotEquals;
|
import static org.junit.Assert.assertNull;
|
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.fail;
|
|
public class ElnTemplateCatalogServiceImplTest {
|
|
@Test
|
public void createsDraftVersionWithIndependentFileAndOnlyActiveBindings() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.store.templates.get("source-1").setMubanMingcheng("无菌检查模板");
|
fixture.store.templates.get("source-1").setShiyanBuzhou("按检验规程执行");
|
fixture.store.templates.get("source-1").setMubanFenlei("category-1");
|
fixture.store.templates.get("source-1").setMubanFenleiMingcheng("检验记录");
|
fixture.addBinding("binding-active", "source-1", "ACTIVE", "field-version-published");
|
fixture.store.sourceBindings.get(0).setCheckboxGroupCode("result_choice");
|
fixture.addBinding("binding-cancelled", "source-1", "CANCELLED", "field-version-other");
|
|
ElnTemplateVos.Detail result = fixture.service.createVersion("source-1", form("V2", "upgrade-1"));
|
|
ElnTemplateVersionEntity target = fixture.store.templates.get(result.getId());
|
assertNotEquals("source-1", target.getId());
|
assertEquals("TMP-001", target.getMubanBianhao());
|
assertEquals("V2", target.getMubanBanben());
|
assertEquals("无菌检查模板", target.getMubanMingcheng());
|
assertEquals("按检验规程执行", target.getShiyanBuzhou());
|
assertEquals("category-1", target.getMubanFenlei());
|
assertEquals("HD_jyxm_0001", target.getJianyanXiangmuBianma());
|
assertEquals("无菌检查", target.getJianyanXiangmuMingcheng());
|
assertEquals("检验记录", target.getMubanFenleiMingcheng());
|
assertEquals("DRAFT", target.getBizStatus());
|
assertEquals("enabled", target.getBizEnabled());
|
assertEquals("source-1", target.getLaiyuanMubanId());
|
assertEquals("upgrade-1", target.getShengbanCaozuoBianma());
|
assertEquals("user-1", target.getChuangjianRenyuan());
|
assertEquals("user-1", target.getFCreatorUserId());
|
assertEquals(target.getChuangjianShijian(), target.getFCreatorTime());
|
assertTrue(target.getChuangjianShijian() != null);
|
assertEquals("[{\"fileId\":\"" + target.getId() + ".docx\"}]", target.getMubanFile());
|
assertEquals(1, fixture.store.bindings.size());
|
ElnTemplateBindingEntity copied = fixture.store.bindings.get(0);
|
assertNotEquals("binding-active", copied.getId());
|
assertEquals(target.getId(), copied.getMubanId());
|
assertEquals("field-version-published", copied.getZiduanBanbenId());
|
assertEquals("PENDING", copied.getBangdingZhuangtai());
|
assertEquals("binding-active", copied.getLaiyuanBangdingId());
|
assertEquals("upgrade-1:binding-active", copied.getCaozuoBianma());
|
assertEquals("result_choice", copied.getCheckboxGroupCode());
|
assertNull(copied.getDaoqiShijian());
|
assertEquals(1, fixture.audits.size());
|
AuditEventDTO audit = fixture.audits.get(0);
|
assertEquals("ELN_TEMPLATE_VERSION_CREATE", audit.getActionCode());
|
assertEquals("upgrade-1", audit.getOperationId());
|
assertTrue(audit.getDataSnapshot().contains("source-1"));
|
assertTrue(audit.getDataSnapshot().contains(target.getId()));
|
assertTrue(audit.getDataSnapshot().contains("SUCCESS"));
|
assertFalse(audit.getDataSnapshot().toLowerCase().contains("ticket"));
|
}
|
|
@Test
|
public void leavesCopiedBindingStepEmptyUntilTargetStepsAreCreated() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addBinding("binding-active", "source-1", "ACTIVE", "field-version-published");
|
fixture.store.sourceBindings.get(0).setStepId("source-step");
|
fixture.versionGroupCopyHook = () -> {
|
ElnTemplateBindingEntity copied = fixture.copiedBinding("binding-active");
|
assertNull(copied.getStepId());
|
copied.setStepId("target-step");
|
};
|
|
ElnTemplateVos.Detail result = fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-step-remap"));
|
|
assertEquals("target-step", fixture.copiedBinding("binding-active").getStepId());
|
assertEquals(result.getId(), fixture.copiedBinding("binding-active").getMubanId());
|
}
|
|
@Test
|
public void createVersionLocksTheSourceTemplateBeforeCopyingBindings() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
|
fixture.service.createVersion("source-1", form("V2", "upgrade-with-lock"));
|
|
assertEquals(Arrays.asList("source-1"), fixture.store.lockedTemplateIds);
|
}
|
|
@Test
|
public void copiesRepeatGroupConfigurationOnCreateAndIdempotentRetry() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
|
ElnTemplateVos.Detail first = fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-repeat-groups"));
|
fixture.service.createVersion("source-1", form("V2", "upgrade-repeat-groups"));
|
|
assertEquals(Arrays.asList(
|
"tenant-1:source-1:" + first.getId(),
|
"tenant-1:source-1:" + first.getId()), fixture.repeatGroupCopies);
|
}
|
|
@Test
|
public void remapsCopiedReferenceToCopiedPrimary() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addBinding("primary-source", "source-1", "ACTIVE", "field-version-published");
|
fixture.addReferenceBinding("reference-source", "source-1", "ACTIVE",
|
"field-version-published", "primary-source");
|
|
fixture.service.createVersion("source-1", form("V2", "upgrade-1"));
|
|
ElnTemplateBindingEntity copiedPrimary = fixture.copiedBinding("primary-source");
|
ElnTemplateBindingEntity copiedReference = fixture.copiedBinding("reference-source");
|
assertEquals(copiedPrimary.getId(), copiedReference.getYinyongShiliId());
|
assertNotEquals("primary-source", copiedReference.getYinyongShiliId());
|
}
|
|
@Test
|
public void rejectsCopiedReferenceWhosePrimaryIsNotInActiveCopySet() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addReferenceBinding("reference-source", "source-1", "ACTIVE",
|
"field-version-published", "missing-primary");
|
|
expectDataException(() -> fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-1")));
|
|
assertTrue(fixture.store.bindings.isEmpty());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void retriesSameOperationIdempotentlyAndStillOverwritesFile() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addBinding("binding-1", "source-1", "ACTIVE", "field-version-1");
|
|
ElnTemplateVos.Detail first = fixture.service.createVersion("source-1", form("V2", "upgrade-1"));
|
ElnTemplateVos.Detail retried = fixture.service.createVersion("source-1", form("V2", "upgrade-1"));
|
|
assertEquals(first.getId(), retried.getId());
|
assertEquals(2, fixture.copiedTargetIds.size());
|
assertEquals(Arrays.asList(first.getId(), first.getId()), fixture.copiedTargetIds);
|
assertEquals(2, fixture.store.templates.size());
|
assertEquals(1, fixture.store.bindings.size());
|
}
|
|
@Test
|
public void rejectsSameOperationWhenEffectiveDateDiffers() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.service.createVersion("source-1", form("V2", "upgrade-1"));
|
ElnTemplateForms.CreateVersion changed = form("V2", "upgrade-1");
|
changed.setShengxiaoRiqi(new Date(1700000001000L));
|
|
expectDataException(() -> fixture.service.createVersion("source-1", changed));
|
}
|
|
@Test
|
public void rejectsConcurrentWinnerWhenEffectiveDateDiffersIncludingNull() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.store.concurrentWinnerOperation = "upgrade-1";
|
fixture.store.concurrentWinnerEffectiveDate = null;
|
|
expectDataException(() -> fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-1")));
|
}
|
|
@Test
|
public void resolvesConcurrentSameOperationAfterUniqueConflict() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.store.concurrentWinnerOperation = "upgrade-1";
|
|
ElnTemplateVos.Detail result = fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-1"));
|
|
assertEquals(ElnTemplateCatalogServiceImpl.deterministicTargetId(
|
"tenant-1", "source-1", "upgrade-1"), result.getId());
|
assertEquals(1, fixture.copiedTargetIds.size());
|
assertEquals(result.getId(), fixture.copiedTargetIds.get(0));
|
}
|
|
@Test
|
public void convertsConcurrentBusinessVersionConflictToBusinessError() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.store.concurrentWinnerOperation = "other-operation";
|
|
expectDataException(() -> fixture.service.createVersion(
|
"source-1", form("V2", "upgrade-1")));
|
|
assertTrue(fixture.copiedTargetIds.isEmpty());
|
}
|
|
@Test
|
public void isolatesCreateAndConflictResolutionFromAnOuterTransaction() throws Exception {
|
RecordingTransactionManager transactions = new RecordingTransactionManager();
|
Fixture fixture = new Fixture(springTransactionRunner(transactions));
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.store.concurrentWinnerOperation = "upgrade-1";
|
|
ElnTemplateVos.Detail result = new TransactionTemplate(transactions).execute(status ->
|
fixture.service.createVersion("source-1", form("V2", "upgrade-1")));
|
|
assertEquals(ElnTemplateCatalogServiceImpl.deterministicTargetId(
|
"tenant-1", "source-1", "upgrade-1"), result.getId());
|
assertEquals(Arrays.asList(
|
TransactionDefinition.PROPAGATION_REQUIRED,
|
TransactionDefinition.PROPAGATION_REQUIRES_NEW,
|
TransactionDefinition.PROPAGATION_REQUIRES_NEW),
|
transactions.propagations);
|
}
|
|
@Test
|
public void derivesSameTargetIdAfterDatabaseRollbackRetry() {
|
Fixture firstAttempt = new Fixture();
|
firstAttempt.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
Fixture retryAfterRollback = new Fixture();
|
retryAfterRollback.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
|
String firstId = firstAttempt.service.createVersion("source-1", form("V2", "upgrade-1")).getId();
|
String retryId = retryAfterRollback.service.createVersion("source-1", form("V2", "upgrade-1")).getId();
|
|
assertEquals(firstId, retryId);
|
assertEquals(firstId + ".docx", firstAttempt.copiedTargetIds.get(0) + ".docx");
|
assertEquals(firstAttempt.copiedTargetIds, retryAfterRollback.copiedTargetIds);
|
}
|
|
@Test
|
public void rejectsMissingCrossTenantSourceAndDuplicateBusinessVersion() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-2", "other-tenant", "TMP-002", "V1");
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "existing-v2", "TMP-001", "V2");
|
|
expectDataException(() -> fixture.service.createVersion("missing", form("V2", "op-1")));
|
expectDataException(() -> fixture.service.createVersion("other-tenant", form("V2", "op-2")));
|
expectDataException(() -> fixture.service.createVersion("source-1", form("V2", "op-3")));
|
|
assertTrue(fixture.copiedTargetIds.isEmpty());
|
}
|
|
@Test
|
public void doesNotWriteFabricatedAttachmentBindingOrAuditWhenFileCopyFails() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addBinding("binding-1", "source-1", "ACTIVE", "field-version-1");
|
fixture.fileFailure = true;
|
|
expectDataException(() -> fixture.service.createVersion("source-1", form("V2", "upgrade-1")));
|
|
ElnTemplateVersionEntity inserted = fixture.store.findByOperation("tenant-1", "upgrade-1");
|
assertNull(inserted.getMubanFile());
|
assertTrue(fixture.store.bindings.isEmpty());
|
assertTrue(fixture.audits.isEmpty());
|
}
|
|
@Test
|
public void pageAndDetailAreTenantScoped() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
fixture.addSource("tenant-2", "source-2", "TMP-002", "V1");
|
|
assertEquals(1, fixture.service.page(new ElnTemplateForms.Query()).getTotal());
|
assertEquals("source-1", fixture.service.detail("source-1").getId());
|
expectDataException(() -> fixture.service.detail("source-2"));
|
}
|
|
@Test
|
public void pageReturnsTemplateCreationTime() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
Date createdAt = new Date(1700000000000L);
|
fixture.store.templates.get("source-1").setChuangjianShijian(createdAt);
|
|
ElnTemplateVos.ListItem item = fixture.service.page(
|
new ElnTemplateForms.Query()).getRecords().get(0);
|
|
assertEquals(createdAt, item.getChuangjianShijian());
|
}
|
|
@Test
|
public void latestScopeReturnsOneRepresentativeVersionPerTemplateCode() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "template-v1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "template-v2", "TMP-001", "V2");
|
fixture.addSource("tenant-1", "other-v1", "TMP-002", "V1");
|
fixture.store.templates.get("template-v1").setFLastModifyTime(
|
new Date(1700000000000L));
|
fixture.store.templates.get("template-v2").setFLastModifyTime(
|
new Date(1800000000000L));
|
ElnTemplateForms.Query query = new ElnTemplateForms.Query();
|
query.setVersionScope(ElnTemplateForms.VersionScope.LATEST);
|
|
IPage<ElnTemplateVos.ListItem> page = fixture.service.page(query);
|
|
assertEquals(2L, page.getTotal());
|
assertEquals(2, page.getRecords().size());
|
assertEquals("template-v2", page.getRecords().stream()
|
.filter(item -> "TMP-001".equals(item.getMubanBianhao()))
|
.findFirst().orElseThrow(AssertionError::new).getId());
|
}
|
|
@Test
|
public void directoryUsesCurrentVersionAndKeepsTemplatesWithoutOne() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "template-v1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "template-v2", "TMP-001", "V2");
|
fixture.store.templates.get("template-v1").setShengxiaoRiqi(
|
new Date(1700000000000L));
|
fixture.store.templates.get("template-v2").setBizStatus("DRAFT");
|
fixture.store.templates.get("template-v2").setFLastModifyTime(
|
new Date(1800000000000L));
|
fixture.store.currentTemplates.put("TMP-001", "template-v1");
|
fixture.addBinding("binding-current", "template-v1", "ACTIVE",
|
"field-version-published");
|
fixture.addSource("tenant-1", "draft-only", "TMP-002", "R01");
|
fixture.store.templates.get("draft-only").setBizStatus("DRAFT");
|
|
IPage<ElnTemplateVos.CatalogItem> page = fixture.service.directory(
|
new ElnTemplateForms.Query());
|
|
assertEquals(2L, page.getTotal());
|
ElnTemplateVos.CatalogItem effective = page.getRecords().stream()
|
.filter(item -> "TMP-001".equals(item.getMubanBianhao()))
|
.findFirst().orElseThrow(AssertionError::new);
|
assertEquals("template-v1", effective.getShengxiaoMubanId());
|
assertEquals("V1", effective.getShengxiaoMubanBanben());
|
assertEquals("PUBLISHED", effective.getBizStatus());
|
assertEquals("enabled", effective.getBizEnabled());
|
assertEquals(Long.valueOf(1), effective.getZiduanShu());
|
|
ElnTemplateVos.CatalogItem draft = page.getRecords().stream()
|
.filter(item -> "TMP-002".equals(item.getMubanBianhao()))
|
.findFirst().orElseThrow(AssertionError::new);
|
assertNull(draft.getShengxiaoMubanId());
|
assertNull(draft.getShengxiaoMubanBanben());
|
assertNull(draft.getBizStatus());
|
assertNull(draft.getBizEnabled());
|
assertNull(draft.getZiduanShu());
|
assertEquals(1, fixture.store.bindingBatchReads);
|
}
|
|
@Test
|
public void allScopeFiltersByTrimmedTemplateCategory() throws Exception {
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(
|
new MybatisConfiguration(), "eln-template-category-test"),
|
ElnTemplateVersionEntity.class);
|
final AbstractWrapper<?, ?, ?>[] captured = new AbstractWrapper<?, ?, ?>[1];
|
ElnTemplateMapper mapper = mapperProxy((method, args) -> {
|
if ("selectPage".equals(method)) {
|
captured[0] = (AbstractWrapper<?, ?, ?>) args[1];
|
return new Page<ElnTemplateVersionEntity>(1, 20).setTotal(0);
|
}
|
return null;
|
});
|
ElnTemplateCatalogServiceImpl.TemplateCatalogStore store = myBatisStore(mapper);
|
ElnTemplateForms.Query query = new ElnTemplateForms.Query();
|
query.setMubanFenlei(" category-a ");
|
|
store.page("tenant-1", query);
|
|
assertTrue(captured[0].getSqlSegment().contains("muban_fenlei"));
|
assertTrue(captured[0].getParamNameValuePairs().containsValue("category-a"));
|
}
|
|
@Test
|
public void latestScopeFiltersCodePageAndCandidatesByTrimmedTemplateCategory()
|
throws Exception {
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(
|
new MybatisConfiguration(), "eln-template-category-latest-test"),
|
ElnTemplateVersionEntity.class);
|
ElnTemplateVersionEntity categoryAV1 = template(
|
"category-a-v1", "TMP-001", "category-a", 1700000000000L);
|
ElnTemplateVersionEntity categoryAV2 = template(
|
"category-a-v2", "TMP-001", "category-a", 1800000000000L);
|
ElnTemplateVersionEntity categoryBV1 = template(
|
"category-b-v1", "TMP-002", "category-b", 1900000000000L);
|
final String[] codePageCategory = new String[1];
|
final AbstractWrapper<?, ?, ?>[] candidateQuery =
|
new AbstractWrapper<?, ?, ?>[1];
|
ElnTemplateMapper mapper = mapperProxy((method, args) -> {
|
if ("selectLatestTemplateCodes".equals(method)) {
|
codePageCategory[0] = (String) args[2];
|
Page<String> page = new Page<>(1, 20);
|
if ("category-a".equals(codePageCategory[0])) {
|
return page.setRecords(Arrays.asList("TMP-001")).setTotal(1);
|
}
|
return page.setRecords(Arrays.asList("TMP-001", "TMP-002")).setTotal(2);
|
}
|
if ("selectList".equals(method)) {
|
candidateQuery[0] = (AbstractWrapper<?, ?, ?>) args[0];
|
candidateQuery[0].getSqlSegment();
|
if (candidateQuery[0].getParamNameValuePairs().containsValue("category-a")) {
|
return Arrays.asList(categoryAV1, categoryAV2);
|
}
|
return Arrays.asList(categoryAV1, categoryAV2, categoryBV1);
|
}
|
return null;
|
});
|
ElnTemplateCatalogServiceImpl.TemplateCatalogStore store = myBatisStore(mapper);
|
ElnTemplateForms.Query query = new ElnTemplateForms.Query();
|
query.setVersionScope(ElnTemplateForms.VersionScope.LATEST);
|
query.setMubanFenlei(" category-a ");
|
|
IPage<ElnTemplateVersionEntity> result = store.page("tenant-1", query);
|
|
assertEquals("category-a", codePageCategory[0]);
|
assertTrue(candidateQuery[0].getSqlSegment() + " "
|
+ candidateQuery[0].getParamNameValuePairs(),
|
candidateQuery[0].getParamNameValuePairs().containsValue("category-a"));
|
assertEquals(1L, result.getTotal());
|
assertEquals(1, result.getRecords().size());
|
assertEquals("category-a-v2", result.getRecords().get(0).getId());
|
}
|
|
@Test
|
public void detailRecoversDisplayFieldsFromSourceChainAndResolvesNames() {
|
Fixture fixture = new Fixture(userId -> "管理员", true);
|
fixture.addSource("tenant-1", "source-1", "TMP-001", "V1");
|
ElnTemplateVersionEntity source = fixture.store.templates.get("source-1");
|
source.setMubanMingcheng("微生物限度检验模板");
|
source.setShiyanBuzhou("按检验规程执行");
|
source.setMubanFenlei("category-1");
|
source.setJianyanXiangmuMingcheng("微生物限度");
|
source.setMubanFenleiMingcheng("检验记录");
|
source.setChuangjianRenyuan("user-1");
|
source.setChuangjianShijian(new Date(1700000000000L));
|
|
fixture.addSource("tenant-1", "version-2", "TMP-001", "V2");
|
ElnTemplateVersionEntity intermediate = fixture.store.templates.get("version-2");
|
intermediate.setLaiyuanMubanId("source-1");
|
intermediate.setJianyanXiangmuMingcheng(null);
|
intermediate.setMubanFenleiMingcheng(null);
|
fixture.addSource("tenant-1", "version-3", "TMP-001", "V3");
|
ElnTemplateVersionEntity current = fixture.store.templates.get("version-3");
|
current.setLaiyuanMubanId("version-2");
|
current.setJianyanXiangmuMingcheng(null);
|
current.setMubanFenleiMingcheng(null);
|
ElnTemplateVos.Detail detail = fixture.service.detail("version-3");
|
|
assertEquals("微生物限度检验模板", detail.getMubanMingcheng());
|
assertEquals("按检验规程执行", detail.getShiyanBuzhou());
|
assertEquals("item-1", detail.getJianyanXiangmu());
|
assertEquals("微生物限度", detail.getJianyanXiangmuMingcheng());
|
assertEquals("category-1", detail.getMubanFenlei());
|
assertEquals("检验记录", detail.getMubanFenleiMingcheng());
|
assertEquals("user-1", detail.getChuangjianRenyuan());
|
assertEquals("管理员", detail.getChuangjianRenyuanMingcheng());
|
assertEquals(new Date(1700000000000L), detail.getChuangjianShijian());
|
}
|
|
@Test
|
public void detailStopsAtCircularSourceChainAndFallsBackToCreatorId() {
|
Fixture fixture = new Fixture(userId -> {
|
throw new IllegalStateException("用户服务不可用");
|
}, true);
|
fixture.addSource("tenant-1", "version-1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "version-2", "TMP-001", "V2");
|
fixture.store.templates.get("version-1").setLaiyuanMubanId("version-2");
|
ElnTemplateVersionEntity current = fixture.store.templates.get("version-2");
|
current.setLaiyuanMubanId("version-1");
|
current.setFCreatorUserId("user-2");
|
|
ElnTemplateVos.Detail detail = fixture.service.detail("version-2");
|
|
assertEquals("user-2", detail.getChuangjianRenyuanMingcheng());
|
assertNull(detail.getMubanMingcheng());
|
}
|
|
@Test
|
public void pageReturnsCurrentPointerAndDistinctActiveFieldCountInBatch() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "template-v1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "template-v2", "TMP-001", "V2");
|
fixture.store.currentTemplates.put("TMP-001", "template-v2");
|
fixture.addBinding("primary", "template-v2", "ACTIVE", "field-version-1");
|
fixture.addReferenceBinding("reference", "template-v2", "ACTIVE",
|
"field-version-1", "primary");
|
fixture.addBinding("second-field", "template-v2", "ACTIVE", "field-version-2");
|
fixture.store.sourceBindings.get(2).setZiduanDingyiId("field-2");
|
fixture.addBinding("cancelled", "template-v2", "CANCELLED", "field-version-3");
|
fixture.store.sourceBindings.get(3).setZiduanDingyiId("field-3");
|
|
IPage<ElnTemplateVos.ListItem> page = fixture.service.page(new ElnTemplateForms.Query());
|
|
ElnTemplateVos.ListItem v1 = item(page, "template-v1");
|
ElnTemplateVos.ListItem v2 = item(page, "template-v2");
|
assertFalse(v1.getShifouDangqian());
|
assertEquals(Long.valueOf(0L), v1.getZiduanShu());
|
assertTrue(v2.getShifouDangqian());
|
assertEquals(Long.valueOf(2L), v2.getZiduanShu());
|
assertEquals(1, fixture.store.currentBatchReads);
|
assertEquals(1, fixture.store.bindingBatchReads);
|
}
|
|
@Test
|
public void pageReturnsTestDefinitionNameInBatch() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "template-v1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "template-v2", "TMP-002", "V1");
|
fixture.store.templates.get("template-v2").setJianyanXiangmu("item-2");
|
fixture.store.templates.get("template-v1").setJianyanXiangmuMingcheng("无菌检查");
|
fixture.store.templates.get("template-v2").setJianyanXiangmuMingcheng("可见异物");
|
|
IPage<ElnTemplateVos.ListItem> page =
|
fixture.service.page(new ElnTemplateForms.Query());
|
|
assertEquals("item-1", item(page, "template-v1").getJianyanXiangmu());
|
assertEquals("无菌检查",
|
item(page, "template-v1").getJianyanXiangmuMingcheng());
|
assertEquals("可见异物",
|
item(page, "template-v2").getJianyanXiangmuMingcheng());
|
}
|
|
@Test
|
public void pageReturnsTemplateCategoryNameInBatch() {
|
Fixture fixture = new Fixture();
|
fixture.addSource("tenant-1", "template-v1", "TMP-001", "V1");
|
fixture.addSource("tenant-1", "template-v2", "TMP-002", "V1");
|
fixture.store.templates.get("template-v1").setMubanFenlei("category-1");
|
fixture.store.templates.get("template-v2").setMubanFenlei("category-2");
|
fixture.store.templates.get("template-v1").setMubanFenleiMingcheng("检验记录");
|
fixture.store.templates.get("template-v2").setMubanFenleiMingcheng("仪器记录");
|
|
IPage<ElnTemplateVos.ListItem> page =
|
fixture.service.page(new ElnTemplateForms.Query());
|
|
assertEquals("category-1", item(page, "template-v1").getMubanFenlei());
|
assertEquals("检验记录",
|
item(page, "template-v1").getMubanFenleiMingcheng());
|
assertEquals("仪器记录",
|
item(page, "template-v2").getMubanFenleiMingcheng());
|
assertEquals(0, fixture.store.templateCategoryBatchReads);
|
}
|
|
private static ElnTemplateVos.ListItem item(
|
IPage<ElnTemplateVos.ListItem> page, String id) {
|
return page.getRecords().stream().filter(item -> id.equals(item.getId()))
|
.findFirst().orElseThrow(() -> new AssertionError("未找到模板: " + id));
|
}
|
|
private static ElnTemplateForms.CreateVersion form(String version, String operation) {
|
ElnTemplateForms.CreateVersion form = new ElnTemplateForms.CreateVersion();
|
form.setMubanBanben(version);
|
form.setShengbanCaozuoBianma(operation);
|
form.setShengxiaoRiqi(new Date(1700000000000L));
|
return form;
|
}
|
|
private static void expectDataException(Runnable action) {
|
try {
|
action.run();
|
fail("预期抛出 DataException");
|
} catch (DataException expected) {
|
assertFalse(expected.getMessage().trim().isEmpty());
|
}
|
}
|
|
private static ElnTemplateCatalogServiceImpl.TransactionRunner springTransactionRunner(
|
PlatformTransactionManager transactionManager) throws Exception {
|
Class<?> type = Class.forName(ElnTemplateCatalogServiceImpl.class.getName()
|
+ "$SpringTransactionRunner");
|
Constructor<?> constructor = type.getDeclaredConstructor(PlatformTransactionManager.class);
|
constructor.setAccessible(true);
|
return (ElnTemplateCatalogServiceImpl.TransactionRunner)
|
constructor.newInstance(transactionManager);
|
}
|
|
private static ElnTemplateMapper mapperProxy(
|
MapperInvocation invocation) {
|
return (ElnTemplateMapper) Proxy.newProxyInstance(
|
ElnTemplateMapper.class.getClassLoader(),
|
new Class<?>[]{ElnTemplateMapper.class},
|
(proxy, method, args) -> invocation.invoke(method.getName(), args));
|
}
|
|
private static ElnTemplateVersionEntity template(
|
String id, String code, String category, long lastModifyTime) {
|
ElnTemplateVersionEntity template = new ElnTemplateVersionEntity();
|
template.setId(id);
|
template.setFTenantId("tenant-1");
|
template.setMubanBianhao(code);
|
template.setMubanFenlei(category);
|
template.setFLastModifyTime(new Date(lastModifyTime));
|
return template;
|
}
|
|
private static ElnTemplateCatalogServiceImpl.TemplateCatalogStore myBatisStore(
|
ElnTemplateMapper mapper) throws Exception {
|
Constructor<?> constructor = Class.forName(
|
"jnpf.elnService.template.impl.ElnTemplateCatalogServiceImpl$MyBatisTemplateCatalogStore")
|
.getDeclaredConstructor(ElnTemplateMapper.class,
|
ElnTemplateBindingMapper.class, ElnTemplateCurrentVersionMapper.class);
|
constructor.setAccessible(true);
|
return (ElnTemplateCatalogServiceImpl.TemplateCatalogStore)
|
constructor.newInstance(mapper, null, null);
|
}
|
|
private interface MapperInvocation {
|
Object invoke(String method, Object[] args) throws Throwable;
|
}
|
|
private static final class Fixture {
|
private final MemoryStore store = new MemoryStore();
|
private final List<AuditEventDTO> audits = new ArrayList<>();
|
private final List<String> copiedTargetIds = new ArrayList<>();
|
private final List<String> repeatGroupCopies = new ArrayList<>();
|
private final AtomicInteger bindingIds = new AtomicInteger();
|
private boolean fileFailure;
|
private Runnable versionGroupCopyHook = () -> { };
|
private final ElnTemplateCatalogServiceImpl service;
|
|
private Fixture() {
|
this(callback -> callback.get());
|
}
|
|
private Fixture(ElnTemplateCatalogServiceImpl.UserNameResolver userNameResolver,
|
boolean customResolver) {
|
this(callback -> callback.get(), userNameResolver);
|
}
|
|
private Fixture(ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner) {
|
this(transactionRunner, userId -> userId);
|
}
|
|
private Fixture(ElnTemplateCatalogServiceImpl.TransactionRunner transactionRunner,
|
ElnTemplateCatalogServiceImpl.UserNameResolver userNameResolver) {
|
service = new ElnTemplateCatalogServiceImpl(
|
store,
|
() -> new ElnTemplateCatalogServiceImpl.UserContext("tenant-1", "user-1"),
|
ElnTemplateCatalogServiceImpl::deterministicTargetId,
|
() -> "binding-new-" + bindingIds.incrementAndGet(),
|
(sourceAttachments, targetTemplateId) -> {
|
copiedTargetIds.add(targetTemplateId);
|
if (fileFailure) {
|
throw new DataException("文件复制失败");
|
}
|
return "[{\"fileId\":\"" + targetTemplateId + ".docx\"}]";
|
},
|
audits::add,
|
transactionRunner,
|
java.util.Collections.emptyList(),
|
ElnTemplateCatalogServiceImpl.TemplatePublishFence.noop(),
|
userNameResolver,
|
(tenantId, sourceTemplateId, targetTemplateId) -> {
|
repeatGroupCopies.add(tenantId + ":" + sourceTemplateId + ":" + targetTemplateId);
|
versionGroupCopyHook.run();
|
});
|
}
|
|
private void addSource(String tenantId, String id, String code, String version) {
|
ElnTemplateVersionEntity template = new ElnTemplateVersionEntity();
|
template.setId(id);
|
template.setFTenantId(tenantId);
|
template.setMubanBianhao(code);
|
template.setMubanBanben(version);
|
template.setJianyanXiangmu("item-1");
|
template.setJianyanXiangmuBianma("HD_jyxm_0001");
|
template.setJianyanXiangmuMingcheng("无菌检查");
|
template.setMubanFile("[{\"fileId\":\"" + id + ".docx\",\"fileExtension\":\"docx\"}]");
|
template.setBizStatus("PUBLISHED");
|
template.setBizEnabled("enabled");
|
template.setFVersion(3);
|
store.templates.put(id, template);
|
}
|
|
private void addBinding(String id, String templateId, String status, String fieldVersionId) {
|
addBinding(id, templateId, status, fieldVersionId, "PRIMARY", null);
|
}
|
|
private void addReferenceBinding(String id, String templateId, String status,
|
String fieldVersionId, String primaryBindingId) {
|
addBinding(id, templateId, status, fieldVersionId, "REFERENCE", primaryBindingId);
|
}
|
|
private void addBinding(String id, String templateId, String status, String fieldVersionId,
|
String role, String primaryBindingId) {
|
ElnTemplateBindingEntity binding = new ElnTemplateBindingEntity();
|
binding.setId(id);
|
binding.setFTenantId("tenant-1");
|
binding.setMubanId(templateId);
|
binding.setMubanBianhao("TMP-001");
|
binding.setZiduanDingyiId("field-1");
|
binding.setZiduanBianma("sample_id");
|
binding.setZiduanBanbenId(fieldVersionId);
|
binding.setKongjianBiaoqian("eln.field.sample_id");
|
binding.setKongjianBieming("样品编号");
|
binding.setKongjianLeixing("TEXT");
|
binding.setKongjianJuese(role);
|
binding.setYinyongShiliId(primaryBindingId);
|
binding.setBangdingZhuangtai(status);
|
binding.setFVersion(4);
|
store.sourceBindings.add(binding);
|
}
|
|
private ElnTemplateBindingEntity copiedBinding(String sourceBindingId) {
|
for (ElnTemplateBindingEntity binding : store.bindings) {
|
if (sourceBindingId.equals(binding.getLaiyuanBangdingId())) {
|
return binding;
|
}
|
}
|
throw new AssertionError("未找到复制绑定: " + sourceBindingId);
|
}
|
}
|
|
private static final class MemoryStore implements ElnTemplateCatalogServiceImpl.TemplateCatalogStore {
|
private final Map<String, ElnTemplateVersionEntity> templates = new LinkedHashMap<>();
|
private final List<ElnTemplateBindingEntity> sourceBindings = new ArrayList<>();
|
private final List<ElnTemplateBindingEntity> bindings = new ArrayList<>();
|
private final List<String> lockedTemplateIds = new ArrayList<>();
|
private final Map<String, String> currentTemplates = new LinkedHashMap<>();
|
private final Map<String, String> testDefinitionNames = new LinkedHashMap<>();
|
private final Map<String, String> templateCategoryNames = new LinkedHashMap<>();
|
private int currentBatchReads;
|
private int bindingBatchReads;
|
private int testDefinitionBatchReads;
|
private int templateCategoryBatchReads;
|
private String concurrentWinnerOperation;
|
private Date concurrentWinnerEffectiveDate = new Date(1700000000000L);
|
|
@Override
|
public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) {
|
ElnTemplateVersionEntity template = templates.get(templateId);
|
return template != null && tenantId.equals(template.getFTenantId()) ? template : null;
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findTemplateForUpdate(String tenantId, String templateId) {
|
lockedTemplateIds.add(templateId);
|
return findTemplate(tenantId, templateId);
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findByOperation(String tenantId, String operationCode) {
|
for (ElnTemplateVersionEntity template : templates.values()) {
|
if (tenantId.equals(template.getFTenantId())
|
&& operationCode.equals(template.getShengbanCaozuoBianma())) {
|
return template;
|
}
|
}
|
return null;
|
}
|
|
@Override
|
public ElnTemplateVersionEntity findByCodeAndVersion(
|
String tenantId, String templateCode, String businessVersion) {
|
for (ElnTemplateVersionEntity template : templates.values()) {
|
if (tenantId.equals(template.getFTenantId())
|
&& templateCode.equals(template.getMubanBianhao())
|
&& businessVersion.equals(template.getMubanBanben())) {
|
return template;
|
}
|
}
|
return null;
|
}
|
|
@Override
|
public void insertTemplate(ElnTemplateVersionEntity template) {
|
if (concurrentWinnerOperation != null) {
|
ElnTemplateVersionEntity winner = new ElnTemplateVersionEntity();
|
winner.setId(ElnTemplateCatalogServiceImpl.deterministicTargetId(
|
template.getFTenantId(), template.getLaiyuanMubanId(),
|
concurrentWinnerOperation));
|
winner.setFTenantId(template.getFTenantId());
|
winner.setMubanBianhao(template.getMubanBianhao());
|
winner.setMubanBanben(template.getMubanBanben());
|
winner.setMubanFile("[{\"fileId\":\"" + winner.getId() + ".docx\"}]");
|
winner.setBizStatus("DRAFT");
|
winner.setBizEnabled("enabled");
|
winner.setLaiyuanMubanId(template.getLaiyuanMubanId());
|
winner.setShengbanCaozuoBianma(concurrentWinnerOperation);
|
winner.setShengxiaoRiqi(concurrentWinnerEffectiveDate);
|
templates.put(winner.getId(), winner);
|
concurrentWinnerOperation = null;
|
throw new DuplicateKeyException("模拟并发唯一冲突");
|
}
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public void updateTemplateFile(ElnTemplateVersionEntity template) {
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public void updateTemplateLifecycle(ElnTemplateVersionEntity template) {
|
templates.put(template.getId(), template);
|
}
|
|
@Override
|
public ElnTemplateCurrentVersionEntity findCurrent(String tenantId, String templateCode) {
|
String templateId = currentTemplates.get(templateCode);
|
if (templateId == null) {
|
return null;
|
}
|
ElnTemplateCurrentVersionEntity current = new ElnTemplateCurrentVersionEntity();
|
current.setFTenantId(tenantId);
|
current.setMubanBianhao(templateCode);
|
current.setDangqianMubanId(templateId);
|
return current;
|
}
|
|
@Override
|
public List<ElnTemplateCurrentVersionEntity> listCurrentVersions(
|
String tenantId, List<String> templateCodes) {
|
currentBatchReads++;
|
List<ElnTemplateCurrentVersionEntity> result = new ArrayList<>();
|
for (String code : templateCodes) {
|
ElnTemplateCurrentVersionEntity current = findCurrent(tenantId, code);
|
if (current != null) {
|
result.add(current);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public boolean insertCurrent(ElnTemplateCurrentVersionEntity current) {
|
throw new UnsupportedOperationException();
|
}
|
|
@Override
|
public void updateCurrent(ElnTemplateCurrentVersionEntity current) {
|
throw new UnsupportedOperationException();
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listActiveBindings(String tenantId, String templateId) {
|
List<ElnTemplateBindingEntity> result = new ArrayList<>();
|
for (ElnTemplateBindingEntity binding : sourceBindings) {
|
if (tenantId.equals(binding.getFTenantId())
|
&& templateId.equals(binding.getMubanId())
|
&& "ACTIVE".equals(binding.getBangdingZhuangtai())) {
|
result.add(binding);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public List<ElnTemplateBindingEntity> listActiveBindingsByTemplateIds(
|
String tenantId, List<String> templateIds) {
|
bindingBatchReads++;
|
List<ElnTemplateBindingEntity> result = new ArrayList<>();
|
for (ElnTemplateBindingEntity binding : sourceBindings) {
|
if (tenantId.equals(binding.getFTenantId())
|
&& templateIds.contains(binding.getMubanId())
|
&& "ACTIVE".equals(binding.getBangdingZhuangtai())) {
|
result.add(binding);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public List<ElnTemplateVos.TemplateCategory> listTemplateCategories(
|
String tenantId, List<String> templateCategoryIds) {
|
templateCategoryBatchReads++;
|
List<ElnTemplateVos.TemplateCategory> result = new ArrayList<>();
|
for (String id : templateCategoryIds) {
|
String name = templateCategoryNames.get(id);
|
if (name != null) {
|
ElnTemplateVos.TemplateCategory category =
|
new ElnTemplateVos.TemplateCategory();
|
category.setId(id);
|
category.setName(name);
|
result.add(category);
|
}
|
}
|
return result;
|
}
|
|
@Override
|
public void insertBinding(ElnTemplateBindingEntity binding) {
|
bindings.add(binding);
|
}
|
|
@Override
|
public IPage<ElnTemplateVersionEntity> page(String tenantId, ElnTemplateForms.Query query) {
|
List<ElnTemplateVersionEntity> result = new ArrayList<>();
|
for (ElnTemplateVersionEntity template : templates.values()) {
|
if (tenantId.equals(template.getFTenantId())) {
|
result.add(template);
|
}
|
}
|
if (ElnTemplateForms.VersionScope.LATEST.equals(query.getVersionScope())) {
|
Map<String, ElnTemplateVersionEntity> latestByCode = new LinkedHashMap<>();
|
result.sort((left, right) -> {
|
int byTime = Comparator.nullsLast(Comparator.<Date>reverseOrder())
|
.compare(left.getFLastModifyTime(), right.getFLastModifyTime());
|
return byTime != 0 ? byTime : right.getId().compareTo(left.getId());
|
});
|
for (ElnTemplateVersionEntity template : result) {
|
latestByCode.putIfAbsent(template.getMubanBianhao(), template);
|
}
|
result = new ArrayList<>(latestByCode.values());
|
}
|
return new Page<ElnTemplateVersionEntity>(query.getCurrentPage(), query.getPageSize())
|
.setRecords(result).setTotal(result.size());
|
}
|
|
@Override
|
public IPage<ElnTemplateVos.CatalogItem> pageDirectory(
|
String tenantId, ElnTemplateForms.Query query) {
|
Map<String, ElnTemplateVersionEntity> representativeByCode = new LinkedHashMap<>();
|
for (ElnTemplateVersionEntity template : templates.values()) {
|
if (tenantId.equals(template.getFTenantId())) {
|
representativeByCode.putIfAbsent(template.getMubanBianhao(), template);
|
}
|
}
|
List<ElnTemplateVos.CatalogItem> result = new ArrayList<>();
|
for (Map.Entry<String, ElnTemplateVersionEntity> entry
|
: representativeByCode.entrySet()) {
|
ElnTemplateVersionEntity representative = entry.getValue();
|
ElnTemplateVersionEntity effective = templates.get(
|
currentTemplates.get(entry.getKey()));
|
if (effective != null && (!tenantId.equals(effective.getFTenantId())
|
|| !entry.getKey().equals(effective.getMubanBianhao())
|
|| effective.getFDeleteMark() != null
|
&& effective.getFDeleteMark() != 0)) {
|
effective = null;
|
}
|
ElnTemplateVos.CatalogItem item = new ElnTemplateVos.CatalogItem();
|
item.setMubanBianhao(entry.getKey());
|
ElnTemplateVersionEntity metadata = effective == null
|
? representative : effective;
|
item.setJianyanXiangmu(metadata.getJianyanXiangmu());
|
item.setJianyanXiangmuMingcheng(
|
metadata.getJianyanXiangmuMingcheng());
|
item.setMubanFenlei(metadata.getMubanFenlei());
|
item.setMubanFenleiMingcheng(metadata.getMubanFenleiMingcheng());
|
if (effective != null) {
|
item.setShengxiaoMubanId(effective.getId());
|
item.setShengxiaoMubanBanben(effective.getMubanBanben());
|
item.setBizStatus(effective.getBizStatus());
|
item.setBizEnabled(effective.getBizEnabled());
|
item.setShengxiaoRiqi(effective.getShengxiaoRiqi());
|
}
|
result.add(item);
|
}
|
return new Page<ElnTemplateVos.CatalogItem>(
|
query.getCurrentPage(), query.getPageSize())
|
.setRecords(result).setTotal(result.size());
|
}
|
}
|
|
private static final class RecordingTransactionManager implements PlatformTransactionManager {
|
private final List<Integer> propagations = new ArrayList<>();
|
|
@Override
|
public TransactionStatus getTransaction(TransactionDefinition definition) {
|
propagations.add(definition.getPropagationBehavior());
|
return new SimpleTransactionStatus();
|
}
|
|
@Override
|
public void commit(TransactionStatus status) {
|
}
|
|
@Override
|
public void rollback(TransactionStatus status) {
|
}
|
}
|
}
|