This commit is contained in:
wujinliang
2026-08-27 21:48:47 +08:00
parent daf0ee2acd
commit 9d54c57b7c
38 changed files with 432 additions and 37 deletions
@@ -41,7 +41,13 @@ public class NotificationRecipientResolver {
if (user != null && "ACTIVE".equals(user.getStatus())) unique.put(item.getId(), new ResolvedRecipient(
item.getTenantId(), item.getId(), item.getUserId(), user.getDisplayName(), item.getTenantId() == 0 ? "PLATFORM" : "MERCHANT"));
});
if (unique.isEmpty()) fail("NOTIFICATION_RECIPIENT_EMPTY");
if (unique.isEmpty()) {
if (request.recipientType() == com.baiyue.iam.notification.dto.NotificationDtos.RecipientType.TENANT_ROLE
|| request.recipientType() == com.baiyue.iam.notification.dto.NotificationDtos.RecipientType.PLATFORM_ROLE) {
fail("该角色下暂无可接收消息的有效用户,请先为角色分配启用中的成员");
}
fail("未找到可接收消息的有效成员,请检查用户 ID 和目标企业");
}
return List.copyOf(unique.values());
}
@@ -49,7 +55,7 @@ public class NotificationRecipientResolver {
var role = roles.selectOne(new LambdaQueryWrapper<com.baiyue.iam.entity.IamRole>()
.eq(com.baiyue.iam.entity.IamRole::getTenantId, request.tenantId())
.eq(com.baiyue.iam.entity.IamRole::getRoleCode, request.roleCode()).eq(com.baiyue.iam.entity.IamRole::getStatus,"ACTIVE").last("LIMIT 1"));
if (role == null) fail("NOTIFICATION_RECIPIENT_FORBIDDEN");
if (role == null) fail("角色编码不存在,请检查角色编码是否正确");
List<Long> ids = membershipRoles.selectList(new LambdaQueryWrapper<com.baiyue.iam.entity.IamMembershipRole>()
.eq(com.baiyue.iam.entity.IamMembershipRole::getTenantId,request.tenantId())
.eq(com.baiyue.iam.entity.IamMembershipRole::getRoleId,role.getId())).stream()
@@ -29,7 +29,7 @@ public class RbacRepository {
public IamRole findRole(Long id, long tenantId, String appCode) {
return roleMapper.selectOne(new LambdaQueryWrapper<IamRole>()
.eq(IamRole::getId, id).eq(IamRole::getTenantId, tenantId)
.eq(IamRole::getId, id).in(IamRole::getTenantId, List.of(0L, tenantId))
.eq(IamRole::getAppCode, appCode));
}
@@ -80,7 +80,7 @@ public class RbacRepository {
public List<IamRole> findRoles(long tenantId, String appCode) {
return roleMapper.selectList(new LambdaQueryWrapper<IamRole>()
.eq(IamRole::getTenantId, tenantId).eq(IamRole::getAppCode, appCode)
.in(IamRole::getTenantId, List.of(0L, tenantId)).eq(IamRole::getAppCode, appCode)
.orderByAsc(IamRole::getRoleCode));
}
@@ -109,7 +109,7 @@ public class RbacRepository {
public Set<Long> findRolePermissionIds(long tenantId, Long roleId) {
List<IamRolePermission> rows = rolePermissionMapper.selectList(
new LambdaQueryWrapper<IamRolePermission>()
.eq(IamRolePermission::getTenantId, tenantId)
.in(IamRolePermission::getTenantId, List.of(0L, tenantId))
.eq(IamRolePermission::getRoleId, roleId)
.orderByAsc(IamRolePermission::getPermissionId));
Set<Long> result = new LinkedHashSet<>();
@@ -64,7 +64,8 @@ public class MemberService {
List<Long> roleIds = repository.findRoleIds(tenantId, membership.getId());
List<String> roleNames = roleIds.stream()
.map(roleMapper::selectById)
.filter(role -> role != null && Long.valueOf(tenantId).equals(role.getTenantId()))
.filter(role -> role != null && "MERCHANT".equals(role.getAppCode())
&& (Long.valueOf(tenantId).equals(role.getTenantId()) || Long.valueOf(0L).equals(role.getTenantId())))
.map(IamRole::getRoleName)
.toList();
return new PlatformCustomerMemberDtos.MemberResponse(membership.getId(), user.getId(),
@@ -85,3 +85,10 @@ INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, creat
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
FROM iam_permission p JOIN iam_role r ON r.tenant_id=0 AND r.app_code='PLATFORM' AND r.status='ACTIVE'
WHERE p.tenant_id=0 AND p.permission_code LIKE 'platform:notification%' AND r.role_code='PLATFORM_ADMIN';
-- All active platform employee roles can receive and read their own inbox messages.
INSERT IGNORE INTO iam_role_permission (tenant_id, role_id, permission_id, created_at)
SELECT 0, r.id, p.id, CURRENT_TIMESTAMP(3)
FROM iam_permission p JOIN iam_role r ON r.tenant_id=0 AND r.app_code='PLATFORM' AND r.status='ACTIVE'
WHERE p.tenant_id=0
AND p.permission_code IN ('platform:notification:view','platform:notification:read','platform:notification:read-all');
@@ -145,7 +145,7 @@ components:
tenantId: {type: integer, format: int64, minimum: 0}
templateCode: {type: string, maxLength: 64}
recipientType: {type: string, enum: [USER, MEMBERSHIP, TENANT_ROLE, TENANT_ALL, PLATFORM_ROLE]}
recipientIds: {type: array, minItems: 1, maxItems: 1000, items: {type: integer, format: int64}, description: Required for USER or MEMBERSHIP; forbidden for role/all recipient types.}
recipientIds: {type: array, minItems: 1, maxItems: 1000, items: {type: integer, format: int64}, description: Required for USER or MEMBERSHIP; forbidden for role/all recipient types. For a specified enterprise member, pass the user's ID and use USER; membership IDs are reserved for explicit membership-level calls.}
roleCode: {type: string, minLength: 1, maxLength: 64, description: Required for TENANT_ROLE or PLATFORM_ROLE; forbidden otherwise.}
variables: {type: object, maxProperties: 32, additionalProperties: {type: string, maxLength: 2000}}
scheduledAt: {type: string, format: date-time, nullable: true}
@@ -0,0 +1,101 @@
package com.baiyue.iam.notification;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.iam.entity.IamMembership;
import com.baiyue.iam.entity.IamRole;
import com.baiyue.iam.entity.IamUser;
import com.baiyue.iam.mapper.IamMembershipMapper;
import com.baiyue.iam.mapper.IamMembershipRoleMapper;
import com.baiyue.iam.mapper.IamRoleMapper;
import com.baiyue.iam.mapper.IamUserMapper;
import com.baiyue.iam.notification.dto.NotificationDtos.CreateNotificationRequest;
import com.baiyue.iam.notification.dto.NotificationDtos.RecipientType;
import com.baiyue.iam.notification.service.NotificationRecipientResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;
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.Mockito.mock;
import static org.mockito.Mockito.when;
class NotificationRecipientResolverTest {
private final IamMembershipMapper memberships = mock(IamMembershipMapper.class);
private final IamUserMapper users = mock(IamUserMapper.class);
private final IamRoleMapper roles = mock(IamRoleMapper.class);
private final IamMembershipRoleMapper membershipRoles = mock(IamMembershipRoleMapper.class);
private final NotificationRecipientResolver resolver = new NotificationRecipientResolver();
@BeforeEach
void setup() {
ReflectionTestUtils.setField(resolver, "memberships", memberships);
ReflectionTestUtils.setField(resolver, "users", users);
ReflectionTestUtils.setField(resolver, "roles", roles);
ReflectionTestUtils.setField(resolver, "membershipRoles", membershipRoles);
}
@Test
void resolvesSpecifiedEnterpriseMemberByUserId() {
IamMembership membership = new IamMembership();
membership.setId(301L);
membership.setTenantId(9L);
membership.setUserId(2092537408743079938L);
membership.setStatus("ACTIVE");
IamUser user = new IamUser();
user.setId(membership.getUserId());
user.setDisplayName("测试成员");
user.setStatus("ACTIVE");
when(memberships.selectList(any())).thenReturn(List.of(membership));
when(users.selectById(membership.getUserId())).thenReturn(user);
var recipients = resolver.resolve(request(RecipientType.USER, List.of(membership.getUserId())));
assertThat(recipients).containsExactly(new NotificationRecipientResolver.ResolvedRecipient(
9L, 301L, membership.getUserId(), "测试成员", "MERCHANT"));
}
@Test
void explainsWhenNoActiveRecipientCanBeResolved() {
when(memberships.selectList(any())).thenReturn(List.of());
assertThatThrownBy(() -> resolver.resolve(request(RecipientType.USER, List.of(2092537408743079938L))))
.isInstanceOf(BizException.class)
.hasMessage("未找到可接收消息的有效成员,请检查用户 ID 和目标企业");
}
@Test
void explainsWhenRoleCodeDoesNotExist() {
when(roles.selectOne(any())).thenReturn(null);
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "MISSING_ROLE")))
.isInstanceOf(BizException.class)
.hasMessage("角色编码不存在,请检查角色编码是否正确");
}
@Test
void explainsWhenRoleHasNoActiveMembers() {
IamRole role = new IamRole();
role.setId(501L);
when(roles.selectOne(any())).thenReturn(role);
when(membershipRoles.selectList(any())).thenReturn(List.of());
assertThatThrownBy(() -> resolver.resolve(roleRequest(RecipientType.PLATFORM_ROLE, "CUSTOMER_REVIEWER")))
.isInstanceOf(BizException.class)
.hasMessage("该角色下暂无可接收消息的有效用户,请先为角色分配启用中的成员");
}
private CreateNotificationRequest request(RecipientType recipientType, List<Long> recipientIds) {
return new CreateNotificationRequest(9L, "ORDER_STATUS_CHANGED", recipientType, recipientIds,
null, Map.of(), null, "notification-test-key", null, null);
}
private CreateNotificationRequest roleRequest(RecipientType recipientType, String roleCode) {
return new CreateNotificationRequest(0L, "ORDER_STATUS_CHANGED", recipientType, null,
roleCode, Map.of(), null, "notification-role-test-key", null, null);
}
}
@@ -71,7 +71,8 @@ class MemberServiceTest {
when(tenantRepository.findTenant(100L)).thenReturn(tenant);
when(repository.findByTenant(100L)).thenReturn(List.of(member));
IamRole ownerRole = new IamRole();
ownerRole.setId(50L); ownerRole.setTenantId(100L); ownerRole.setRoleName("企业管理员");
ownerRole.setId(50L); ownerRole.setTenantId(0L); ownerRole.setRoleName("企业管理员");
ownerRole.setAppCode("MERCHANT");
when(roleMapper.selectById(50L)).thenReturn(ownerRole);
when(auth.summary("internal-token", 40L)).thenReturn(ApiResponse.success(
new AuthAccountClient.AccountSummary(40L, "merchant_owner", "企业管理员", "ACTIVE")));