fix(用户管理): 删除用户时级联清理服务端密钥,修已删账号留下孤儿密钥行
biz_user_api_secret 按 uid 绑定且无外键,删除用户后密钥/代理配置成为无主行 (生产已出现 5 条孤儿)。deleteUser 内同步调用 adminClearByUser 并纳入同一事务, 删除失败(行不存在)时不触碰密钥。
This commit is contained in:
+8
@@ -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.dto.UserColumnPermissionUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
||||||
|
import com.nanri.aiimage.modules.usersecret.service.UserApiSecretService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -32,6 +34,7 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
public class AdminUserService {
|
public class AdminUserService {
|
||||||
|
|
||||||
private static final DateTimeFormatter CREATED_AT_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
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 WerkzeugPasswordEncoder passwordEncoder;
|
||||||
private final AdminAuthSupport adminAuthSupport;
|
private final AdminAuthSupport adminAuthSupport;
|
||||||
private final PermissionMenuService permissionMenuService;
|
private final PermissionMenuService permissionMenuService;
|
||||||
|
private final UserApiSecretService userApiSecretService;
|
||||||
|
|
||||||
public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize,
|
public AdminUserListVo listUsers(AdminUserEntity currentUser, Integer page, Integer pageSize,
|
||||||
String username, Long createdById, String roleFilter) {
|
String username, Long createdById, String roleFilter) {
|
||||||
@@ -271,6 +275,8 @@ public class AdminUserService {
|
|||||||
operator, userId, permissionRequest, menuType, false);
|
operator, userId, permissionRequest, menuType, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 删除用户并级联清理其服务端密钥;不清理会留下无主密钥行(biz_user_api_secret 按 uid 绑定,无外键)。 */
|
||||||
|
@Transactional
|
||||||
public void deleteUser(AdminUserEntity currentUser, Long uid) {
|
public void deleteUser(AdminUserEntity currentUser, Long uid) {
|
||||||
String role = adminAuthSupport.currentRole(currentUser);
|
String role = adminAuthSupport.currentRole(currentUser);
|
||||||
if (role == null) {
|
if (role == null) {
|
||||||
@@ -297,6 +303,8 @@ public class AdminUserService {
|
|||||||
if (affected == 0) {
|
if (affected == 0) {
|
||||||
throw new BusinessException("用户不存在");
|
throw new BusinessException("用户不存在");
|
||||||
}
|
}
|
||||||
|
int secretRows = userApiSecretService.adminClearByUser(uid);
|
||||||
|
log.info("[admin-user] 用户已删除 uid={} 级联清理密钥行={}", uid, secretRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, String> loadCreatorMap(List<AdminUserEntity> rows) {
|
private Map<Long, String> loadCreatorMap(List<AdminUserEntity> rows) {
|
||||||
|
|||||||
+56
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.nanri.aiimage.modules.admin.service;
|
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.AdminUserCreateRequest;
|
||||||
import com.nanri.aiimage.modules.admin.model.dto.AdminUserUpdateRequest;
|
import com.nanri.aiimage.modules.admin.model.dto.AdminUserUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.admin.support.AdminAuthSupport;
|
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.dto.UserColumnPermissionUpdateRequest;
|
||||||
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
import com.nanri.aiimage.modules.permission.model.entity.AdminUserEntity;
|
||||||
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
import com.nanri.aiimage.modules.permission.service.PermissionMenuService;
|
||||||
|
import com.nanri.aiimage.modules.usersecret.service.UserApiSecretService;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -16,6 +18,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
@@ -32,8 +35,9 @@ class AdminUserServiceTest {
|
|||||||
WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class);
|
WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class);
|
||||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
PermissionMenuService permissionService = mock(PermissionMenuService.class);
|
PermissionMenuService permissionService = mock(PermissionMenuService.class);
|
||||||
|
UserApiSecretService userApiSecretService = mock(UserApiSecretService.class);
|
||||||
AdminUserService service = new AdminUserService(
|
AdminUserService service = new AdminUserService(
|
||||||
userMapper, passwordEncoder, authSupport, permissionService);
|
userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService);
|
||||||
AdminUserEntity operator = user(10L, "admin", 1, null);
|
AdminUserEntity operator = user(10L, "admin", 1, null);
|
||||||
AdminUserCreateRequest request = new AdminUserCreateRequest();
|
AdminUserCreateRequest request = new AdminUserCreateRequest();
|
||||||
request.setUsername("created-user");
|
request.setUsername("created-user");
|
||||||
@@ -77,8 +81,9 @@ class AdminUserServiceTest {
|
|||||||
WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class);
|
WerkzeugPasswordEncoder passwordEncoder = mock(WerkzeugPasswordEncoder.class);
|
||||||
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
AdminAuthSupport authSupport = mock(AdminAuthSupport.class);
|
||||||
PermissionMenuService permissionService = mock(PermissionMenuService.class);
|
PermissionMenuService permissionService = mock(PermissionMenuService.class);
|
||||||
|
UserApiSecretService userApiSecretService = mock(UserApiSecretService.class);
|
||||||
AdminUserService service = new AdminUserService(
|
AdminUserService service = new AdminUserService(
|
||||||
userMapper, passwordEncoder, authSupport, permissionService);
|
userMapper, passwordEncoder, authSupport, permissionService, userApiSecretService);
|
||||||
AdminUserEntity operator = user(10L, "admin", 1, null);
|
AdminUserEntity operator = user(10L, "admin", 1, null);
|
||||||
AdminUserEntity target = user(20L, "normal", 0, 10L);
|
AdminUserEntity target = user(20L, "normal", 0, 10L);
|
||||||
AdminUserUpdateRequest request = new AdminUserUpdateRequest();
|
AdminUserUpdateRequest request = new AdminUserUpdateRequest();
|
||||||
@@ -110,6 +115,55 @@ class AdminUserServiceTest {
|
|||||||
.isAnnotationPresent(Transactional.class)).isTrue();
|
.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) {
|
private AdminUserEntity user(Long id, String role, int isAdmin, Long createdById) {
|
||||||
AdminUserEntity user = new AdminUserEntity();
|
AdminUserEntity user = new AdminUserEntity();
|
||||||
user.setId(id);
|
user.setId(id);
|
||||||
|
|||||||
Reference in New Issue
Block a user