package jnpf.elnService.formula; import jnpf.audit.model.AuditEventDTO; import jnpf.elnEntity.field.ElnFieldVersionEntity; 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.ElnTemplateBindingEntity; import jnpf.elnEntity.template.ElnTemplateVersionEntity; import jnpf.elnMapper.ElnTemplateMapper; import jnpf.elnMapper.ElnTemplateFormulaBindingMapper; import jnpf.elnService.formula.impl.ElnTemplateFormulaBindingServiceImpl; import jnpf.elnService.formula.impl.FormulaTemplateVersionCopyParticipant; import jnpf.elnService.template.TemplateAnnotationAuthorization; import jnpf.exception.DataException; import jnpf.util.JsonUtil; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import org.junit.Test; import org.springframework.dao.DataAccessResourceFailureException; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class ElnTemplateFormulaBindingServiceImplTest { @Test public void reservePersistsExactResultLockSnapshotAndReturnsItInContext() { Fixture f = new Fixture(); ElnFormulaForms.TemplateBinding form = f.request("formula-current", "snapshot-op", map("amount", "input"), "result"); form.setJieguoSuoKuaizhao(Arrays.asList(f.control("result", ""), f.control("result-ref", "contentLocked"))); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), form); ElnFormulaVos.TemplateBinding fromContext = f.service.context(f.authorization()) .getBindings().get(0); assertEquals(2, reserved.getJieguoSuoKuaizhao().size()); assertEquals("result", reserved.getJieguoSuoKuaizhao().get(0).getBindingId()); assertEquals("contentLocked", fromContext.getJieguoSuoKuaizhao().get(1).getLock()); } @Test public void versionCopyContextExposesInheritedFormulaResultLockBindingsBeforeFormulaCopyExists() { Fixture f = new Fixture(); f.store.template.setLaiyuanMubanId("source-template"); f.store.template.setShengbanCaozuoBianma("copy-with-formula"); ElnTemplateBindingEntity result = f.store.fieldBindings.get("result"); result.setBangdingZhuangtai("PENDING"); result.setLaiyuanBangdingId("source-result"); ElnTemplateBindingEntity reference = f.store.fieldBindings.get("result-ref"); reference.setBangdingZhuangtai("PENDING"); reference.setLaiyuanBangdingId("source-result-ref"); ElnTemplateFormulaBindingEntity sourceFormula = f.formulaBinding( "source-formula", "source-template", "ACTIVE"); sourceFormula.setJieguoBangdingId("source-result"); f.store.bindings.put(sourceFormula.getId(), sourceFormula); ElnFormulaVos.TemplateMappingContext context = f.service.context(f.authorization()); assertTrue(context.getBindings().isEmpty()); assertEquals(Arrays.asList("result", "result-ref"), context.getInheritedFormulaLockBindingIds()); } @Test public void reserveRejectsLockSnapshotOutsideFormulaResultControls() { Fixture f = new Fixture(); ElnFormulaForms.TemplateBinding form = f.request("formula-current", "snapshot-invalid", map("amount", "input"), "result"); form.setJieguoSuoKuaizhao(Arrays.asList(f.control("result", ""), f.control("input", ""))); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.reserve(f.authorization(), form)); } @Test public void reserveRejectsOperationCodeLongerThan160Characters() { Fixture f = new Fixture(); expectFailure("长度", () -> f.service.reserve(f.authorization(), f.request("formula-current", operationOfLength(161), map("amount", "input"), "result"))); } @Test public void activateRejectsLongOperationCodeWithoutChangingState() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "activate-length", map("amount", "input"), "result")); ElnFormulaForms.ActivateFormulaBinding activate = new ElnFormulaForms.ActivateFormulaBinding(); activate.setOperationCode(operationOfLength(161)); activate.setExpectedVersion(1); activate.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("长度", () -> f.service.activate(f.authorization(), reserved.getId(), activate)); assertEquals("PENDING", f.store.binding(reserved.getId()).getBangdingZhuangtai()); } @Test public void upgradeCheckRejectsLongTargetOperationCode() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding source = f.activeMapping("upgrade-length-source"); ElnFormulaForms.TemplateBinding target = f.request("formula-current", operationOfLength(161), map("amount", "input"), "result"); expectFailure("长度", () -> f.service.checkUpgrade( f.authorization(), source.getId(), target)); } @Test public void versionCopyConfirmRejectsLongUpgradeOperationCode() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("version-copy-length"); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode(operationOfLength(161)); expectFailure("长度", () -> f.service.confirmVersionCopy(f.authorization(), confirm)); } @Test public void formulaBindingInsertIgnoresOnlyExactOperationConflictWithAllColumns() { Method insertMethod = null; for (Method method : ElnTemplateFormulaBindingMapper.class.getDeclaredMethods()) { if (method.getAnnotation(Insert.class) != null) { insertMethod = method; break; } } assertTrue(insertMethod != null); assertEquals(int.class, insertMethod.getReturnType()); String sql = String.join(" ", insertMethod.getAnnotation(Insert.class).value()) .toLowerCase().replaceAll("\\s+", " ").trim(); assertTrue(sql.contains("insert into eln_template_formula_binding (muban_id, " + "gongshi_dingyi_id, gongshi_banben_id, canshu_yingshe, " + "jieguo_bangding_id, bangding_zhuangtai, tuopu_xuhao, caozuo_bianma, " + "jieguo_suo_kuaizhao, " + "laiyuan_gongshi_bangding_id, daoqi_shijian, zhuangtai_yuanyin, f_id, " + "f_tenant_id, f_version, f_creator_user_id, f_creator_time, " + "f_last_modify_user_id, f_last_modify_time, f_delete_user_id, " + "f_delete_time, f_delete_mark) values (")); assertTrue(sql.contains("on conflict (f_tenant_id, muban_id, caozuo_bianma) " + "do nothing")); } @Test public void templateActivationLockUsesMyBatisPlusRowLockInsteadOfRawSelect() throws Exception { Method method = ElnTemplateMapper.class.getMethod( "lockTemplateForUpdate", String.class, String.class); assertTrue(method.isDefault()); assertTrue(method.getAnnotation(Select.class) == null); } @Test public void formulaStatusUpdateUsesJsonbTypeHandlers() throws Exception { Method method = ElnTemplateFormulaBindingMapper.class.getMethod( "updateStatus", ElnTemplateFormulaBindingEntity.class, int.class, String.class, String.class); Update update = method.getAnnotation(Update.class); assertTrue(update != null); String sql = String.join(" ", update.value()).toLowerCase() .replaceAll("\\s+", " ").trim(); assertTrue(sql.contains("canshu_yingshe = #{entity.canshuyingshe,jdbctype=other," + "typehandler=jnpf.elnentity.formula.elntemplateformulabindingentity$" + "parametermappingtypehandler}")); assertTrue(sql.contains("f_tenant_id = #{entity.ftenantid}")); assertTrue(sql.contains("f_version = #{expectedversion}")); assertTrue(sql.contains("bangding_zhuangtai = #{entity.bangdingzhuangtai}")); } @Test public void reservesIdempotentlyAndActivatesOnlyWithReadOnlyResultControls() { Fixture f = new Fixture(); ElnFormulaForms.TemplateBinding request = f.request("formula-current", "op-1", map("amount", "input"), "result"); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), request); ElnFormulaVos.TemplateBinding retried = f.service.reserve(f.authorization(), request); assertEquals(reserved.getId(), retried.getId()); assertEquals("PENDING", reserved.getStatus()); assertTrue(f.store.binding(reserved.getId()).getDaoqiShijian().after(new Date(0))); ElnFormulaForms.ActivateFormulaBinding activate = new ElnFormulaForms.ActivateFormulaBinding(); activate.setOperationCode("op-1"); activate.setExpectedVersion(1); activate.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); f.service.activate(f.authorization(), reserved.getId(), activate); f.service.activate(f.authorization(), reserved.getId(), activate); assertEquals("ACTIVE", f.store.binding(reserved.getId()).getBangdingZhuangtai()); assertEquals(Integer.valueOf(0), f.store.binding(reserved.getId()).getTuopuXuhao()); assertTrue(f.hasAudit("ELN_TEMPLATE_FORMULA_RESERVE")); assertTrue(f.hasAudit("ELN_TEMPLATE_FORMULA_ACTIVATE")); assertEquals(1, f.auditCount("ELN_TEMPLATE_FORMULA_ACTIVATE")); assertFalse(f.audits.get(0).getDataSnapshot().toLowerCase().contains("ticket")); } @Test public void concurrentActivationsOnSameTemplateSerializeAndRejectCycle() throws Exception { ConcurrentGraphStore store = new ConcurrentGraphStore(); Fixture f = new Fixture(store); f.addField("result-a", "result-a-version", "DECIMAL", manualBinding()); f.addField("result-b", "result-b-version", "DECIMAL", manualBinding()); ElnTemplateFormulaBindingEntity first = f.formulaBinding( "formula-a", "template-1", "PENDING"); first.setCanshuYingshe(map("amount", "result-b")); first.setJieguoBangdingId("result-a"); first.setCaozuoBianma("activate-a"); ElnTemplateFormulaBindingEntity second = f.formulaBinding( "formula-b", "template-1", "PENDING"); second.setCanshuYingshe(map("amount", "result-a")); second.setJieguoBangdingId("result-b"); second.setCaozuoBianma("activate-b"); store.bindings.put(first.getId(), first); store.bindings.put(second.getId(), second); ExecutorService executor = Executors.newFixedThreadPool(2); try { Future firstResult = executor.submit(activation( f, store, first.getId(), "activate-a", "result-a")); Future secondResult = executor.submit(activation( f, store, second.getId(), "activate-b", "result-b")); List results = Arrays.asList( firstResult.get(5, TimeUnit.SECONDS), secondResult.get(5, TimeUnit.SECONDS)); assertEquals(1, Collections.frequency(results, "SUCCESS")); assertTrue(results.toString(), results.stream() .anyMatch(value -> value.contains("循环"))); assertEquals(2, store.lockCalls.get()); assertEquals(1, store.activeCount()); } finally { executor.shutdownNow(); } } @Test public void concurrentUpgradesOnSameTemplateSerializeAndRejectCycle() throws Exception { ConcurrentGraphStore store = new ConcurrentGraphStore(); Fixture f = new Fixture(store); f.addField("result-a", "upgrade-result-a-version", "DECIMAL", manualBinding()); f.addField("result-b", "upgrade-result-b-version", "DECIMAL", manualBinding()); ElnTemplateFormulaBindingEntity first = f.formulaBinding( "upgrade-a", "template-1", "ACTIVE"); first.setJieguoBangdingId("result-a"); first.setCaozuoBianma("original-a"); ElnTemplateFormulaBindingEntity second = f.formulaBinding( "upgrade-b", "template-1", "ACTIVE"); second.setJieguoBangdingId("result-b"); second.setCaozuoBianma("original-b"); store.bindings.put(first.getId(), first); store.bindings.put(second.getId(), second); ExecutorService executor = Executors.newFixedThreadPool(2); try { Future firstResult = executor.submit(upgrade( f, store, first.getId(), "upgrade-op-a", "result-b", "result-a")); Future secondResult = executor.submit(upgrade( f, store, second.getId(), "upgrade-op-b", "result-a", "result-b")); List results = Arrays.asList( firstResult.get(5, TimeUnit.SECONDS), secondResult.get(5, TimeUnit.SECONDS)); assertEquals(1, Collections.frequency(results, "SUCCESS")); assertTrue(results.toString(), results.stream() .anyMatch(value -> value.contains("循环"))); assertEquals(2, store.lockCalls.get()); assertTrue(new FormulaDependencyGraph().validate(store.committedBindings()).isValid()); } finally { executor.shutdownNow(); } } @Test public void versionCopyConfirmationKeepsStateAndGraphAccessInsideTemplateLock() { LockCheckingStore store = new LockCheckingStore(); Fixture f = new Fixture(store); store.template.setShengbanCaozuoBianma("copy-lock"); ElnTemplateFormulaBindingEntity copied = f.formulaBinding( "copied-formula-lock", "template-1", "PENDING"); copied.setLaiyuanGongshiBangdingId("source-formula-lock"); copied.setCaozuoBianma("copy-lock:source-formula-lock"); copied.setDaoqiShijian(null); store.bindings.put(copied.getId(), copied); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("copy-lock"); confirm.setExpectedVersions(Collections.singletonMap(copied.getId(), 1)); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); f.service.confirmVersionCopy(f.authorization(), confirm); assertEquals(1, store.lockCalls.get()); assertEquals("ACTIVE", store.bindings.get(copied.getId()).getBangdingZhuangtai()); } @Test public void activateRejectsControlRoleMismatchWithoutChangingPendingState() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "role-activate", map("amount", "input"), "result")); ElnFormulaForms.ActivateFormulaBinding activate = new ElnFormulaForms.ActivateFormulaBinding(); activate.setOperationCode("role-activate"); activate.setExpectedVersion(1); ElnFormulaForms.ControlSnapshot result = f.control("result", "sdtContentLocked"); result.setControlRole("REFERENCE"); activate.setControls(Arrays.asList(result, f.control("result-ref", "sdtContentLocked"))); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.activate( f.authorization(), reserved.getId(), activate)); assertEquals("PENDING", f.store.binding(reserved.getId()).getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_ACTIVATE")); } @Test public void activateRejectsLegacySnapshotWithoutControlRole() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "legacy-controls", map("amount", "input"), "result")); ElnFormulaForms.ActivateFormulaBinding activate = new ElnFormulaForms.ActivateFormulaBinding(); activate.setOperationCode("legacy-controls"); activate.setExpectedVersion(1); ElnFormulaForms.ControlSnapshot result = f.control("result", "sdtContentLocked"); ElnFormulaForms.ControlSnapshot reference = f.control("result-ref", "sdtContentLocked"); result.setControlRole(null); reference.setControlRole(null); activate.setControls(Arrays.asList(result, reference)); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.activate( f.authorization(), reserved.getId(), activate)); assertEquals("PENDING", f.store.binding(reserved.getId()).getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_ACTIVATE")); } @Test public void reserveReturnsConcurrentWinnerWithSameRequestWithoutAudit() { Fixture f = new Fixture(); ElnTemplateFormulaBindingEntity winner = f.formulaBinding( "concurrent-formula", "template-1", "PENDING"); winner.setCaozuoBianma("concurrent-op"); winner.setDaoqiShijian(new Date(System.currentTimeMillis() + 60_000L)); f.store.concurrentInsert = winner; ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", " concurrent-op ", map("amount", "input"), "result")); assertEquals("concurrent-formula", reserved.getId()); assertEquals("PENDING", reserved.getStatus()); assertEquals(1, f.store.bindings.size()); assertSame(winner, f.store.binding("concurrent-formula")); assertEquals(2, f.store.findByOperationCalls); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_RESERVE")); } @Test public void reserveRejectsConcurrentWinnerWithDifferentRequestWithoutAudit() { Fixture f = new Fixture(); ElnTemplateFormulaBindingEntity winner = f.formulaBinding( "concurrent-formula", "template-1", "PENDING"); winner.setCaozuoBianma("concurrent-mismatch"); winner.setJieguoBangdingId("other-result"); f.store.concurrentInsert = winner; expectFailure("操作码", () -> f.service.reserve(f.authorization(), f.request("formula-current", "concurrent-mismatch", map("amount", "input"), "result"))); assertEquals(1, f.store.bindings.size()); assertSame(winner, f.store.binding("concurrent-formula")); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_RESERVE")); } @Test public void reservePropagatesNonUniqueDatabaseFailureUnchanged() { Fixture f = new Fixture(); DataAccessResourceFailureException failure = new DataAccessResourceFailureException("database unavailable"); f.store.insertFailure = failure; try { f.service.reserve(f.authorization(), f.request("formula-current", "db-failure", map("amount", "input"), "result")); fail("数据库异常必须原样冒泡"); } catch (DataAccessResourceFailureException ex) { assertSame(failure, ex); } assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_RESERVE")); } @Test public void rejectsIncompleteSlotsWrongTypesInvalidResultAndDisabledFormula() { Fixture f = new Fixture(); expectFailure("参数", () -> f.service.reserve(f.authorization(), f.request("formula-current", "missing", Collections.emptyMap(), "result"))); f.fieldVersions.get("input-version").setShujuLeixing("TEXT"); expectFailure("类型", () -> f.service.reserve(f.authorization(), f.request("formula-current", "wrong-type", map("amount", "input"), "result"))); f.fieldVersions.get("input-version").setShujuLeixing("DECIMAL"); f.fieldVersions.get("result-version").setPiciMoshi("BATCH_SHARED"); expectFailure("结果", () -> f.service.reserve(f.authorization(), f.request("formula-current", "wrong-result", map("amount", "input"), "result"))); f.fieldVersions.get("result-version").setPiciMoshi("RECORD_PRIVATE"); f.definition.setBizEnabled("disabled"); expectFailure("停用", () -> f.service.reserve(f.authorization(), f.request("formula-current", "disabled", map("amount", "input"), "result"))); } @Test public void rejectsMutualExclusionGroupMemberAsFormulaResult() { Fixture f = new Fixture(); f.store.fieldBindings.get("result").setCheckboxGroupCode("result_choice"); expectFailure("选择组", () -> f.service.reserve(f.authorization(), f.request("formula-current", "grouped-result", map("amount", "input"), "result"))); } @Test public void cancelRetryIsIdempotentAndAuditedOnce() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "cancel-op", map("amount", "input"), "result")); ElnFormulaForms.CancelFormulaBinding cancel = new ElnFormulaForms.CancelFormulaBinding(); cancel.setOperationCode("cancel-op"); cancel.setExpectedVersion(1); cancel.setReason("插件失败"); f.service.cancel(f.authorization(), reserved.getId(), cancel); f.service.cancel(f.authorization(), reserved.getId(), cancel); assertEquals("CANCELLED", f.store.binding(reserved.getId()).getBangdingZhuangtai()); assertEquals(1, f.auditCount("ELN_TEMPLATE_FORMULA_CANCEL")); } @Test public void rejectsHistoricalPublishedForManualReserveButAllowsPreRelease() { Fixture f = new Fixture(); f.addFormulaVersion("formula-old", "PUBLISHED", "DECIMAL"); expectFailure("历史", () -> f.service.reserve(f.authorization(), f.request("formula-old", "old", map("amount", "input"), "result"))); f.addFormulaVersion("formula-pre", "PRE_RELEASE", "DECIMAL"); assertEquals("formula-pre", f.service.reserve(f.authorization(), f.request("formula-pre", "pre", map("amount", "input"), "result")).getFormulaVersionId()); } @Test public void rejectsCollectionAndSelfDependencyAndCrossTemplateId() { Fixture f = new Fixture(); f.versions.get("formula-current").setJieguoLeixing("DECIMAL_LIST"); expectFailure("UNSUPPORTED_COLLECTION_BINDING", () -> f.service.reserve(f.authorization(), f.request("formula-current", "list", map("amount", "input"), "result"))); f.versions.get("formula-current").setJieguoLeixing("DECIMAL"); expectFailure("自身", () -> f.service.reserve(f.authorization(), f.request("formula-current", "self", map("amount", "result"), "result"))); ElnTemplateFormulaBindingEntity foreign = f.formulaBinding("foreign", "template-2", "PENDING"); f.store.bindings.put(foreign.getId(), foreign); ElnFormulaForms.CancelFormulaBinding cancel = new ElnFormulaForms.CancelFormulaBinding(); cancel.setOperationCode("foreign-op"); cancel.setExpectedVersion(1); cancel.setReason("cancel"); expectFailure("模板", () -> f.service.cancel(f.authorization(), "foreign", cancel)); } @Test public void acceptsDecimalListMappedToRepeatColumnWithPrimaryResult() { Fixture f = new Fixture(); f.store.slots.get("formula-current").get(0).setShujuLeixing("DECIMAL_LIST"); f.addRepeatField("repeat-input", "repeat-input-version", "group-a"); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "aggregate", map("amount", "repeat-input"), "result")); assertEquals("PENDING", reserved.getStatus()); } @Test public void acceptsScalarRepeatFormulaWithinOneGroupAndRejectsCrossGroup() { Fixture valid = new Fixture(); valid.addRepeatField("repeat-input", "repeat-input-version", "group-a"); valid.addRepeatField("repeat-result", "repeat-result-version", "group-a"); ElnFormulaForms.TemplateBinding request = valid.request("formula-current", "row-formula", map("amount", "repeat-input"), "repeat-result"); request.setJieguoSuoKuaizhao(Collections.singletonList( valid.control("repeat-result", ""))); assertEquals("PENDING", valid.service.reserve( valid.authorization(), request).getStatus()); Fixture invalid = new Fixture(); invalid.addRepeatField("repeat-input", "repeat-input-version", "group-a"); invalid.addRepeatField("repeat-result", "repeat-result-version", "group-b"); ElnFormulaForms.TemplateBinding crossGroup = invalid.request("formula-current", "row-cross-group", map("amount", "repeat-input"), "repeat-result"); crossGroup.setJieguoSuoKuaizhao(Collections.singletonList( invalid.control("repeat-result", ""))); expectFailure("同一重复组", () -> invalid.service.reserve( invalid.authorization(), crossGroup)); } @Test public void versionCopyParticipantRemapsBindingsAndIsIdempotent() { CopyStore store = new CopyStore(); ElnTemplateVersionEntity target = new ElnTemplateVersionEntity(); target.setId("target"); target.setFTenantId("tenant-1"); target.setBizStatus("DRAFT"); target.setShengbanCaozuoBianma("upgrade-1"); store.target = target; ElnTemplateFormulaBindingEntity source = new ElnTemplateFormulaBindingEntity(); source.setId("source-formula"); source.setFTenantId("tenant-1"); source.setMubanId("source"); source.setGongshiDingyiId("formula-def"); source.setGongshiBanbenId("historical-published"); source.setCanshuYingshe(map("amount", "old-input")); source.setJieguoBangdingId("old-result"); source.setBangdingZhuangtai("ACTIVE"); store.source.add(source); FormulaTemplateVersionCopyParticipant participant = new FormulaTemplateVersionCopyParticipant(store, () -> "copied-formula"); Map fieldMap = new LinkedHashMap<>(); fieldMap.put("old-input", "new-input"); fieldMap.put("old-result", "new-result"); participant.afterBindingsActivated("tenant-1", "source", "target", fieldMap); participant.afterBindingsActivated("tenant-1", "source", "target", fieldMap); assertEquals(1, store.inserted.size()); ElnTemplateFormulaBindingEntity copied = store.inserted.get(0); assertEquals(map("amount", "new-input"), copied.getCanshuYingshe()); assertEquals("new-result", copied.getJieguoBangdingId()); assertEquals("historical-published", copied.getGongshiBanbenId()); assertEquals("upgrade-1:source-formula", copied.getCaozuoBianma()); assertEquals("PENDING", copied.getBangdingZhuangtai()); assertEquals(null, copied.getDaoqiShijian()); } @Test public void versionCopyAcceptsConcurrentWinnerWithExactCopySemantics() { CopyStore store = new CopyStore(); Map fieldMap = store.prepareCopy(); ElnTemplateFormulaBindingEntity winner = store.concurrentWinner(fieldMap); store.concurrentInsert = winner; new FormulaTemplateVersionCopyParticipant(store, () -> "losing-copy") .afterBindingsActivated("tenant-1", "source", "target", fieldMap); assertEquals(1, store.inserted.size()); assertSame(winner, store.inserted.get(0)); assertEquals("PENDING", winner.getBangdingZhuangtai()); assertEquals(null, winner.getDaoqiShijian()); assertEquals(2, store.findByOperationCalls); } @Test public void versionCopyRejectsConcurrentWinnerWithDifferentCopySemantics() { CopyStore store = new CopyStore(); Map fieldMap = store.prepareCopy(); ElnTemplateFormulaBindingEntity winner = store.concurrentWinner(fieldMap); winner.setCanshuYingshe(map("amount", "wrong-input")); store.concurrentInsert = winner; expectFailure("操作码", () -> new FormulaTemplateVersionCopyParticipant( store, () -> "losing-copy").afterBindingsActivated( "tenant-1", "source", "target", fieldMap)); assertEquals(1, store.inserted.size()); assertSame(winner, store.inserted.get(0)); } @Test public void versionCopyPropagatesNonUniqueDatabaseFailureUnchanged() { CopyStore store = new CopyStore(); Map fieldMap = store.prepareCopy(); DataAccessResourceFailureException failure = new DataAccessResourceFailureException("database unavailable"); store.insertFailure = failure; try { new FormulaTemplateVersionCopyParticipant(store, () -> "copy-failure") .afterBindingsActivated("tenant-1", "source", "target", fieldMap); fail("数据库异常必须原样冒泡"); } catch (DataAccessResourceFailureException ex) { assertSame(failure, ex); } assertTrue(store.inserted.isEmpty()); } @Test public void versionCopyParticipantAllowsPreReleaseFormulaForDraftTarget() { CopyStore store = new CopyStore(); Map fieldMap = store.prepareCopy(); store.formulaVersion.setBizStatus("PRE_RELEASE"); new FormulaTemplateVersionCopyParticipant(store, () -> "copied-formula") .afterBindingsActivated("tenant-1", "source", "target", fieldMap); assertEquals(1, store.inserted.size()); assertEquals("historical-published", store.inserted.get(0).getGongshiBanbenId()); assertEquals("PENDING", store.inserted.get(0).getBangdingZhuangtai()); } @Test public void versionCopyParticipantRejectsDraftFormulaVersion() { CopyStore store = new CopyStore(); ElnTemplateVersionEntity target = new ElnTemplateVersionEntity(); target.setId("target"); target.setFTenantId("tenant-1"); target.setBizStatus("DRAFT"); target.setShengbanCaozuoBianma("upgrade-1"); store.target = target; store.formulaVersion.setBizStatus("DRAFT"); ElnTemplateFormulaBindingEntity source = new ElnTemplateFormulaBindingEntity(); source.setId("source-formula"); source.setFTenantId("tenant-1"); source.setMubanId("source"); source.setGongshiDingyiId("formula-def"); source.setGongshiBanbenId("historical-published"); source.setCanshuYingshe(map("amount", "old-input")); source.setJieguoBangdingId("old-result"); source.setBangdingZhuangtai("ACTIVE"); store.source.add(source); Map fieldMap = new LinkedHashMap<>(); fieldMap.put("old-input", "new-input"); fieldMap.put("old-result", "new-result"); expectFailure("已发布", () -> new FormulaTemplateVersionCopyParticipant( store, () -> "copied-formula").afterBindingsActivated( "tenant-1", "source", "target", fieldMap)); assertTrue(store.inserted.isEmpty()); } @Test public void upgradeAndRemoveConfirmAreCasIdempotentAndAuditedOnce() { Fixture upgrade = new Fixture(); ElnFormulaVos.TemplateBinding active = upgrade.activeMapping("reserve-upgrade"); upgrade.addFormulaVersion("formula-pre", "PRE_RELEASE", "DECIMAL"); ElnFormulaForms.TemplateBinding target = upgrade.request("formula-pre", "upgrade-op", map("amount", "input"), "result"); ElnFormulaVos.MappingCheck checked = upgrade.service.checkUpgrade( upgrade.authorization(), active.getId(), target); ElnFormulaForms.UpgradeMappingConfirm confirm = new ElnFormulaForms.UpgradeMappingConfirm(); confirm.setOperationCode("upgrade-op"); confirm.setExpectedVersion( checked.getExpectedVersions().get(active.getId())); confirm.setTarget(target); confirm.setControls(Arrays.asList(upgrade.control("result", "sdtContentLocked"), upgrade.control("result-ref", "sdtContentLocked"))); upgrade.service.confirmUpgrade(upgrade.authorization(), active.getId(), confirm); upgrade.service.confirmUpgrade(upgrade.authorization(), active.getId(), confirm); assertEquals("formula-pre", upgrade.store.binding(active.getId()).getGongshiBanbenId()); assertEquals(1, upgrade.auditCount("ELN_TEMPLATE_FORMULA_UPGRADE")); Fixture remove = new Fixture(); ElnFormulaVos.TemplateBinding removable = remove.activeMapping("reserve-remove"); ElnFormulaVos.MappingCheck removal = remove.service.checkRemove( remove.authorization(), removable.getId()); ElnFormulaForms.RemoveMappingConfirm removeConfirm = new ElnFormulaForms.RemoveMappingConfirm(); removeConfirm.setOperationCode(removal.getOperationCode()); removeConfirm.setExpectedVersion(removal.getExpectedVersions().get(removable.getId())); removeConfirm.setControls(Arrays.asList(remove.control("result", null), remove.control("result-ref", null))); remove.service.confirmRemove(remove.authorization(), removable.getId(), removeConfirm); remove.service.confirmRemove(remove.authorization(), removable.getId(), removeConfirm); assertEquals("REMOVED", remove.store.binding(removable.getId()).getBangdingZhuangtai()); assertEquals(1, remove.auditCount("ELN_TEMPLATE_FORMULA_REMOVE")); } @Test public void versionCopyConfirmRequiresExactSetAndRetriesWithoutSecondAudit() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy"); ElnTemplateFormulaBindingEntity copied = f.formulaBinding( "copied-formula", "template-1", "PENDING"); copied.setLaiyuanGongshiBangdingId("source-formula"); copied.setCaozuoBianma("upgrade-copy:source-formula"); copied.setDaoqiShijian(null); f.store.bindings.put(copied.getId(), copied); ElnFormulaForms.FormulaVersionCopyConfirm incomplete = new ElnFormulaForms.FormulaVersionCopyConfirm(); incomplete.setUpgradeOperationCode("upgrade-copy"); incomplete.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("集合", () -> f.service.confirmVersionCopy(f.authorization(), incomplete)); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy"); confirm.setExpectedVersions(Collections.singletonMap("copied-formula", 1)); confirm.setControls(incomplete.getControls()); f.service.confirmVersionCopy(f.authorization(), confirm); f.service.confirmVersionCopy(f.authorization(), confirm); assertEquals("ACTIVE", copied.getBangdingZhuangtai()); assertEquals(1, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void versionCopyConfirmValidatesTheUnionOfAllResultControls() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy-many"); ElnTemplateFormulaBindingEntity first = f.formulaBinding( "copied-formula-1", "template-1", "PENDING"); first.setLaiyuanGongshiBangdingId("source-formula-1"); first.setCaozuoBianma("upgrade-copy-many:source-formula-1"); first.setDaoqiShijian(null); f.store.bindings.put(first.getId(), first); f.addField("result-2", "result-version-2", "DECIMAL", manualBinding()); f.addFieldBinding("result-2-ref", "result-version-2", "REFERENCE", "result-2"); ElnTemplateFormulaBindingEntity second = f.formulaBinding( "copied-formula-2", "template-1", "PENDING"); second.setJieguoBangdingId("result-2"); second.setLaiyuanGongshiBangdingId("source-formula-2"); second.setCaozuoBianma("upgrade-copy-many:source-formula-2"); second.setDaoqiShijian(null); f.store.bindings.put(second.getId(), second); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy-many"); Map expected = new LinkedHashMap<>(); expected.put(first.getId(), 1); expected.put(second.getId(), 1); confirm.setExpectedVersions(expected); confirm.setControls(Arrays.asList( f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"), f.control("result-2", "sdtContentLocked"), f.control("result-2-ref", "sdtContentLocked"))); f.service.confirmVersionCopy(f.authorization(), confirm); assertEquals("ACTIVE", first.getBangdingZhuangtai()); assertEquals("ACTIVE", second.getBangdingZhuangtai()); assertEquals(2, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void versionCopyConfirmRejectsMixedPendingAndActiveState() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy-mixed"); ElnTemplateFormulaBindingEntity pending = f.formulaBinding( "copied-formula-pending", "template-1", "PENDING"); pending.setLaiyuanGongshiBangdingId("source-formula-pending"); pending.setCaozuoBianma("upgrade-copy-mixed:source-formula-pending"); pending.setDaoqiShijian(null); f.store.bindings.put(pending.getId(), pending); ElnTemplateFormulaBindingEntity active = f.formulaBinding( "copied-formula-active", "template-1", "ACTIVE"); active.setLaiyuanGongshiBangdingId("source-formula-active"); active.setCaozuoBianma("upgrade-copy-mixed:source-formula-active"); active.setDaoqiShijian(null); active.setFVersion(2); f.store.bindings.put(active.getId(), active); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy-mixed"); confirm.setExpectedVersions(Collections.singletonMap(pending.getId(), 1)); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("混合", () -> f.service.confirmVersionCopy(f.authorization(), confirm)); assertEquals("PENDING", pending.getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void versionCopyConfirmRejectsMalformedCopiedOperationWithoutPartialActivation() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy-strict"); ElnTemplateFormulaBindingEntity valid = f.formulaBinding( "copied-formula-valid", "template-1", "PENDING"); valid.setLaiyuanGongshiBangdingId("source-formula-valid"); valid.setCaozuoBianma("upgrade-copy-strict:source-formula-valid"); valid.setDaoqiShijian(null); f.store.bindings.put(valid.getId(), valid); ElnTemplateFormulaBindingEntity malformed = f.formulaBinding( "copied-formula-malformed", "template-1", "PENDING"); malformed.setLaiyuanGongshiBangdingId("source-formula-malformed"); malformed.setCaozuoBianma("upgrade-copy-strict:wrong-source"); malformed.setDaoqiShijian(null); f.store.bindings.put(malformed.getId(), malformed); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy-strict"); confirm.setExpectedVersions(Collections.singletonMap(valid.getId(), 1)); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("操作码", () -> f.service.confirmVersionCopy(f.authorization(), confirm)); assertEquals("PENDING", valid.getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void versionCopyConfirmRejectsCopiedPendingWithExpiry() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy-expiry"); ElnTemplateFormulaBindingEntity copied = f.formulaBinding( "copied-formula-expiry", "template-1", "PENDING"); copied.setLaiyuanGongshiBangdingId("source-formula-expiry"); copied.setCaozuoBianma("upgrade-copy-expiry:source-formula-expiry"); copied.setDaoqiShijian(new Date(System.currentTimeMillis() + 60_000L)); f.store.bindings.put(copied.getId(), copied); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy-expiry"); confirm.setExpectedVersions(Collections.singletonMap(copied.getId(), 1)); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("到期", () -> f.service.confirmVersionCopy(f.authorization(), confirm)); assertEquals("PENDING", copied.getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void upgradeCheckRejectsOperationCodeOwnedByAnotherMapping() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding source = f.activeMapping("upgrade-source-op"); ElnTemplateFormulaBindingEntity occupied = f.formulaBinding( "occupied-formula", "template-1", "ACTIVE"); occupied.setCaozuoBianma("occupied-upgrade-op"); f.store.bindings.put(occupied.getId(), occupied); ElnFormulaForms.TemplateBinding target = f.request("formula-current", " occupied-upgrade-op ", map("amount", "input"), "result"); expectFailure("操作码", () -> f.service.checkUpgrade( f.authorization(), source.getId(), target)); } @Test public void upgradeNormalizesOperationCodeForCheckConfirmAndRetry() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding source = f.activeMapping("upgrade-trim-source"); ElnFormulaForms.TemplateBinding target = f.request("formula-current", " upgrade-trimmed ", map("amount", "input"), "result"); ElnFormulaVos.MappingCheck checked = f.service.checkUpgrade( f.authorization(), source.getId(), target); assertEquals("upgrade-trimmed", checked.getOperationCode()); ElnFormulaForms.UpgradeMappingConfirm confirm = new ElnFormulaForms.UpgradeMappingConfirm(); confirm.setOperationCode(" upgrade-trimmed "); confirm.setExpectedVersion(2); confirm.setTarget(target); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); f.service.confirmUpgrade(f.authorization(), source.getId(), confirm); f.service.confirmUpgrade(f.authorization(), source.getId(), confirm); assertEquals("upgrade-trimmed", f.store.binding(source.getId()).getCaozuoBianma()); assertEquals(1, f.auditCount("ELN_TEMPLATE_FORMULA_UPGRADE")); } @Test public void removeCheckReportsAndConfirmRejectsDownstreamFormula() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding upstream = f.activeMapping("upstream"); f.addField("result-2", "result-version-2", "DECIMAL", manualBinding()); ElnTemplateFormulaBindingEntity downstream = f.formulaBinding( "downstream", "template-1", "ACTIVE"); downstream.setCanshuYingshe(map("amount", "result")); downstream.setJieguoBangdingId("result-2"); f.store.bindings.put(downstream.getId(), downstream); ElnFormulaVos.MappingCheck check = f.service.checkRemove( f.authorization(), upstream.getId()); assertEquals(Collections.singletonList("downstream"), check.getDownstreamFormulaBindingIds()); ElnFormulaForms.RemoveMappingConfirm confirm = new ElnFormulaForms.RemoveMappingConfirm(); confirm.setOperationCode(check.getOperationCode()); confirm.setExpectedVersion(check.getExpectedVersions().get(upstream.getId())); confirm.setControls(Arrays.asList(f.control("result", null), f.control("result-ref", null))); expectFailure("下游", () -> f.service.confirmRemove( f.authorization(), upstream.getId(), confirm)); assertEquals("ACTIVE", f.store.binding(upstream.getId()).getBangdingZhuangtai()); } @Test public void upgradeValidatesLocksForTheTargetResultBinding() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding active = f.activeMapping("reserve-upgrade-result"); f.addFormulaVersion("formula-pre", "PRE_RELEASE", "DECIMAL"); f.addField("result-2", "result-version-2", "DECIMAL", manualBinding()); f.addFieldBinding("result-2-ref", "result-version-2", "REFERENCE", "result-2"); ElnFormulaForms.TemplateBinding target = f.request("formula-pre", "upgrade-result-op", map("amount", "input"), "result-2"); ElnFormulaVos.MappingCheck checked = f.service.checkUpgrade( f.authorization(), active.getId(), target); ElnFormulaForms.UpgradeMappingConfirm confirm = new ElnFormulaForms.UpgradeMappingConfirm(); confirm.setOperationCode("upgrade-result-op"); confirm.setExpectedVersion(checked.getExpectedVersions().get(active.getId())); confirm.setTarget(target); confirm.setControls(Arrays.asList(f.control("result-2", "sdtContentLocked"), f.control("result-2-ref", "sdtContentLocked"))); f.service.confirmUpgrade(f.authorization(), active.getId(), confirm); assertEquals("result-2", f.store.binding(active.getId()).getJieguoBangdingId()); } @Test public void removeConfirmRejectsUnlockedSnapshotWithWrongControlTag() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding active = f.activeMapping("reserve-remove-tag"); ElnFormulaVos.MappingCheck checked = f.service.checkRemove( f.authorization(), active.getId()); ElnFormulaForms.RemoveMappingConfirm confirm = new ElnFormulaForms.RemoveMappingConfirm(); confirm.setOperationCode(checked.getOperationCode()); confirm.setExpectedVersion(checked.getExpectedVersions().get(active.getId())); ElnFormulaForms.ControlSnapshot primary = f.control("result", null); primary.setControlTag("eln.field.forged"); confirm.setControls(Arrays.asList(primary, f.control("result-ref", null))); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.confirmRemove( f.authorization(), active.getId(), confirm)); assertEquals("ACTIVE", f.store.binding(active.getId()).getBangdingZhuangtai()); } @Test public void removeConfirmRejectsControlRoleMismatchWithoutChangingActiveState() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding active = f.activeMapping("reserve-remove-role"); ElnFormulaVos.MappingCheck checked = f.service.checkRemove( f.authorization(), active.getId()); ElnFormulaForms.RemoveMappingConfirm confirm = new ElnFormulaForms.RemoveMappingConfirm(); confirm.setOperationCode(checked.getOperationCode()); confirm.setExpectedVersion(checked.getExpectedVersions().get(active.getId())); ElnFormulaForms.ControlSnapshot result = f.control("result", null); result.setControlRole("REFERENCE"); confirm.setControls(Arrays.asList(result, f.control("result-ref", null))); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.confirmRemove( f.authorization(), active.getId(), confirm)); assertEquals("ACTIVE", f.store.binding(active.getId()).getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_REMOVE")); } @Test public void versionCopyConfirmRejectsControlRoleMismatchWithoutActivation() { Fixture f = new Fixture(); f.store.template.setShengbanCaozuoBianma("upgrade-copy-role"); ElnTemplateFormulaBindingEntity copied = f.formulaBinding( "copied-formula-role", "template-1", "PENDING"); copied.setLaiyuanGongshiBangdingId("source-formula-role"); copied.setCaozuoBianma("upgrade-copy-role:source-formula-role"); copied.setDaoqiShijian(null); f.store.bindings.put(copied.getId(), copied); ElnFormulaForms.FormulaVersionCopyConfirm confirm = new ElnFormulaForms.FormulaVersionCopyConfirm(); confirm.setUpgradeOperationCode("upgrade-copy-role"); confirm.setExpectedVersions(Collections.singletonMap(copied.getId(), 1)); ElnFormulaForms.ControlSnapshot result = f.control("result", "sdtContentLocked"); result.setControlRole("REFERENCE"); confirm.setControls(Arrays.asList(result, f.control("result-ref", "sdtContentLocked"))); expectFailure("FORMULA_RESULT_LOCK_MISMATCH", () -> f.service.confirmVersionCopy( f.authorization(), confirm)); assertEquals("PENDING", copied.getBangdingZhuangtai()); assertEquals(0, f.auditCount("ELN_TEMPLATE_FORMULA_VERSION_COPY_CONFIRM")); } @Test public void confirmUpgradeRejectsNonActiveSourceMapping() { Fixture f = new Fixture(); ElnFormulaVos.TemplateBinding reserved = f.service.reserve(f.authorization(), f.request("formula-current", "cancel-before-upgrade", map("amount", "input"), "result")); ElnFormulaForms.CancelFormulaBinding cancel = new ElnFormulaForms.CancelFormulaBinding(); cancel.setOperationCode("cancel-before-upgrade"); cancel.setExpectedVersion(reserved.getVersion()); f.service.cancel(f.authorization(), reserved.getId(), cancel); f.addFormulaVersion("formula-pre", "PRE_RELEASE", "DECIMAL"); ElnFormulaForms.TemplateBinding target = f.request("formula-pre", "invalid-upgrade", map("amount", "input"), "result"); ElnFormulaForms.UpgradeMappingConfirm confirm = new ElnFormulaForms.UpgradeMappingConfirm(); confirm.setOperationCode("invalid-upgrade"); confirm.setExpectedVersion(2); confirm.setTarget(target); confirm.setControls(Arrays.asList(f.control("result", "sdtContentLocked"), f.control("result-ref", "sdtContentLocked"))); expectFailure("状态", () -> f.service.confirmUpgrade( f.authorization(), reserved.getId(), confirm)); assertEquals("CANCELLED", f.store.binding(reserved.getId()).getBangdingZhuangtai()); } private static void expectFailure(String message, Runnable operation) { try { operation.run(); fail("应拒绝: " + message); } catch (DataException ex) { assertTrue(ex.getMessage(), ex.getMessage().contains(message)); } } private static Map map(String key, String value) { Map result = new LinkedHashMap<>(); result.put(key, value); return result; } private static String operationOfLength(int length) { StringBuilder result = new StringBuilder(length); for (int index = 0; index < length; index++) result.append('x'); return result.toString(); } private static Callable activation(Fixture fixture, ConcurrentGraphStore store, String bindingId, String operationCode, String resultBindingId) { return () -> { ElnFormulaForms.ActivateFormulaBinding form = new ElnFormulaForms.ActivateFormulaBinding(); form.setOperationCode(operationCode); form.setExpectedVersion(1); form.setControls(Collections.singletonList( fixture.control(resultBindingId, "sdtContentLocked"))); try { fixture.service.activate(fixture.authorization(), bindingId, form); store.commitTransaction(); return "SUCCESS"; } catch (DataException ex) { store.rollbackTransaction(); return ex.getMessage(); } catch (RuntimeException | Error ex) { store.rollbackTransaction(); throw ex; } }; } private static Callable upgrade(Fixture fixture, ConcurrentGraphStore store, String bindingId, String operationCode, String parameterBindingId, String resultBindingId) { return () -> { ElnFormulaForms.TemplateBinding target = fixture.request( "formula-current", operationCode, map("amount", parameterBindingId), resultBindingId); ElnFormulaForms.UpgradeMappingConfirm form = new ElnFormulaForms.UpgradeMappingConfirm(); form.setOperationCode(operationCode); form.setExpectedVersion(1); form.setTarget(target); form.setControls(Collections.singletonList( fixture.control(resultBindingId, "sdtContentLocked"))); try { fixture.service.confirmUpgrade(fixture.authorization(), bindingId, form); store.commitTransaction(); return "SUCCESS"; } catch (DataException ex) { store.rollbackTransaction(); return ex.getMessage(); } catch (RuntimeException | Error ex) { store.rollbackTransaction(); throw ex; } }; } private static final class Fixture { private final MemoryStore store; private final List audits = Collections.synchronizedList(new ArrayList<>()); private final ElnFormulaDefEntity definition = new ElnFormulaDefEntity(); private final Map versions; private final Map fieldVersions; private final ElnTemplateFormulaBindingServiceImpl service; private Fixture() { this(new MemoryStore()); } private Fixture(MemoryStore store) { this.store = store; this.versions = store.versions; this.fieldVersions = store.fieldVersions; ElnTemplateVersionEntity template = new ElnTemplateVersionEntity(); template.setId("template-1"); template.setFTenantId("tenant-1"); template.setBizStatus("DRAFT"); template.setBizEnabled("enabled"); store.template = template; definition.setId("formula-def"); definition.setFTenantId("tenant-1"); definition.setBizEnabled("enabled"); definition.setDangqianGongshiBanbenId("formula-current"); store.definition = definition; addFormulaVersion("formula-current", "PUBLISHED", "DECIMAL"); ElnFormulaParameterSlotEntity slot = new ElnFormulaParameterSlotEntity(); slot.setId("slot-1"); slot.setFTenantId("tenant-1"); slot.setGongshiBanbenId("formula-current"); slot.setCanshuBianma("amount"); slot.setShujuLeixing("DECIMAL"); store.slots.put("formula-current", Collections.singletonList(slot)); addField("input", "input-version", "DECIMAL", manualBinding()); addField("result", "result-version", "DECIMAL", manualBinding()); addFieldBinding("result-ref", "result-version", "REFERENCE", "result"); service = new ElnTemplateFormulaBindingServiceImpl(store, new FormulaDependencyGraph(), () -> "formula-binding-1", audits::add, 30L); } private void addFormulaVersion(String id, String status, String resultType) { ElnFormulaVersionEntity version = new ElnFormulaVersionEntity(); version.setId(id); version.setFTenantId("tenant-1"); version.setGongshiDingyiId("formula-def"); version.setBizStatus(status); version.setJieguoLeixing(resultType); version.setFVersion(1); versions.put(id, version); if (!store.slots.containsKey(id)) { ElnFormulaParameterSlotEntity copied = new ElnFormulaParameterSlotEntity(); copied.setId("slot-" + id); copied.setFTenantId("tenant-1"); copied.setGongshiBanbenId(id); copied.setCanshuBianma("amount"); copied.setShujuLeixing("DECIMAL"); store.slots.put(id, Collections.singletonList(copied)); } } private void addField(String bindingId, String versionId, String type, String bindingConfig) { ElnFieldVersionEntity version = new ElnFieldVersionEntity(); version.setId(versionId); version.setFTenantId("tenant-1"); version.setZiduanDingyiId("field-" + bindingId); version.setShujuLeixing(type); version.setPiciMoshi("RECORD_PRIVATE"); version.setBangdingPeizhi(bindingConfig); fieldVersions.put(versionId, version); addFieldBinding(bindingId, versionId, "PRIMARY", null); } private void addRepeatField(String bindingId, String versionId, String groupCode) { addField(bindingId, versionId, "DECIMAL", manualBinding()); ElnTemplateBindingEntity binding = store.fieldBindings.get(bindingId); binding.setKongjianJuese("REPEAT"); binding.setRepeatGroupCode(groupCode); binding.setRepeatPrototypeRowId("prototype"); binding.setKongjianBiaoqian("eln.repeat." + groupCode + ".prototype." + bindingId); } private void addFieldBinding(String id, String versionId, String role, String primary) { ElnTemplateBindingEntity binding = new ElnTemplateBindingEntity(); binding.setId(id); binding.setFTenantId("tenant-1"); binding.setMubanId("template-1"); binding.setZiduanBanbenId(versionId); ElnFieldVersionEntity version = fieldVersions.get(versionId); binding.setZiduanDingyiId(version == null ? null : version.getZiduanDingyiId()); binding.setKongjianBiaoqian("eln.field." + id); binding.setKongjianJuese(role); binding.setYinyongShiliId(primary); binding.setBangdingZhuangtai("ACTIVE"); store.fieldBindings.put(id, binding); } private ElnFormulaForms.TemplateBinding request(String versionId, String operation, Map parameters, String result) { ElnFormulaForms.TemplateBinding form = new ElnFormulaForms.TemplateBinding(); form.setFormulaVersionId(versionId); form.setParameterBindings(parameters); form.setResultBindingId(result); form.setOperationCode(operation); form.setJieguoSuoKuaizhao(Arrays.asList(control("result", ""), control("result-ref", "contentLocked"))); return form; } private ElnFormulaForms.ControlSnapshot control(String bindingId, String lock) { ElnTemplateBindingEntity binding = store.fieldBindings.get(bindingId); ElnFormulaForms.ControlSnapshot result = new ElnFormulaForms.ControlSnapshot(); result.setBindingId(bindingId); result.setControlTag(binding.getKongjianBiaoqian()); result.setControlRole(binding.getKongjianJuese()); result.setLock(lock); return result; } private boolean hasAudit(String action) { return audits.stream().anyMatch(item -> action.equals(item.getActionCode())); } private long auditCount(String action) { return audits.stream().filter(item -> action.equals(item.getActionCode())).count(); } private ElnFormulaVos.TemplateBinding activeMapping(String operation) { ElnFormulaVos.TemplateBinding reserved = service.reserve(authorization(), request("formula-current", operation, map("amount", "input"), "result")); ElnFormulaForms.ActivateFormulaBinding activate = new ElnFormulaForms.ActivateFormulaBinding(); activate.setOperationCode(operation); activate.setExpectedVersion(1); activate.setControls(Arrays.asList(control("result", "sdtContentLocked"), control("result-ref", "sdtContentLocked"))); service.activate(authorization(), reserved.getId(), activate); reserved.setStatus("ACTIVE"); reserved.setVersion(2); return reserved; } private ElnTemplateFormulaBindingEntity formulaBinding(String id, String template, String status) { ElnTemplateFormulaBindingEntity item = new ElnTemplateFormulaBindingEntity(); item.setId(id); item.setFTenantId("tenant-1"); item.setMubanId(template); item.setGongshiDingyiId("formula-def"); item.setGongshiBanbenId("formula-current"); item.setCanshuYingshe(map("amount", "input")); item.setJieguoBangdingId("result"); item.setBangdingZhuangtai(status); item.setCaozuoBianma("foreign-op"); item.setJieguoSuoKuaizhao(Arrays.asList(control("result", ""), control("result-ref", "contentLocked"))); item.setFVersion(1); return item; } private TemplateAnnotationAuthorization authorization() { try { Constructor constructor = TemplateAnnotationAuthorization.class.getDeclaredConstructor( String.class, String.class, String.class, String.class); constructor.setAccessible(true); return constructor.newInstance("tenant-1", "user-1", "template-1", "file-1"); } catch (Exception ex) { throw new AssertionError(ex); } } } private static String manualBinding() { Map read = new LinkedHashMap<>(); read.put("source", "NONE"); Map write = new LinkedHashMap<>(); write.put("target", "ELN_RECORD"); Map result = new LinkedHashMap<>(); result.put("read", read); result.put("write", write); return JsonUtil.getObjectToString(result); } private static class MemoryStore implements ElnTemplateFormulaBindingServiceImpl.Store { protected ElnTemplateVersionEntity template; private ElnFormulaDefEntity definition; private final Map versions = new LinkedHashMap<>(); private final Map> slots = new LinkedHashMap<>(); private final Map fieldBindings = new LinkedHashMap<>(); private final Map fieldVersions = new LinkedHashMap<>(); protected final Map bindings = new LinkedHashMap<>(); private ElnTemplateFormulaBindingEntity concurrentInsert; private RuntimeException insertFailure; private int findByOperationCalls; @Override public ElnTemplateVersionEntity findTemplate(String tenant, String id) { return template; } @Override public ElnTemplateVersionEntity lockTemplate(String tenant, String id) { return template; } @Override public ElnFormulaDefEntity findFormulaDefinition(String tenant, String id) { return definition; } @Override public ElnFormulaVersionEntity findFormulaVersion(String tenant, String id) { return versions.get(id); } @Override public List listSlots(String tenant, String version) { return slots.getOrDefault(version, Collections.emptyList()); } @Override public ElnTemplateBindingEntity findFieldBinding(String tenant, String id) { return fieldBindings.get(id); } @Override public List listFieldBindings(String tenant, String template) { List result = new ArrayList<>(); for (ElnTemplateBindingEntity item : fieldBindings.values()) if (template.equals(item.getMubanId())) result.add(item); return result; } @Override public ElnFieldVersionEntity findFieldVersion(String tenant, String id) { return fieldVersions.get(id); } @Override public ElnTemplateFormulaBindingEntity findByOperation(String tenant, String template, String operation) { findByOperationCalls++; return bindings.values().stream().filter(item -> tenant.equals(item.getFTenantId()) && template.equals(item.getMubanId()) && operation.equals(item.getCaozuoBianma())).findFirst().orElse(null); } @Override public ElnTemplateFormulaBindingEntity findBinding(String tenant, String id) { return bindings.get(id); } @Override public List listBindings(String tenant, String template) { List result = new ArrayList<>(); for (ElnTemplateFormulaBindingEntity item : bindings.values()) if (template.equals(item.getMubanId())) result.add(item); return result; } @Override public boolean insertIfAbsent(ElnTemplateFormulaBindingEntity binding) { if (insertFailure != null) throw insertFailure; if (concurrentInsert != null) { bindings.put(concurrentInsert.getId(), concurrentInsert); return false; } bindings.put(binding.getId(), binding); return true; } @Override public boolean updateStatus(ElnTemplateFormulaBindingEntity binding, int expectedVersion, String status, String reason) { if (binding.getFVersion() != expectedVersion) return false; binding.setBangdingZhuangtai(status); binding.setZhuangtaiYuanyin(reason); binding.setFVersion(expectedVersion + 1); return true; } @Override public void updateTopology(ElnTemplateFormulaBindingEntity binding, int order) { binding.setTuopuXuhao(order); } private ElnTemplateFormulaBindingEntity binding(String id) { return bindings.get(id); } } private static final class ConcurrentGraphStore extends MemoryStore { private final CyclicBarrier uncommittedGraphReads = new CyclicBarrier(2); private final ReentrantLock templateLock = new ReentrantLock(); private final ThreadLocal templateLockHeld = ThreadLocal.withInitial(() -> false); private final ThreadLocal staged = new ThreadLocal<>(); private final AtomicInteger lockCalls = new AtomicInteger(); @Override public ElnTemplateVersionEntity lockTemplate(String tenantId, String templateId) { lockCalls.incrementAndGet(); templateLock.lock(); templateLockHeld.set(true); return template; } @Override public synchronized ElnTemplateFormulaBindingEntity findBinding( String tenantId, String id) { return copyBinding(bindings.get(id)); } @Override public synchronized ElnTemplateFormulaBindingEntity findByOperation( String tenantId, String templateId, String operationCode) { for (ElnTemplateFormulaBindingEntity binding : bindings.values()) { if (tenantId.equals(binding.getFTenantId()) && templateId.equals(binding.getMubanId()) && operationCode.equals(binding.getCaozuoBianma())) { return copyBinding(binding); } } return null; } @Override public boolean updateStatus(ElnTemplateFormulaBindingEntity binding, int expectedVersion, String status, String reason) { if (binding.getFVersion() != expectedVersion) return false; binding.setBangdingZhuangtai(status); binding.setZhuangtaiYuanyin(reason); binding.setFVersion(expectedVersion + 1); staged.set(copyBinding(binding)); return true; } @Override public List listBindings( String tenantId, String templateId) { ElnTemplateFormulaBindingEntity local = staged.get(); if (local != null && !templateLockHeld.get()) { try { uncommittedGraphReads.await(5, TimeUnit.SECONDS); } catch (Exception ex) { throw new AssertionError("并发事务未同时进入图校验", ex); } } Map visible = new LinkedHashMap<>(); synchronized (this) { for (ElnTemplateFormulaBindingEntity binding : bindings.values()) { visible.put(binding.getId(), copyBinding(binding)); } } if (local != null) visible.put(local.getId(), copyBinding(local)); return new ArrayList<>(visible.values()); } @Override public void updateTopology(ElnTemplateFormulaBindingEntity binding, int order) { ElnTemplateFormulaBindingEntity local = staged.get(); if (local != null && local.getId().equals(binding.getId())) { local.setTuopuXuhao(order); } } private void commitTransaction() { ElnTemplateFormulaBindingEntity local = staged.get(); if (local != null) { synchronized (this) { bindings.put(local.getId(), copyBinding(local)); } } finishTransaction(); } private void rollbackTransaction() { finishTransaction(); } private void finishTransaction() { staged.remove(); if (templateLockHeld.get()) { templateLockHeld.remove(); templateLock.unlock(); } } private synchronized int activeCount() { int count = 0; for (ElnTemplateFormulaBindingEntity binding : bindings.values()) { if ("ACTIVE".equals(binding.getBangdingZhuangtai())) count++; } return count; } private synchronized List committedBindings() { List result = new ArrayList<>(); for (ElnTemplateFormulaBindingEntity binding : bindings.values()) { result.add(copyBinding(binding)); } return result; } private static ElnTemplateFormulaBindingEntity copyBinding( ElnTemplateFormulaBindingEntity binding) { return binding == null ? null : JsonUtil.getJsonToBean( JsonUtil.getObjectToString(binding), ElnTemplateFormulaBindingEntity.class); } } private static final class LockCheckingStore extends MemoryStore { private final AtomicInteger lockCalls = new AtomicInteger(); private boolean locked; @Override public ElnTemplateVersionEntity lockTemplate(String tenantId, String templateId) { lockCalls.incrementAndGet(); locked = true; return super.lockTemplate(tenantId, templateId); } @Override public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) { requireLocked(); return super.findTemplate(tenantId, templateId); } @Override public List listBindings( String tenantId, String templateId) { requireLocked(); return super.listBindings(tenantId, templateId); } @Override public boolean updateStatus(ElnTemplateFormulaBindingEntity binding, int expectedVersion, String status, String reason) { requireLocked(); return super.updateStatus(binding, expectedVersion, status, reason); } @Override public void updateTopology(ElnTemplateFormulaBindingEntity binding, int order) { requireLocked(); super.updateTopology(binding, order); } private void requireLocked() { if (!locked) throw new AssertionError("模板图读写必须位于数据库锁内"); } } private static final class CopyStore implements FormulaTemplateVersionCopyParticipant.Store { private ElnTemplateVersionEntity target; private final ElnFormulaVersionEntity formulaVersion = new ElnFormulaVersionEntity(); private final List source = new ArrayList<>(); private final List inserted = new ArrayList<>(); private ElnTemplateFormulaBindingEntity concurrentInsert; private RuntimeException insertFailure; private int findByOperationCalls; private CopyStore() { formulaVersion.setId("historical-published"); formulaVersion.setGongshiDingyiId("formula-def"); formulaVersion.setBizStatus("PUBLISHED"); } private Map prepareCopy() { target = new ElnTemplateVersionEntity(); target.setId("target"); target.setFTenantId("tenant-1"); target.setBizStatus("DRAFT"); target.setShengbanCaozuoBianma("upgrade-1"); ElnTemplateFormulaBindingEntity item = new ElnTemplateFormulaBindingEntity(); item.setId("source-formula"); item.setFTenantId("tenant-1"); item.setMubanId("source"); item.setGongshiDingyiId("formula-def"); item.setGongshiBanbenId("historical-published"); item.setCanshuYingshe(map("amount", "old-input")); item.setJieguoBangdingId("old-result"); item.setBangdingZhuangtai("ACTIVE"); source.add(item); Map fieldMap = new LinkedHashMap<>(); fieldMap.put("old-input", "new-input"); fieldMap.put("old-result", "new-result"); return fieldMap; } private ElnTemplateFormulaBindingEntity concurrentWinner(Map fieldMap) { ElnTemplateFormulaBindingEntity item = new ElnTemplateFormulaBindingEntity(); item.setId("concurrent-copy"); item.setFTenantId("tenant-1"); item.setMubanId("target"); item.setGongshiDingyiId("formula-def"); item.setGongshiBanbenId("historical-published"); item.setCanshuYingshe(map("amount", fieldMap.get("old-input"))); item.setJieguoBangdingId(fieldMap.get("old-result")); item.setBangdingZhuangtai("PENDING"); item.setCaozuoBianma("upgrade-1:source-formula"); item.setLaiyuanGongshiBangdingId("source-formula"); item.setDaoqiShijian(null); item.setFVersion(1); item.setFDeleteMark(0); return item; } @Override public ElnTemplateVersionEntity findTemplate(String tenantId, String templateId) { return target; } @Override public List listActive( String tenantId, String sourceTemplateId) { return source; } @Override public ElnFormulaVersionEntity findFormulaVersion( String tenantId, String versionId) { return formulaVersion; } @Override public ElnTemplateFormulaBindingEntity findByOperation( String tenantId, String targetTemplateId, String operationCode) { findByOperationCalls++; return inserted.stream().filter(item -> tenantId.equals(item.getFTenantId()) && targetTemplateId.equals(item.getMubanId()) && operationCode.equals(item.getCaozuoBianma())) .findFirst().orElse(null); } @Override public boolean insertIfAbsent(ElnTemplateFormulaBindingEntity binding) { if (insertFailure != null) throw insertFailure; inserted.add(concurrentInsert == null ? binding : concurrentInsert); return concurrentInsert == null; } } }