package jnpf.elnService.formula.impl; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.audit.model.AuditEventDTO; import jnpf.elnEntity.formula.ElnFormulaDefEntity; import jnpf.elnEntity.formula.ElnFormulaForms; import jnpf.elnEntity.formula.ElnFormulaParameterSlotEntity; import jnpf.elnEntity.formula.ElnFormulaVersionEntity; import jnpf.elnEntity.formula.ElnFormulaVos; import jnpf.elnEntity.formula.ElnTemplateFormulaBindingEntity; import jnpf.elnEntity.template.ElnTemplateVersionEntity; import jnpf.elnService.formula.ElnFormulaCatalogService; import jnpf.elnService.impl.ElnFieldCodeGenerator; import jnpf.exception.DataException; import org.junit.Test; import org.springframework.dao.DuplicateKeyException; import org.springframework.transaction.support.TransactionSynchronizationManager; import java.lang.reflect.Method; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; 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 ElnFormulaCatalogServiceImplTest { @Test public void createsFormulaWithGeneratedCodeVersionOneSlotsAndAudit() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail detail = fixture.service.create(formula("含量计算", "${样品质量}", parameter("样品质量", "DECIMAL"))); assertTrue(detail.getGongshiBianma().matches("hanliang_jisuan_[a-z0-9]{8}")); assertEquals("enabled", detail.getBizEnabled()); assertNull(detail.getDangqianGongshiBanbenId()); assertEquals(1, detail.getVersions().size()); ElnFormulaVos.Version version = detail.getVersions().get(0); assertEquals(Integer.valueOf(1), version.getBanbenHao()); assertEquals("DRAFT", version.getBizStatus()); assertTrue(version.getParameters().get(0).getCanshuBianma() .matches("yang_pin_zhi_liang_[a-z0-9]{8}")); assertEquals(Arrays.asList("ELN_FORMULA_CREATE", "ELN_FORMULA_VERSION_CREATE"), fixture.auditActions()); } @Test public void createsFormulaCodesWithWordLevelPinyinForDomainAndMixedNames() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail domain = fixture.service.create(formula("有关物质计算", "1")); ElnFormulaVos.Detail mixed = fixture.service.create(formula("HPLC含量计算", "1")); assertTrue(domain.getGongshiBianma().matches("youguan_wuzhi_jisuan_[a-z0-9]{8}")); assertTrue(mixed.getGongshiBianma().matches("hplc_hanliang_jisuan_[a-z0-9]{8}")); } @Test public void createsDraftWithStableParameterCodesAndRejectsEmptyExpressionAtPreRelease() { Fixture fixture = new Fixture(); ElnFormulaForms.CreateDraft draft = new ElnFormulaForms.CreateDraft(); draft.setGongshiMingcheng("含量计算草稿"); draft.setParameters(Arrays.asList(parameter("样品质量", "DECIMAL"), parameter("稀释倍数", "DECIMAL"))); ElnFormulaVos.Detail detail = fixture.service.createDraft(draft); ElnFormulaVos.Version version = detail.getVersions().get(0); assertEquals("DRAFT", version.getBizStatus()); assertEquals("", version.getBiaodashi()); assertEquals("DECIMAL", version.getJieguoLeixing()); assertNull(version.getXiuyueMoshi()); assertNull(version.getXiuyueXiaoshuWei()); assertEquals(2, version.getParameters().size()); assertTrue(version.getParameters().get(0).getCanshuBianma().matches("yang_pin_zhi_liang_[a-z0-9]{8}")); assertTrue(version.getParameters().get(1).getCanshuBianma().matches("xi_shi_bei_shu_[a-z0-9]{8}")); assertNotEquals(version.getParameters().get(0).getCanshuBianma(), version.getParameters().get(1).getCanshuBianma()); assertEquals(Arrays.asList("ELN_FORMULA_CREATE", "ELN_FORMULA_VERSION_CREATE"), fixture.auditActions()); expectDataException(() -> fixture.service.preRelease(version.getId())); assertEquals("DRAFT", fixture.service.detail(detail.getId()).getVersions().get(0).getBizStatus()); } @Test public void updatesAuditableDefinitionWithoutChangingCode() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("原名称", "1")); ElnFormulaForms.UpdateDefinition update = new ElnFormulaForms.UpdateDefinition(); update.setGongshiMingcheng("新名称"); update.setYewuYongtu("用于复核"); update.setBeizhu("变更说明"); ElnFormulaVos.Detail updated = fixture.service.updateDefinition(created.getId(), update); assertEquals(created.getGongshiBianma(), updated.getGongshiBianma()); assertEquals("新名称", updated.getGongshiMingcheng()); assertEquals("用于复核", updated.getYewuYongtu()); assertEquals("ELN_FORMULA_UPDATE", fixture.auditActions().get(2)); assertTrue(fixture.audits.get(2).getDataSnapshot().contains("原名称")); assertTrue(fixture.audits.get(2).getDataSnapshot().contains("新名称")); } @Test public void copiesSlotCodesAndDraftEditOnlyDeletesCurrentVersionSlots() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("含量", "1", parameter("质量", "DECIMAL"), parameter("体积", "DECIMAL"))); ElnFormulaVos.Version v1 = created.getVersions().get(0); fixture.service.preRelease(v1.getId()); fixture.service.publish(v1.getId()); ElnFormulaVos.Version v2 = fixture.service.createVersion(created.getId(), copyOf(v1.getId())); ElnFormulaForms.Parameter kept = parameterFrom(v2.getParameters().get(0)); ElnFormulaForms.Parameter added = parameter("稀释倍数", "DECIMAL"); ElnFormulaForms.UpdateVersion update = version("${" + keptCode(v2, 0) + "}", kept, added); ElnFormulaVos.Version updated = fixture.service.updateDraft(v2.getId(), update); assertEquals(v1.getParameters().get(0).getCanshuBianma(), updated.getParameters().get(0).getCanshuBianma()); assertTrue(updated.getParameters().get(1).getCanshuBianma() .matches("xi_shi_bei_shu_[a-z0-9]{8}")); assertEquals(2, fixture.service.detail(created.getId()).getVersions().get(0) .getParameters().size()); assertEquals(2, updated.getParameters().size()); assertNotEquals(v1.getParameters().get(1).getId(), updated.getParameters().get(1).getId()); } @Test public void neverReusesCancelledVersionNumberAndPublishedContentIsImmutable() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("平均值", "1")); String v1 = created.getVersions().get(0).getId(); fixture.service.preRelease(v1); fixture.service.publish(v1); ElnFormulaVos.Version v2 = fixture.service.createVersion(created.getId(), copyOf(v1)); fixture.service.cancel(v2.getId(), "不再使用"); ElnFormulaVos.Version v3 = fixture.service.createVersion(created.getId(), copyOf(v1)); assertEquals(Integer.valueOf(2), v2.getBanbenHao()); assertEquals(Integer.valueOf(3), v3.getBanbenHao()); expectDataException(() -> fixture.service.updateDraft(v1, version("2"))); } @Test public void followsLifecyclePublishesCurrentAndSwitchesOnlySamePublishedFormula() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail first = fixture.service.create(formula("公式一", "1")); String v1 = first.getVersions().get(0).getId(); ElnFormulaVos.Detail other = fixture.service.create(formula("公式二", "2")); String otherV1 = other.getVersions().get(0).getId(); expectDataException(() -> fixture.service.publish(v1)); fixture.service.preRelease(v1); expectDataException(() -> fixture.service.updateDraft(v1, version("3"))); fixture.service.publish(v1); assertEquals(v1, fixture.service.detail(first.getId()).getDangqianGongshiBanbenId()); expectDataException(() -> fixture.service.switchCurrent(first.getId(), otherV1, "错误切换")); expectDataException(() -> fixture.service.switchCurrent(other.getId(), otherV1, "尚未发布")); } @Test public void blocksCancellingReferencedPreReleaseVersion() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("引用公式", "1")); String versionId = created.getVersions().get(0).getId(); fixture.service.preRelease(versionId); fixture.hasDraftReference = true; expectDataException(() -> fixture.service.cancel(versionId, "尝试作废")); assertEquals("PRE_RELEASE", fixture.service.detail(created.getId()) .getVersions().get(0).getBizStatus()); } @Test public void blocksCancellingPreReleaseWhenReferenceCheckerIsUnavailable() { Fixture fixture = new Fixture(false, new String[0]); ElnFormulaVos.Detail created = fixture.service.create(formula("引用检查不可用", "1")); String versionId = created.getVersions().get(0).getId(); fixture.service.preRelease(versionId); expectDataException("引用检查能力不可用", () -> fixture.service.cancel(versionId, "尝试作废")); assertEquals("PRE_RELEASE", fixture.service.detail(created.getId()) .getVersions().get(0).getBizStatus()); } @Test public void allowsCancellingPreReleaseWhenCheckerConfirmsNoDraftReference() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("无引用公式", "1")); String versionId = created.getVersions().get(0).getId(); fixture.service.preRelease(versionId); fixture.service.cancel(versionId, "确认无引用后作废"); assertEquals("CANCELLED", fixture.service.detail(created.getId()) .getVersions().get(0).getBizStatus()); } @Test public void preReleaseReloadsStoredVersionAndSlotsForValidation() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("数据库校验", "1", parameter("质量", "DECIMAL"))); String versionId = created.getVersions().get(0).getId(); fixture.store.versions.get(versionId).setBiaodashi("${unknown_a1}"); expectDataException(() -> fixture.service.preRelease(versionId)); assertEquals("DRAFT", fixture.store.versions.get(versionId).getBizStatus()); } @Test public void rejectsCancelReasonLongerThanDatabaseColumn() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("原因长度", "1")); String versionId = created.getVersions().get(0).getId(); fixture.service.preRelease(versionId); expectDataException(() -> fixture.service.cancel(versionId, repeat("原", 501))); assertEquals("PRE_RELEASE", fixture.service.detail(created.getId()) .getVersions().get(0).getBizStatus()); } @Test public void generatesBoundedValidCodesAndRetriesFormulaAndParameterCollisions() { Fixture fixture = new Fixture("same0001", "same0001", "next0002", "slot0001", "slot0001", "slot0002"); fixture.service.create(formula("同名", "1")); ElnFormulaVos.Detail second = fixture.service.create(formula("同名", "${参数}", parameter("参数", "DECIMAL"), parameter("参数", "DECIMAL"))); ElnFormulaVos.Detail longName = fixture.service.create(formula(repeat("超长公式", 20), "1")); ElnFormulaVos.Detail numeric = fixture.service.create(formula("123公式", "1")); ElnFormulaVos.Detail fallback = fixture.service.create(formula("***", "1")); assertTrue(second.getGongshiBianma().endsWith("next0002")); assertTrue(longName.getGongshiBianma().length() <= 80); assertTrue(numeric.getGongshiBianma().startsWith("f_123_gongshi_")); assertTrue(fallback.getGongshiBianma().startsWith("formula_")); for (ElnFormulaVos.Parameter parameter : second.getVersions().get(0).getParameters()) { assertTrue(parameter.getCanshuBianma().length() <= 80); assertTrue(parameter.getCanshuBianma().matches("[a-z][a-z0-9_]*")); } assertNotEquals(second.getVersions().get(0).getParameters().get(0).getCanshuBianma(), second.getVersions().get(0).getParameters().get(1).getCanshuBianma()); ElnFormulaVos.Detail parameterEdges = fixture.service.create(formula("参数边界", "1", parameter(repeat("超长参数", 20), "DECIMAL"), parameter("123参数", "DECIMAL"), parameter("***", "DECIMAL"))); List edgeParameters = parameterEdges.getVersions().get(0) .getParameters(); assertTrue(edgeParameters.get(0).getCanshuBianma().length() <= 80); assertTrue(edgeParameters.get(1).getCanshuBianma().startsWith("p_123_can_shu_")); assertTrue(edgeParameters.get(2).getCanshuBianma().startsWith("parameter_")); for (ElnFormulaVos.Parameter parameter : edgeParameters) { assertTrue(parameter.getCanshuBianma().matches("[a-z][a-z0-9_]*")); } } @Test public void activeTransactionDelegatesAuditsImmediatelyWithoutRegisteringSynchronization() { Fixture fixture = new Fixture(); TransactionSynchronizationManager.setActualTransactionActive(true); TransactionSynchronizationManager.initSynchronization(); try { fixture.service.create(formula("回滚公式", "1")); assertEquals(Arrays.asList("ELN_FORMULA_CREATE", "ELN_FORMULA_VERSION_CREATE"), fixture.auditActions()); assertTrue(TransactionSynchronizationManager.getSynchronizations().isEmpty()); } finally { TransactionSynchronizationManager.clearSynchronization(); TransactionSynchronizationManager.setActualTransactionActive(false); } } @Test public void catalogContractHasNoDeleteMethod() { for (Method method : ElnFormulaCatalogService.class.getMethods()) { assertFalse("delete".equals(method.getName())); } } @Test public void pageCountsOnlyPendingAndActiveTemplateFormulaBindings() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("引用统计", "1")); fixture.store.addFormulaBinding(binding("binding-active", "tenant-1", "template-1", formula.getId(), "version-1", "ACTIVE")); fixture.store.addFormulaBinding(binding("binding-pending", "tenant-1", "template-2", formula.getId(), "version-2", "PENDING")); fixture.store.addFormulaBinding(binding("binding-cancelled", "tenant-1", "template-3", formula.getId(), "version-3", "CANCELLED")); fixture.store.addFormulaBinding(binding("binding-removed", "tenant-1", "template-4", formula.getId(), "version-4", "REMOVED")); fixture.store.addFormulaBinding(binding("binding-foreign", "tenant-2", "template-5", formula.getId(), "version-5", "ACTIVE")); ElnFormulaVos.ListItem item = fixture.service.page(query()).getRecords().get(0); assertEquals(Integer.valueOf(2), property(item, "mubanYinyongShu")); } @Test public void pagePrefetchesVersionSummariesWithoutPerFormulaQueries() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail first = fixture.service.create(formula("批量公式一", "1")); ElnFormulaVos.Detail second = fixture.service.create(formula("批量公式二", "1")); ElnFormulaVersionEntity firstVersion = fixture.store.versions.get( first.getVersions().get(0).getId()); firstVersion.setBizStatus("PUBLISHED"); fixture.store.definitions.get(first.getId()) .setDangqianGongshiBanbenId(firstVersion.getId()); ElnFormulaVersionEntity secondVersion = fixture.store.versions.get( second.getVersions().get(0).getId()); fixture.store.definitions.get(second.getId()) .setDangqianGongshiBanbenId(secondVersion.getId()); ElnFormulaDefEntity empty = new ElnFormulaDefEntity(); empty.setId("formula-empty"); empty.setFTenantId("tenant-1"); empty.setGongshiBianma("formula_empty"); empty.setGongshiMingcheng("无版本公式"); empty.setBizEnabled("enabled"); fixture.store.definitions.put(empty.getId(), empty); ElnFormulaVersionEntity foreign = new ElnFormulaVersionEntity(); foreign.setId("foreign-version"); foreign.setFTenantId("tenant-2"); foreign.setGongshiDingyiId(first.getId()); foreign.setBanbenHao(99); foreign.setBizStatus("PUBLISHED"); fixture.store.versions.put(foreign.getId(), foreign); fixture.store.listVersionsCalls = 0; List records = fixture.service.page(query()).getRecords(); Map byId = new LinkedHashMap<>(); for (ElnFormulaVos.ListItem item : records) { byId.put(item.getId(), item); } assertEquals(0, fixture.store.listVersionsCalls); assertEquals(1, fixture.store.batchVersionCalls); assertEquals("tenant-1", fixture.store.batchVersionTenant); assertEquals(new LinkedHashSet<>(Arrays.asList( first.getId(), second.getId(), empty.getId())), fixture.store.batchVersionDefinitionIds); assertEquals(Integer.valueOf(1), byId.get(first.getId()).getDangqianBanbenHao()); assertEquals(Integer.valueOf(1), byId.get(first.getId()).getZuixinFabuBanbenHao()); assertEquals(Integer.valueOf(1), byId.get(second.getId()).getDangqianBanbenHao()); assertNull(byId.get(second.getId()).getZuixinFabuBanbenHao()); assertNull(byId.get(empty.getId()).getDangqianBanbenHao()); assertNull(byId.get(empty.getId()).getZuixinFabuBanbenHao()); } @Test public void detailReturnsStableTemplateReferenceSummariesWithMetadata() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("引用摘要", "1")); fixture.store.addTemplate(template("template-b", "tenant-1", "模板-B", "PUBLISHED")); fixture.store.addTemplate(template("template-a", "tenant-1", "模板-A", "DRAFT")); fixture.store.addFormulaBinding(binding("binding-b", "tenant-1", "template-b", formula.getId(), "version-2", "PENDING")); fixture.store.addFormulaBinding(binding("binding-a", "tenant-1", "template-a", formula.getId(), "version-1", "ACTIVE")); ElnFormulaVos.Detail detail = fixture.service.detail(formula.getId()); List references = (List) property(detail, "templateReferences"); assertEquals(2, references.size()); assertEquals("binding-a", property(references.get(0), "bindingId")); assertEquals("template-a", property(references.get(0), "templateId")); assertEquals("模板-A", property(references.get(0), "templateName")); assertEquals("DRAFT", property(references.get(0), "templateStatus")); assertEquals("ACTIVE", property(references.get(0), "bindingStatus")); assertEquals("version-1", property(references.get(0), "formulaVersionId")); assertEquals("template-b", property(references.get(1), "templateId")); } @Test public void detailReturnsReferenceCountAndAtMostFivePreviews() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("大量引用", "1")); for (int index = 7; index >= 1; index--) { String templateId = "template-" + index; fixture.store.addTemplate(template(templateId, "tenant-1", "模板-" + index, "PUBLISHED")); fixture.store.addFormulaBinding(binding("binding-" + index, "tenant-1", templateId, formula.getId(), "version-" + index, "ACTIVE")); } ElnFormulaVos.Detail detail = fixture.service.detail(formula.getId()); List references = (List) property(detail, "templateReferences"); assertEquals(Integer.valueOf(7), property(detail, "mubanYinyongShu")); assertEquals(5, references.size()); assertEquals("template-1", property(references.get(0), "templateId")); assertEquals("template-5", property(references.get(4), "templateId")); } @Test public void pagesLiveTemplateReferencesWithStableOrderAndTenantIsolation() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("分页引用", "1")); for (int index = 7; index >= 1; index--) { String templateId = "template-" + index; fixture.store.addTemplate(template(templateId, "tenant-1", "模板-" + index, "PUBLISHED")); fixture.store.addFormulaBinding(binding("binding-" + index, "tenant-1", templateId, formula.getId(), "version-" + index, "ACTIVE")); } fixture.store.addFormulaBinding(binding("binding-foreign", "tenant-2", "template-0", formula.getId(), "version-0", "ACTIVE")); fixture.store.addFormulaBinding(binding("binding-cancelled", "tenant-1", "template-8", formula.getId(), "version-8", "CANCELLED")); ElnFormulaForms.ReferenceQuery query = new ElnFormulaForms.ReferenceQuery(); query.setCurrentPage(2); query.setPageSize(3); IPage page = fixture.service.pageTemplateReferences(formula.getId(), query); assertEquals(7, page.getTotal()); assertEquals(3, page.getRecords().size()); assertEquals("template-4", page.getRecords().get(0).getTemplateId()); assertEquals("template-6", page.getRecords().get(2).getTemplateId()); } @Test public void usesBindingIdAsTheFinalStableReferenceOrder() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("同键引用", "1")); fixture.store.addTemplate(template("template-1", "tenant-1", "模板-1", "PUBLISHED")); fixture.store.addFormulaBinding(binding("binding-z", "tenant-1", "template-1", formula.getId(), "version-1", "ACTIVE")); fixture.store.addFormulaBinding(binding("binding-a", "tenant-1", "template-1", formula.getId(), "version-1", "ACTIVE")); IPage page = fixture.service.pageTemplateReferences( formula.getId(), new ElnFormulaForms.ReferenceQuery()); assertEquals("binding-a", page.getRecords().get(0).getBindingId()); assertEquals("binding-z", page.getRecords().get(1).getBindingId()); } @Test public void filtersTemplateReferencesByTrimmedTemplateNameOrId() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("搜索引用", "1")); fixture.store.addTemplate(template("template-alpha", "tenant-1", "含量测定模板", "PUBLISHED")); fixture.store.addTemplate(template("template-beta", "tenant-1", "有关物质模板", "DRAFT")); fixture.store.addFormulaBinding(binding("binding-alpha", "tenant-1", "template-alpha", formula.getId(), "version-1", "ACTIVE")); fixture.store.addFormulaBinding(binding("binding-beta", "tenant-1", "template-beta", formula.getId(), "version-2", "PENDING")); ElnFormulaForms.ReferenceQuery byName = new ElnFormulaForms.ReferenceQuery(); byName.setKeyword(" 含量测定 "); ElnFormulaForms.ReferenceQuery byId = new ElnFormulaForms.ReferenceQuery(); byId.setKeyword(" beta "); IPage namePage = fixture.service.pageTemplateReferences(formula.getId(), byName); IPage idPage = fixture.service.pageTemplateReferences(formula.getId(), byId); assertEquals(1, namePage.getTotal()); assertEquals("template-alpha", namePage.getRecords().get(0).getTemplateId()); assertEquals(1, idPage.getTotal()); assertEquals("template-beta", idPage.getRecords().get(0).getTemplateId()); } @Test public void detailKeepsLiveReferenceWhenTemplateRecordIsMissingWithoutCrossTenantLeak() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail formula = fixture.service.create(formula("缺失模板", "1")); fixture.store.addFormulaBinding(binding("binding-missing", "tenant-1", "missing-template", formula.getId(), "version-1", "ACTIVE")); fixture.store.addFormulaBinding(binding("binding-foreign", "tenant-2", "foreign-template", formula.getId(), "version-2", "PENDING")); List references = (List) property(fixture.service.detail(formula.getId()), "templateReferences"); assertEquals(1, references.size()); assertEquals("missing-template", property(references.get(0), "templateId")); assertNull(property(references.get(0), "templateName")); assertNull(property(references.get(0), "templateStatus")); } @Test public void optimisticUpdateCountZeroFailsAndSuccessAdvancesEntityVersion() { expectDataException(() -> ElnFormulaCatalogServiceImpl.nextVersionAfterUpdate( 0, 7, "版本冲突")); assertEquals(8, ElnFormulaCatalogServiceImpl.nextVersionAfterUpdate( 1, 7, "版本冲突")); } @Test public void disableBlocksVersionAndPublishThenEnableRestoresLifecycle() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("停用公式", "1")); String v1 = created.getVersions().get(0).getId(); fixture.service.preRelease(v1); fixture.service.disable(created.getId(), "暂时停用"); expectDataException(() -> fixture.service.publish(v1)); expectDataException(() -> fixture.service.createVersion(created.getId(), copyOf(v1))); assertEquals("disabled", fixture.service.detail(created.getId()).getBizEnabled()); fixture.service.enable(created.getId(), "恢复使用"); fixture.service.publish(v1); assertEquals("enabled", fixture.service.detail(created.getId()).getBizEnabled()); } @Test public void exposesTenantScopedAvailabilityForTemplateBindingGate() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("引用门禁", "1")); assertTrue(fixture.service.isEnabled("tenant-1", created.getId())); assertFalse(fixture.service.isEnabled("other-tenant", created.getId())); fixture.service.disable(created.getId(), "停用"); assertFalse(fixture.service.isEnabled("tenant-1", created.getId())); } @Test public void emitsEveryFixedLifecycleAuditAction() { Fixture fixture = new Fixture(); ElnFormulaVos.Detail created = fixture.service.create(formula("审计公式", "1")); fixture.service.updateDefinition(created.getId(), definition("审计公式二")); String v1 = created.getVersions().get(0).getId(); fixture.service.updateDraft(v1, version("2")); fixture.service.preRelease(v1); fixture.service.publish(v1); ElnFormulaVos.Version v2 = fixture.service.createVersion(created.getId(), copyOf(v1)); fixture.service.cancel(v2.getId(), "废弃 V2"); fixture.service.disable(created.getId(), "停用"); fixture.service.enable(created.getId(), "启用"); fixture.service.switchCurrent(created.getId(), v1, "切回 V1"); assertEquals(Arrays.asList("ELN_FORMULA_CREATE", "ELN_FORMULA_VERSION_CREATE", "ELN_FORMULA_UPDATE", "ELN_FORMULA_VERSION_UPDATE", "ELN_FORMULA_VERSION_PRE_RELEASE", "ELN_FORMULA_VERSION_PUBLISH", "ELN_FORMULA_VERSION_CREATE", "ELN_FORMULA_VERSION_CANCEL", "ELN_FORMULA_DISABLE", "ELN_FORMULA_ENABLE", "ELN_FORMULA_SWITCH_CURRENT"), fixture.auditActions()); for (AuditEventDTO audit : fixture.audits) { assertTrue("eln_formula".equals(audit.getBizType()) || "eln_formula_version".equals(audit.getBizType())); } } private static ElnFormulaForms.Create formula(String name, String expression, ElnFormulaForms.Parameter... parameters) { ElnFormulaForms.Create form = new ElnFormulaForms.Create(); form.setGongshiMingcheng(name); form.setYewuYongtu("测试用途"); form.setBiaodashi(expression); form.setJieguoLeixing("DECIMAL"); form.setParameters(new ArrayList<>(Arrays.asList(parameters))); return form; } private static ElnFormulaForms.Parameter parameter(String name, String type) { ElnFormulaForms.Parameter form = new ElnFormulaForms.Parameter(); form.setCanshuMingcheng(name); form.setShujuLeixing(type); form.setShifouBitian(1); return form; } private static ElnFormulaForms.Parameter parameterFrom(ElnFormulaVos.Parameter source) { ElnFormulaForms.Parameter form = parameter(source.getCanshuMingcheng(), source.getShujuLeixing()); form.setId(source.getId()); form.setDanwei(source.getDanwei()); form.setPaixu(source.getPaixu()); return form; } private static ElnFormulaForms.CreateVersion copyOf(String sourceVersionId) { ElnFormulaForms.CreateVersion form = new ElnFormulaForms.CreateVersion(); form.setSourceVersionId(sourceVersionId); return form; } private static ElnFormulaForms.UpdateVersion version(String expression, ElnFormulaForms.Parameter... parameters) { ElnFormulaForms.UpdateVersion form = new ElnFormulaForms.UpdateVersion(); form.setBiaodashi(expression); form.setJieguoLeixing("DECIMAL"); form.setParameters(new ArrayList<>(Arrays.asList(parameters))); return form; } private static ElnFormulaForms.UpdateDefinition definition(String name) { ElnFormulaForms.UpdateDefinition form = new ElnFormulaForms.UpdateDefinition(); form.setGongshiMingcheng(name); form.setYewuYongtu("更新用途"); return form; } private static String keptCode(ElnFormulaVos.Version version, int index) { return version.getParameters().get(index).getCanshuBianma(); } private static void expectDataException(Runnable action) { try { action.run(); fail("预期抛出 DataException"); } catch (DataException expected) { assertFalse(expected.getMessage().trim().isEmpty()); } } private static void expectDataException(String message, Runnable action) { try { action.run(); fail("预期抛出 DataException"); } catch (DataException expected) { assertTrue(expected.getMessage().contains(message)); } } private static String repeat(String value, int count) { StringBuilder result = new StringBuilder(); for (int i = 0; i < count; i++) { result.append(value); } return result.toString(); } private static ElnFormulaForms.Query query() { ElnFormulaForms.Query query = new ElnFormulaForms.Query(); query.setCurrentPage(1); query.setPageSize(20); return query; } private static ElnTemplateFormulaBindingEntity binding(String id, String tenantId, String templateId, String formulaId, String formulaVersionId, String status) { ElnTemplateFormulaBindingEntity result = new ElnTemplateFormulaBindingEntity(); result.setId(id); result.setFTenantId(tenantId); result.setMubanId(templateId); result.setGongshiDingyiId(formulaId); result.setGongshiBanbenId(formulaVersionId); result.setBangdingZhuangtai(status); return result; } private static ElnTemplateVersionEntity template(String id, String tenantId, String name, String status) { ElnTemplateVersionEntity result = new ElnTemplateVersionEntity(); result.setId(id); result.setFTenantId(tenantId); result.setJianyanXiangmu(name); result.setBizStatus(status); return result; } private static Object property(Object source, String name) { try { Field field = source.getClass().getDeclaredField(name); field.setAccessible(true); return field.get(source); } catch (ReflectiveOperationException exception) { fail("缺少属性: " + name); return null; } } private static final class Fixture { private final MemoryStore store = new MemoryStore(); private final List audits = new ArrayList<>(); private final AtomicInteger ids = new AtomicInteger(); private boolean hasDraftReference; private final ElnFormulaCatalogServiceImpl service; private Fixture(boolean checkerAvailable, String... suffixes) { Supplier suffixSupplier = suffixSupplier(suffixes); service = new ElnFormulaCatalogServiceImpl(store, new FormulaValidationServiceImpl(), checkerAvailable ? (tenantId, versionId) -> hasDraftReference : null, () -> new ElnFormulaCatalogServiceImpl.UserContext("tenant-1", "user-1"), () -> "id-" + ids.incrementAndGet(), new ElnFieldCodeGenerator(suffixSupplier), audits::add, () -> true); } private Fixture(String... suffixes) { this(true, suffixes); } private Fixture() { this(true, new String[0]); } private List auditActions() { List actions = new ArrayList<>(); for (AuditEventDTO audit : audits) { actions.add(audit.getActionCode()); } return actions; } private static Supplier suffixSupplier(String[] configured) { AtomicInteger sequence = new AtomicInteger(); return () -> { int index = sequence.getAndIncrement(); return index < configured.length ? configured[index] : String.format("s%07d", index + 1); }; } } private static final class MemoryStore implements ElnFormulaCatalogServiceImpl.FormulaCatalogStore { private final Map definitions = new LinkedHashMap<>(); private final Map versions = new LinkedHashMap<>(); private final Map slots = new LinkedHashMap<>(); private final Map formulaBindings = new LinkedHashMap<>(); private final Map templates = new LinkedHashMap<>(); private int listVersionsCalls; private int batchVersionCalls; private String batchVersionTenant; private Set batchVersionDefinitionIds = new LinkedHashSet<>(); private void addFormulaBinding(ElnTemplateFormulaBindingEntity binding) { formulaBindings.put(binding.getId(), binding); } private void addTemplate(ElnTemplateVersionEntity template) { templates.put(template.getId(), template); } @Override public void insertDefinition(ElnFormulaDefEntity entity) { for (ElnFormulaDefEntity existing : definitions.values()) { if (existing.getFTenantId().equals(entity.getFTenantId()) && existing.getGongshiBianma().equals(entity.getGongshiBianma())) { throw new DuplicateKeyException("duplicate formula code"); } } definitions.put(entity.getId(), entity); } @Override public void insertVersion(ElnFormulaVersionEntity entity) { versions.put(entity.getId(), entity); } @Override public void insertSlot(ElnFormulaParameterSlotEntity entity) { for (ElnFormulaParameterSlotEntity existing : slots.values()) { if (existing.getFTenantId().equals(entity.getFTenantId()) && existing.getGongshiBanbenId().equals(entity.getGongshiBanbenId()) && existing.getCanshuBianma().equals(entity.getCanshuBianma())) { throw new DuplicateKeyException("duplicate slot code"); } } slots.put(entity.getId(), entity); } @Override public ElnFormulaDefEntity findDefinition(String tenantId, String id, boolean forUpdate) { ElnFormulaDefEntity value = definitions.get(id); return value != null && tenantId.equals(value.getFTenantId()) ? value : null; } @Override public ElnFormulaVersionEntity findVersion(String tenantId, String id, boolean forUpdate) { ElnFormulaVersionEntity value = versions.get(id); return value != null && tenantId.equals(value.getFTenantId()) ? value : null; } @Override public List listVersions(String tenantId, String definitionId) { listVersionsCalls++; List result = new ArrayList<>(); for (ElnFormulaVersionEntity value : versions.values()) { if (tenantId.equals(value.getFTenantId()) && definitionId.equals(value.getGongshiDingyiId())) { result.add(value); } } return result; } @Override public List listVersionsByDefinitionIds( String tenantId, Collection definitionIds) { batchVersionCalls++; batchVersionTenant = tenantId; batchVersionDefinitionIds = new LinkedHashSet<>(definitionIds); List result = new ArrayList<>(); for (ElnFormulaVersionEntity value : versions.values()) { if (tenantId.equals(value.getFTenantId()) && definitionIds.contains(value.getGongshiDingyiId())) { result.add(value); } } return result; } @Override public List listSlots(String tenantId, String versionId) { List result = new ArrayList<>(); for (ElnFormulaParameterSlotEntity value : slots.values()) { if (tenantId.equals(value.getFTenantId()) && versionId.equals(value.getGongshiBanbenId())) { result.add(value); } } return result; } @Override public List listLiveTemplateFormulaBindings( String tenantId, java.util.Collection definitionIds) { List result = new ArrayList<>(); for (ElnTemplateFormulaBindingEntity binding : formulaBindings.values()) { if (tenantId.equals(binding.getFTenantId()) && definitionIds.contains(binding.getGongshiDingyiId()) && ("ACTIVE".equals(binding.getBangdingZhuangtai()) || "PENDING".equals(binding.getBangdingZhuangtai()))) { result.add(binding); } } return result; } @Override public IPage pageLiveTemplateFormulaBindings( String tenantId, String definitionId, ElnFormulaForms.ReferenceQuery query) { List result = new ArrayList<>(); String keyword = query.getKeyword() == null ? null : query.getKeyword().trim(); for (ElnTemplateFormulaBindingEntity binding : formulaBindings.values()) { ElnTemplateVersionEntity template = templates.get(binding.getMubanId()); boolean matchesKeyword = keyword == null || keyword.isEmpty() || binding.getMubanId().contains(keyword) || template != null && template.getJianyanXiangmu().contains(keyword); if (tenantId.equals(binding.getFTenantId()) && definitionId.equals(binding.getGongshiDingyiId()) && ("ACTIVE".equals(binding.getBangdingZhuangtai()) || "PENDING".equals(binding.getBangdingZhuangtai())) && matchesKeyword) { result.add(binding); } } result.sort(Comparator.comparing(ElnTemplateFormulaBindingEntity::getMubanId) .thenComparing(ElnTemplateFormulaBindingEntity::getGongshiBanbenId) .thenComparing(ElnTemplateFormulaBindingEntity::getBangdingZhuangtai) .thenComparing(ElnTemplateFormulaBindingEntity::getId)); int from = Math.min((query.getCurrentPage() - 1) * query.getPageSize(), result.size()); int to = Math.min(from + query.getPageSize(), result.size()); return new Page( query.getCurrentPage(), query.getPageSize(), result.size()) .setRecords(new ArrayList<>(result.subList(from, to))); } @Override public List listTemplates( String tenantId, java.util.Collection templateIds) { List result = new ArrayList<>(); for (ElnTemplateVersionEntity template : templates.values()) { if (tenantId.equals(template.getFTenantId()) && templateIds.contains(template.getId())) { result.add(template); } } return result; } @Override public IPage page(String tenantId, ElnFormulaForms.Query query) { List result = new ArrayList<>(); for (ElnFormulaDefEntity value : definitions.values()) { if (tenantId.equals(value.getFTenantId())) { result.add(value); } } return new Page(query.getCurrentPage(), query.getPageSize()) .setRecords(result).setTotal(result.size()); } @Override public void updateDefinition(ElnFormulaDefEntity entity) { definitions.put(entity.getId(), entity); } @Override public void updateVersion(ElnFormulaVersionEntity entity) { versions.put(entity.getId(), entity); } @Override public void updateSlot(ElnFormulaParameterSlotEntity entity) { slots.put(entity.getId(), entity); } @Override public void deleteSlot(ElnFormulaParameterSlotEntity entity) { slots.remove(entity.getId()); } } }