暂存提交
This commit is contained in:
@@ -92,7 +92,8 @@ public class MerchantTenantController {
|
||||
@PreAuthorize("hasAuthority('merchant:member:create')")
|
||||
public ApiResponse<MemberResponse> addMember(@Valid @RequestBody CreateMemberRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.create(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
return ApiResponse.success(memberService.create(TenantGuard.tenant(user, "MERCHANT"), request,
|
||||
user.permissions()));
|
||||
}
|
||||
|
||||
@PutMapping("/members/{membershipId:\\d+}")
|
||||
@@ -100,7 +101,8 @@ public class MerchantTenantController {
|
||||
@Operation(summary = "编辑企业成员及角色")
|
||||
public ApiResponse<MemberResponse> updateMember(@PathVariable Long membershipId,
|
||||
@Valid @RequestBody UpdateMemberRequest request, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(memberService.update(TenantGuard.tenant(user, "MERCHANT"), membershipId, request));
|
||||
return ApiResponse.success(memberService.update(TenantGuard.tenant(user, "MERCHANT"), membershipId, request,
|
||||
user.permissions()));
|
||||
}
|
||||
|
||||
@PutMapping("/members/{membershipId:\\d+}/status")
|
||||
|
||||
@@ -18,13 +18,16 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** 企业成员闭环服务,账号凭据归 auth-service,人员档案与租户关系归 IAM。 */
|
||||
@Service
|
||||
public class MemberService {
|
||||
private static final String GRANT_ROLE_PERMISSION = "merchant:member:grant-role";
|
||||
private static final Logger log = LoggerFactory.getLogger(MemberService.class);
|
||||
private final MemberRepository repository;
|
||||
private final IamRoleMapper roleMapper;
|
||||
@@ -49,8 +52,9 @@ public class MemberService {
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "MEMBER_CREATE", resourceType = "MEMBERSHIP", resourceId = "#request.username", description = "新增企业成员")
|
||||
public MemberResponse create(long tenantId, CreateMemberRequest request) {
|
||||
public MemberResponse create(long tenantId, CreateMemberRequest request, Set<String> operatorPermissions) {
|
||||
rejectOwnerMutation(request.memberType());
|
||||
requireGrantRoleIfChanged(List.of(), request.roleIds(), operatorPermissions);
|
||||
validateRoles(tenantId, request.roleIds());
|
||||
validateMemberNo(tenantId, request.memberNo(), null);
|
||||
long authId = IdWorker.getId();
|
||||
@@ -84,11 +88,14 @@ public class MemberService {
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "MEMBER_UPDATE", resourceType = "MEMBERSHIP", resourceId = "#membershipId", description = "编辑企业成员")
|
||||
public MemberResponse update(long tenantId, Long membershipId, UpdateMemberRequest request) {
|
||||
public MemberResponse update(long tenantId, Long membershipId, UpdateMemberRequest request,
|
||||
Set<String> operatorPermissions) {
|
||||
rejectOwnerMutation(request.memberType());
|
||||
IamMembership membership = required(tenantId, membershipId);
|
||||
List<Long> currentRoleIds = repository.findRoleIds(tenantId, membershipId);
|
||||
requireGrantRoleIfChanged(currentRoleIds, request.roleIds(), operatorPermissions);
|
||||
validateRoles(tenantId, request.roleIds());
|
||||
validateMemberNo(tenantId, request.memberNo(), membershipId);
|
||||
IamMembership membership = required(tenantId, membershipId);
|
||||
if ("OWNER".equals(membership.getMemberType())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "企业所有者不允许通过成员管理接口编辑");
|
||||
}
|
||||
@@ -171,6 +178,13 @@ public class MemberService {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "禁止分配其他租户、其他应用或已停用角色");
|
||||
}
|
||||
}
|
||||
private void requireGrantRoleIfChanged(List<Long> currentRoleIds, List<Long> requestedRoleIds,
|
||||
Set<String> operatorPermissions) {
|
||||
if (!new HashSet<>(currentRoleIds).equals(new HashSet<>(requestedRoleIds))
|
||||
&& (operatorPermissions == null || !operatorPermissions.contains(GRANT_ROLE_PERMISSION))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "变更成员角色需要 merchant:member:grant-role 权限");
|
||||
}
|
||||
}
|
||||
private void rejectOwnerMutation(String memberType) {
|
||||
if ("OWNER".equals(memberType)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "不能通过成员管理接口创建或转让企业所有者");
|
||||
|
||||
@@ -29,3 +29,14 @@ baiyue:
|
||||
jwt-secret: ${JWT_SECRET:change-me-to-at-least-32-bytes-long-before-start}
|
||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:change-me-to-an-internal-token-at-least-32-bytes-long}
|
||||
management.endpoints.web.exposure.include: health,info,prometheus
|
||||
|
||||
logging:
|
||||
file:
|
||||
name: "${LOGGING_FILE_NAME:${LOG_PATH:./logs}/${spring.application.name}.log}"
|
||||
logback:
|
||||
rollingpolicy:
|
||||
file-name-pattern: "${LOGGING_ROLLING_FILE_NAME_PATTERN:${LOG_PATH:./logs}/${spring.application.name}.%d{yyyy-MM-dd}.%i.log.gz}"
|
||||
max-file-size: "${LOGGING_ROLLING_MAX_FILE_SIZE:100MB}"
|
||||
max-history: "${LOGGING_ROLLING_MAX_HISTORY:30}"
|
||||
total-size-cap: "${LOGGING_ROLLING_TOTAL_SIZE_CAP:5GB}"
|
||||
clean-history-on-start: "${LOGGING_ROLLING_CLEAN_HISTORY_ON_START:false}"
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.dto.MemberDtos.*;
|
||||
import com.baiyue.iam.entity.IamMembership;
|
||||
import com.baiyue.iam.entity.IamRole;
|
||||
import com.baiyue.iam.entity.IamUser;
|
||||
import com.baiyue.iam.mapper.IamRoleMapper;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
@@ -14,6 +15,7 @@ import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
@@ -80,11 +82,88 @@ class MemberServiceTest {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("所有者", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
assertThrows(BizException.class, () -> service.update(100L, 20L, request));
|
||||
assertThrows(BizException.class, () -> service.update(100L, 20L, request, Set.of()));
|
||||
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void createWithRolesRequiresGrantRolePermission() {
|
||||
CreateMemberRequest request = new CreateMemberRequest("member01", "Password1", "成员",
|
||||
null, null, "EMPLOYEE", null, List.of(50L));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.create(100L, request, Set.of("merchant:member:create")));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verifyNoInteractions(auth);
|
||||
verify(repository, never()).insert(any());
|
||||
}
|
||||
|
||||
@Test void profileOnlyUpdateAllowsUnchangedRolesWithoutGrantPermission() {
|
||||
IamRole role = activeMerchantRole(50L, 100L);
|
||||
when(roleMapper.selectById(50L)).thenReturn(role);
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("新姓名", "13800138000", null,
|
||||
"EMPLOYEE", "M-001", List.of(50L));
|
||||
|
||||
MemberResponse result = service.update(100L, 20L, request, Set.of("merchant:member:edit"));
|
||||
|
||||
assertEquals("新姓名", result.displayName());
|
||||
verify(repository).saveUser(user);
|
||||
verify(repository).replaceRoles(100L, 20L, List.of(50L));
|
||||
}
|
||||
|
||||
@Test void roleChangeRequiresGrantRolePermission() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of(60L));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.update(100L, 20L, request, Set.of("merchant:member:edit")));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verify(repository, never()).save(any());
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void clearingRolesAlsoRequiresGrantRolePermission() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.update(100L, 20L, request, Set.of("merchant:member:edit")));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
verify(repository, never()).replaceRoles(anyLong(), anyLong(), anyList());
|
||||
}
|
||||
|
||||
@Test void grantRolePermissionAllowsClearingRoles() {
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of());
|
||||
|
||||
service.update(100L, 20L, request,
|
||||
Set.of("merchant:member:edit", "merchant:member:grant-role"));
|
||||
|
||||
verify(repository).replaceRoles(100L, 20L, List.of());
|
||||
}
|
||||
|
||||
@Test void crossTenantUpdateReturnsNotFoundBeforeReadingRoles() {
|
||||
when(repository.findByTenantAndId(200L, 20L)).thenReturn(null);
|
||||
UpdateMemberRequest request = new UpdateMemberRequest("测试成员", null, null,
|
||||
"EMPLOYEE", null, List.of(50L));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> service.update(200L, 20L, request, Set.of("merchant:member:grant-role")));
|
||||
|
||||
assertEquals(ResultCode.NOT_FOUND, exception.getResultCode());
|
||||
verify(repository, never()).findRoleIds(200L, 20L);
|
||||
}
|
||||
|
||||
private IamRole activeMerchantRole(long id, long tenantId) {
|
||||
IamRole role = new IamRole();
|
||||
role.setId(id); role.setTenantId(tenantId); role.setAppCode("MERCHANT");
|
||||
role.setRoleCode("MERCHANT_EMPLOYEE"); role.setStatus("ACTIVE");
|
||||
return role;
|
||||
}
|
||||
|
||||
@Test void tenantOwnerPasswordCannotBeResetByMemberAdministrator() {
|
||||
member.setMemberType("OWNER");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user