暂存提交

This commit is contained in:
2026-07-31 18:10:32 +08:00
parent e834482717
commit 9a37657148
35 changed files with 435 additions and 44 deletions
@@ -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, "不能通过成员管理接口创建或转让企业所有者");
+11
View File
@@ -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");