diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java index f1035184..8a103e5d 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/admin/service/AdminUserService.java @@ -15,7 +15,9 @@ import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper; import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest; import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity; import com.nanri.aiimage.modules.permission.service.PermissionMenuService; +import com.nanri.aiimage.modules.usersecret.service.UserApiSecretService; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -32,6 +34,7 @@ import java.util.stream.Collectors; @Service @RequiredArgsConstructor +@Slf4j public class AdminUserService { private static final DateTimeFormatter CREATED_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); @@ -40,6 +43,7 @@ public class AdminUserService { private final WerkzeugPasswordEncoder passwordEncoder; private final AdminAuthSupport adminAuthSupport; private final PermissionMenuService permissionMenuService; + private final UserApiSecretService userApiSecretService; public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize, String username, Long createdById, String roleFilter) { @@ -271,6 +275,8 @@ public class AdminUserService { operator, userId, permissionRequest, menuType, false); } + /** 删除用户并级联清理其服务端密钥;不清理会留下无主密钥行(biz_user_api_secret 按 uid 绑定,无外键)。 */ + @Transactional public void deleteUser(AdminUserEntity currentUser, Long uid) { String role = adminAuthSupport.currentRole(currentUser); if (role == null) { @@ -297,6 +303,8 @@ public class AdminUserService { if (affected == 0) { throw new BusinessException("用户不存在"); } + int secretRows = userApiSecretService.adminClearByUser(uid); + log.info("[admin-user] 用户已删除 uid={} 级联清理密钥行={}", uid, secretRows); } private Map loadCreatorMap(List rows) { diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/admin/service/AdminUserServiceTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/admin/service/AdminUserServiceTest.java index 02274e5b..8a177399 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/admin/service/AdminUserServiceTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/admin/service/AdminUserServiceTest.java @@ -1,5 +1,6 @@ package com.nanri.aiimage.modules.admin.service; +import com.nanri.aiimage.common.exception.BusinessException; import com.nanri.aiimage.modules.admin.model.dto.AdminUserCreateRequest; import com.nanri.aiimage.modules.admin.model.dto.AdminUserUpdateRequest; import com.nanri.aiimage.modules.admin.support.AdminAuthSupport; @@ -8,6 +9,7 @@ import com.nanri.aiimage.modules.permission.mapper.AdminUserMapper; import com.nanri.aiimage.modules.permission.model.dto.UserColumnPermissionUpdateRequest; import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity; import com.nanri.aiimage.modules.permission.service.PermissionMenuService; +import com.nanri.aiimage.modules.usersecret.service.UserApiSecretService; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.transaction.annotation.Transactional; @@ -16,6 +18,7 @@ import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -32,8 +35,9 @@ class AdminUserServiceTest { WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class); AdminAuthSupport authSupport = mock(AdminAuthSupport.class); PermissionMenuService permissionService = mock(PermissionMenuService.class); + UserApiSecretService userApiSecretService = mock(UserApiSecretService.class); AdminUserService service = new AdminUserService( - userMapper, passwordEncoder, authSupport, permissionService); + userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService); AdminUserEntity operator = user(10L, "admin", 1, null); AdminUserCreateRequest request = new AdminUserCreateRequest(); request.setUsername("created-user"); @@ -77,8 +81,9 @@ class AdminUserServiceTest { WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class); AdminAuthSupport authSupport = mock(AdminAuthSupport.class); PermissionMenuService permissionService = mock(PermissionMenuService.class); + UserApiSecretService userApiSecretService = mock(UserApiSecretService.class); AdminUserService service = new AdminUserService( - userMapper, passwordEncoder, authSupport, permissionService); + userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService); AdminUserEntity operator = user(10L, "admin", 1, null); AdminUserEntity target = user(20L, "normal", 0, 10L); AdminUserUpdateRequest request = new AdminUserUpdateRequest(); @@ -110,6 +115,55 @@ class AdminUserServiceTest { .isAnnotationPresent(Transactional.class)).isTrue(); } + @Test + void deleteUserCascadesUserApiSecretsInSameTransactionalMethod() throws Exception { + AdminUserMapper userMapper = mock(AdminUserMapper.class); + WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class); + AdminAuthSupport authSupport = mock(AdminAuthSupport.class); + PermissionMenuService permissionService = mock(PermissionMenuService.class); + UserApiSecretService userApiSecretService = mock(UserApiSecretService.class); + AdminUserService service = new AdminUserService( + userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService); + AdminUserEntity operator = user(10L, "admin", 1, null); + AdminUserEntity target = user(20L, "normal", 0, 10L); + + when(authSupport.currentRole(operator)).thenReturn("admin"); + when(permissionService.hasAnyAdminMenu(eq(operator), any())).thenReturn(true); + when(userMapper.selectById(20L)).thenReturn(target); + when(userMapper.deleteById(20L)).thenReturn(1); + when(userApiSecretService.adminClearByUser(20L)).thenReturn(3); + + service.deleteUser(operator, 20L); + + verify(userMapper).deleteById(20L); + verify(userApiSecretService).adminClearByUser(20L); + assertThat(AdminUserService.class + .getMethod("deleteUser", AdminUserEntity.class, Long.class) + .isAnnotationPresent(Transactional.class)).isTrue(); + } + + @Test + void deleteUserSkipsSecretCleanupWhenUserRowMissing() { + AdminUserMapper userMapper = mock(AdminUserMapper.class); + WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class); + AdminAuthSupport authSupport = mock(AdminAuthSupport.class); + PermissionMenuService permissionService = mock(PermissionMenuService.class); + UserApiSecretService userApiSecretService = mock(UserApiSecretService.class); + AdminUserService service = new AdminUserService( + userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService); + AdminUserEntity operator = user(10L, "admin", 1, null); + AdminUserEntity target = user(20L, "normal", 0, 10L); + + when(authSupport.currentRole(operator)).thenReturn("admin"); + when(permissionService.hasAnyAdminMenu(eq(operator), any())).thenReturn(true); + when(userMapper.selectById(20L)).thenReturn(target); + when(userMapper.deleteById(20L)).thenReturn(0); + + assertThatThrownBy(() -> service.deleteUser(operator, 20L)) + .isInstanceOf(BusinessException.class); + verify(userApiSecretService, never()).adminClearByUser(any()); + } + private AdminUserEntity user(Long id, String role, int isAdmin, Long createdById) { AdminUserEntity user = new AdminUserEntity(); user.setId(id);