暂存提交
This commit is contained in:
@@ -16,6 +16,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -39,15 +40,18 @@ public class PlatformTenantController {
|
||||
@PreAuthorize("hasAuthority('platform:customer:list')")
|
||||
@Operation(summary = "List enterprise tenants / 查询企业列表", description = "Returns paged merchant enterprise tenants for platform operators. / 平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Tenant list returned / 企业列表查询成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid paging parameters / 分页参数无效"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限")
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "企业列表查询成功"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "分页参数类型无效"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "401", description = "需要登录"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "需要平台端权限"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "422", description = "分页范围或状态无效")
|
||||
})
|
||||
public ApiResponse<PageResult<TenantResponse>> page(
|
||||
@RequestParam(defaultValue = "1") @Min(1) long page,
|
||||
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false) String status,
|
||||
@RequestParam(required = false)
|
||||
@Pattern(regexp = "PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED") String status,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.pageMerchants(page, size, keyword, status));
|
||||
|
||||
@@ -55,7 +55,7 @@ public interface AuthorizationQueryMapper {
|
||||
AND (r.tenant_id = 0 OR r.tenant_id = #{tenantId})
|
||||
AND r.app_code = #{appCode} AND r.status = 'ACTIVE' AND r.deleted = 0
|
||||
AND (p.tenant_id = 0 OR p.tenant_id = #{tenantId})
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE'
|
||||
AND p.app_code = #{appCode} AND p.status = 'ACTIVE' AND p.deleted = 0
|
||||
ORDER BY p.permission_code
|
||||
""")
|
||||
List<String> findPermissions(@Param("userId") long userId,
|
||||
|
||||
@@ -6,10 +6,12 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotResponse;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
@@ -24,6 +26,7 @@ public class AuthorizationSnapshotService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public SnapshotResponse load(SnapshotRequest request) {
|
||||
requireRequest(request);
|
||||
String appCode = request.getAppCode().trim().toUpperCase(java.util.Locale.ROOT);
|
||||
if (!APPS.contains(appCode)) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "未知的应用身份域");
|
||||
@@ -38,11 +41,40 @@ public class AuthorizationSnapshotService {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "IAM 用户或成员身份无效");
|
||||
}
|
||||
|
||||
Set<String> roles = new LinkedHashSet<>(mapper.findRoles(
|
||||
Set<String> roles = normalizedSet(mapper.findRoles(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
Set<String> permissions = new LinkedHashSet<>(mapper.findPermissions(
|
||||
Set<String> permissions = normalizedSet(mapper.findPermissions(
|
||||
request.getUserId(), request.getTenantId(), request.getMembershipId(), appCode));
|
||||
return new SnapshotResponse(request.getUserId(), request.getTenantId(), request.getMembershipId(),
|
||||
appCode, appCode, Set.copyOf(roles), Set.copyOf(permissions));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据已认证请求主体重新计算有效权限,而不是复用 JWT 中可能已经过期的权限声明。
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Set<String> currentPermissions(AuthenticatedUser user, String appCode) {
|
||||
if (user == null || user.userId() == null || user.tenantId() == null || user.membershipId() == null) {
|
||||
throw new BizException(ResultCode.UNAUTHORIZED, "Authentication identity is incomplete / 登录身份不完整");
|
||||
}
|
||||
return load(new SnapshotRequest(user.userId(), user.tenantId(), user.membershipId(), appCode))
|
||||
.getPermissions();
|
||||
}
|
||||
|
||||
private void requireRequest(SnapshotRequest request) {
|
||||
if (request == null || request.getUserId() == null || request.getTenantId() == null
|
||||
|| request.getMembershipId() == null || request.getAppCode() == null
|
||||
|| request.getAppCode().isBlank()) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "Authorization identity is incomplete / 授权身份不完整");
|
||||
}
|
||||
}
|
||||
|
||||
private Set<String> normalizedSet(List<String> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return Set.of();
|
||||
}
|
||||
LinkedHashSet<String> normalized = new LinkedHashSet<>();
|
||||
values.stream().filter(java.util.Objects::nonNull).forEach(normalized::add);
|
||||
return Set.copyOf(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ VALUES
|
||||
(5101020, 0, 'PLATFORM', 'MENU', 5101000, 'platform:role:list', '角色管理', '/system/roles', NULL, 20, 'ACTIVE', 0),
|
||||
(5101030, 0, 'PLATFORM', 'MENU', 5101000, 'platform:permission:list', '菜单按钮', '/system/permissions', NULL, 30, 'ACTIVE', 0),
|
||||
(5102010, 0, 'PLATFORM', 'MENU', 5102000, 'platform:order:list', '订单列表', '/first-leg-orders/list', NULL, 10, 'ACTIVE', 0),
|
||||
(5102020, 0, 'PLATFORM', 'MENU', 5102000, 'platform:firstleg:task:list', '履约任务', '/first-leg-tasks/list', NULL, 20, 'ACTIVE', 0),
|
||||
(5103010, 0, 'PLATFORM', 'MENU', 5103000, 'platform:warehouse:list', '国内仓/FBA仓', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5103020, 0, 'PLATFORM', 'MENU', 5103000, 'platform:express:list', '快递公司', '/master-data/express', NULL, 20, 'ACTIVE', 0),
|
||||
(5103030, 0, 'PLATFORM', 'MENU', 5103000, 'platform:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 30, 'ACTIVE', 0),
|
||||
@@ -103,9 +104,18 @@ VALUES
|
||||
(5102016, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:cancel', '取消订单', NULL, NULL, 60, 'ACTIVE', 0),
|
||||
(5102017, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:note', '内部备注', NULL, NULL, 70, 'ACTIVE', 0),
|
||||
(5102018, 0, 'PLATFORM', 'BUTTON', 5102010, 'platform:order:fulfillment', '登记履约快递', NULL, NULL, 80, 'ACTIVE', 0),
|
||||
(5102021, 0, 'PLATFORM', 'BUTTON', 5102020, 'platform:firstleg:task:view', '查看履约任务', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5102022, 0, 'PLATFORM', 'BUTTON', 5102020, 'platform:firstleg:assignment:history', '查看分配历史', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5102023, 0, 'PLATFORM', 'BUTTON', 5102020, 'platform:firstleg:assignment:assign', '分配履约任务', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5103011, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:create', '新增仓库', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103012, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:edit', '编辑仓库', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103013, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:status', '启用停用', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
(5103013, 0, 'PLATFORM', 'BUTTON', 5103010, 'platform:warehouse:status', '启用停用', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5103021, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:create', '新增快递公司', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103022, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:edit', '编辑快递公司', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103023, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:status', '启用停用快递公司', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
(5103031, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:create', '新增往来单位', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5103032, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:edit', '编辑往来单位', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启用停用往来单位', NULL, NULL, 30, 'ACTIVE', 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_type = VALUES(permission_type), parent_id = VALUES(parent_id),
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path),
|
||||
@@ -168,7 +178,7 @@ ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000003, id, 0 FROM iam_permission
|
||||
WHERE permission_code LIKE 'platform:order%'
|
||||
WHERE permission_code LIKE 'platform:order%' OR permission_code LIKE 'platform:firstleg%'
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
|
||||
@@ -18,6 +18,25 @@ tags:
|
||||
description: Platform and merchant role-permission management / 平台端与商户端角色权限管理
|
||||
paths:
|
||||
/api/platform/v1/iam/tenants:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: List enterprise tenants / 查询企业客户列表
|
||||
description: Returns paged merchant tenants. Requires `platform:customer:list`. / 分页查询商户企业客户,需要 `platform:customer:list` 权限。
|
||||
parameters:
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
- {name: keyword, in: query, required: false, schema: {type: string}, description: 企业编码、名称、信用代码或联系人关键字}
|
||||
- {name: status, in: query, required: false, schema: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED]}}
|
||||
responses:
|
||||
'200':
|
||||
description: 企业客户分页查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantPageApiResponse'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'422': {$ref: '#/components/responses/UnprocessableEntity'}
|
||||
post:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: Register enterprise tenant / 创建企业租户
|
||||
@@ -32,6 +51,32 @@ paths:
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/platform/v1/iam/authorization/permissions:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Get current effective permissions / 查询当前用户有效权限
|
||||
description: Returns the current active platform membership's permission codes. It only requires a valid PLATFORM JWT and does not require `platform:role:list`. / 返回当前有效平台成员权限编码;只要求有效 PLATFORM JWT,不依赖 `platform:role:list`。
|
||||
responses:
|
||||
'200':
|
||||
description: 当前平台成员权限查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/PermissionCodesApiResponse'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/merchant/v1/iam/authorization/permissions:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Get current effective permissions / 查询当前用户有效权限
|
||||
description: Returns the current active merchant membership's permission codes. / 返回当前有效商户成员权限编码。
|
||||
responses:
|
||||
'200':
|
||||
description: 当前商户成员权限查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/PermissionCodesApiResponse'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/reviews:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
@@ -79,7 +124,11 @@ paths:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询平台内部用户
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200':
|
||||
description: 平台内部用户查询成功,公开主键为 membershipId 并回显 roleIds
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/InternalUserListApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -326,6 +375,7 @@ components:
|
||||
Forbidden: {description: Permission or tenant scope rejected / 权限或租户范围不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
NotFound: {description: Resource not found / 资源不存在, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Conflict: {description: Resource conflict / 资源冲突, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
UnprocessableEntity: {description: 参数校验失败, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
schemas:
|
||||
ApiResponse:
|
||||
type: object
|
||||
@@ -337,6 +387,48 @@ components:
|
||||
requestId: {type: string}
|
||||
traceId: {type: string}
|
||||
timestamp: {type: string, format: date-time}
|
||||
PermissionCodesApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
uniqueItems: true
|
||||
items: {type: string}
|
||||
TenantPageApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required: [records, page, size, total, pages]
|
||||
properties:
|
||||
records: {type: array, items: {$ref: '#/components/schemas/TenantResponse'}}
|
||||
page: {type: integer, format: int64, minimum: 1}
|
||||
size: {type: integer, format: int64, minimum: 1}
|
||||
total: {type: integer, format: int64, minimum: 0}
|
||||
pages: {type: integer, format: int64, minimum: 0}
|
||||
InternalUserListApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {type: array, items: {$ref: '#/components/schemas/InternalUserResponse'}}
|
||||
TenantResponse:
|
||||
type: object
|
||||
required: [id, tenantCode, tenantName, status]
|
||||
properties:
|
||||
id: {type: integer, format: int64}
|
||||
tenantCode: {type: string}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED]}
|
||||
legalName: {type: string}
|
||||
unifiedSocialCreditCode: {type: string}
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string}
|
||||
contactEmail: {type: string, format: email}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, unifiedSocialCreditCode, contactName, contactPhone]
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.iam.service;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.AuthorizationDtos.SnapshotRequest;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.mapper.AuthorizationQueryMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -56,6 +57,43 @@ class AuthorizationSnapshotServiceTest {
|
||||
verify(mapper).countActiveIdentity(42L, 7L, 9L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadsCurrentPlatformPermissionsWithoutRoleManagementPermission() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(List.of("ORDER_OPERATOR"));
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM"))
|
||||
.thenReturn(List.of("platform:order:list"));
|
||||
|
||||
var result = service(mapper).currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
|
||||
assertThat(result).containsExactly("platform:order:list");
|
||||
verify(mapper).findPermissions(42L, 0L, 9L, "PLATFORM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsIncompleteCurrentIdentityAsBusinessError() {
|
||||
assertThatThrownBy(() -> service(mock(AuthorizationQueryMapper.class))
|
||||
.currentPermissions(new AuthenticatedUser(42L, 0L, 0L, null,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM"))
|
||||
.isInstanceOfSatisfying(BizException.class,
|
||||
exception -> assertThat(exception.getResultCode()).isEqualTo(ResultCode.UNAUTHORIZED));
|
||||
}
|
||||
|
||||
@Test
|
||||
void 空角色和权限查询结果不应触发五百错误() {
|
||||
AuthorizationQueryMapper mapper = mock(AuthorizationQueryMapper.class);
|
||||
when(mapper.countActiveIdentity(42L, 0L, 9L)).thenReturn(1);
|
||||
when(mapper.findRoles(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
when(mapper.findPermissions(42L, 0L, 9L, "PLATFORM")).thenReturn(null);
|
||||
|
||||
var result = service(mapper).currentPermissions(new AuthenticatedUser(42L, 0L, 0L, 9L,
|
||||
"operator", "PLATFORM", "PLATFORM", java.util.Set.of()), "PLATFORM");
|
||||
|
||||
assertThat(result).isEmpty();
|
||||
}
|
||||
|
||||
private AuthorizationSnapshotService service(AuthorizationQueryMapper mapper) {
|
||||
AuthorizationSnapshotService service = new AuthorizationSnapshotService();
|
||||
ReflectionTestUtils.setField(service, "mapper", mapper);
|
||||
|
||||
Reference in New Issue
Block a user