From 655d784beb0a0e89d6444c5a0c82806da7bcd29b Mon Sep 17 00:00:00 2001 From: let5sne Date: Sun, 12 Jul 2026 02:26:01 +0800 Subject: [PATCH] fix(personal): require outer transaction for reservation --- .../service/PersonalSpaceService.java | 3 +- .../personal/PersonalSpaceServiceTest.java | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalSpaceService.java b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalSpaceService.java index 667c5764..ce13846f 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalSpaceService.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/main/java/org/dromara/aihr/personal/service/PersonalSpaceService.java @@ -6,6 +6,7 @@ import org.dromara.common.core.exception.ServiceException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.util.Map; @@ -46,7 +47,7 @@ public class PersonalSpaceService { * * @return the locked personal space id */ - @Transactional + @Transactional(propagation = Propagation.MANDATORY) public long reserve(PersonalOwner owner, long bytes) { if (bytes < 0 || properties.getMaxItems() <= 0 || properties.getMaxSpaceMb() <= 0) { throw new ServiceException(QUOTA_EXCEEDED); diff --git a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSpaceServiceTest.java b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSpaceServiceTest.java index 259a48d4..b70806b0 100644 --- a/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSpaceServiceTest.java +++ b/backend/ruoyi-modules/ruoyi-aihr/src/test/java/org/dromara/aihr/personal/PersonalSpaceServiceTest.java @@ -6,8 +6,17 @@ import org.dromara.aihr.personal.support.PersonalOwner; import org.dromara.common.core.exception.ServiceException; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.springframework.aop.framework.ProxyFactory; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.AbstractPlatformTransactionManager; +import org.springframework.transaction.support.DefaultTransactionStatus; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; import org.mockito.InOrder; import java.util.Map; @@ -188,6 +197,35 @@ class PersonalSpaceServiceTest { verifyNoInteractions(jdbc); } + @Test + void reserveRequiresAnExistingOuterTransactionThroughTheSpringProxy() { + JdbcTemplate jdbc = mock(JdbcTemplate.class); + PersonalSpaceService target = new PersonalSpaceService(jdbc, properties()); + TestTransactionManager transactionManager = new TestTransactionManager(); + ProxyFactory factory = new ProxyFactory(target); + factory.setProxyTargetClass(true); + TransactionInterceptor interceptor = new TransactionInterceptor(); + interceptor.setTransactionManager(transactionManager); + interceptor.setTransactionAttributeSource(new AnnotationTransactionAttributeSource()); + interceptor.afterPropertiesSet(); + factory.addAdvice(interceptor); + PersonalSpaceService proxy = (PersonalSpaceService) factory.getProxy(); + PersonalOwner owner = new PersonalOwner("000000", 101L, null); + + assertThrows(IllegalTransactionStateException.class, () -> proxy.reserve(owner, 1L)); + verifyNoInteractions(jdbc); + + when(jdbc.queryForMap(contains("from aihr_personal_space"), eq("000000"), eq(101L))) + .thenReturn(space(7L, 1024L, 0L, 0)); + Long spaceId = new TransactionTemplate(transactionManager) + .execute(status -> proxy.reserve(owner, 1L)); + + assertEquals(7L, spaceId); + verify(jdbc).update(contains("on duplicate key update"), + eq("000000"), eq(101L), eq(null), eq(500L * 1024 * 1024)); + verify(jdbc).queryForMap(contains("for update"), eq("000000"), eq(101L)); + } + private static Map space(long id, long quota, long used, int count) { return Map.of( "id", id, @@ -200,4 +238,29 @@ class PersonalSpaceServiceTest { private static PersonalKnowledgeProperties properties() { return new PersonalKnowledgeProperties(); } + + private static final class TestTransactionManager extends AbstractPlatformTransactionManager { + + @Override + protected Object doGetTransaction() { + return new Object(); + } + + @Override + protected boolean isExistingTransaction(Object transaction) { + return TransactionSynchronizationManager.isActualTransactionActive(); + } + + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + } + + @Override + protected void doCommit(DefaultTransactionStatus status) { + } + + @Override + protected void doRollback(DefaultTransactionStatus status) { + } + } }