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 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 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(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 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 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 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 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 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 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 audits = new ArrayList<>(); private final List copiedTargetIds = new ArrayList<>(); private final List 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 templates = new LinkedHashMap<>(); private final List sourceBindings = new ArrayList<>(); private final List bindings = new ArrayList<>(); private final List lockedTemplateIds = new ArrayList<>(); private final Map currentTemplates = new LinkedHashMap<>(); private final Map testDefinitionNames = new LinkedHashMap<>(); private final Map 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 listCurrentVersions( String tenantId, List templateCodes) { currentBatchReads++; List 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 listActiveBindings(String tenantId, String templateId) { List 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 listActiveBindingsByTemplateIds( String tenantId, List templateIds) { bindingBatchReads++; List 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 listTemplateCategories( String tenantId, List templateCategoryIds) { templateCategoryBatchReads++; List 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 page(String tenantId, ElnTemplateForms.Query query) { List result = new ArrayList<>(); for (ElnTemplateVersionEntity template : templates.values()) { if (tenantId.equals(template.getFTenantId())) { result.add(template); } } if (ElnTemplateForms.VersionScope.LATEST.equals(query.getVersionScope())) { Map latestByCode = new LinkedHashMap<>(); result.sort((left, right) -> { int byTime = Comparator.nullsLast(Comparator.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(query.getCurrentPage(), query.getPageSize()) .setRecords(result).setTotal(result.size()); } @Override public IPage pageDirectory( String tenantId, ElnTemplateForms.Query query) { Map representativeByCode = new LinkedHashMap<>(); for (ElnTemplateVersionEntity template : templates.values()) { if (tenantId.equals(template.getFTenantId())) { representativeByCode.putIfAbsent(template.getMubanBianhao(), template); } } List result = new ArrayList<>(); for (Map.Entry 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( query.getCurrentPage(), query.getPageSize()) .setRecords(result).setTotal(result.size()); } } private static final class RecordingTransactionManager implements PlatformTransactionManager { private final List 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) { } } }