优化
This commit is contained in:
+6
-1
@@ -30,7 +30,12 @@ public class CustomerListExportProviderController {
|
||||
|
||||
@PostConstruct
|
||||
void initializeToken() {
|
||||
this.internalToken = internalTokenValue.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] configured = internalTokenValue == null
|
||||
? new byte[0] : internalTokenValue.getBytes(StandardCharsets.UTF_8);
|
||||
if (configured.length < 32) {
|
||||
throw new IllegalStateException("INTERNAL_SERVICE_TOKEN 至少需要 32 字节");
|
||||
}
|
||||
this.internalToken = configured;
|
||||
}
|
||||
|
||||
@PostMapping("/batches")
|
||||
|
||||
@@ -40,7 +40,14 @@ public class InternalTenantController {
|
||||
private byte[] internalToken;
|
||||
|
||||
@PostConstruct
|
||||
void initializeToken() { internalToken = internalTokenValue.getBytes(StandardCharsets.UTF_8); }
|
||||
void initializeToken() {
|
||||
byte[] configured = internalTokenValue == null
|
||||
? new byte[0] : internalTokenValue.getBytes(StandardCharsets.UTF_8);
|
||||
if (configured.length < 32) {
|
||||
throw new IllegalStateException("INTERNAL_SERVICE_TOKEN 至少需要 32 字节");
|
||||
}
|
||||
internalToken = configured;
|
||||
}
|
||||
|
||||
@PostMapping("/merchant-registrations")
|
||||
public ApiResponse<MerchantRegistrationResponse> register(
|
||||
|
||||
@@ -8,8 +8,11 @@ public final class TenantGuard {
|
||||
public static long tenant(AuthenticatedUser user,String expectedApp){
|
||||
if(user==null||user.tenantId()==null) throw new BizException(ResultCode.UNAUTHORIZED,"Authentication required / 请先登录");
|
||||
if(!expectedApp.equals(user.appCode())) throw new BizException(ResultCode.FORBIDDEN,"Wrong application scope / 应用权限域不匹配");
|
||||
if("PLATFORM".equals(expectedApp)) return 0L;
|
||||
if(user.tenantId()<=0) throw new BizException(ResultCode.FORBIDDEN,"Merchant tenant required / 缺少商户租户");
|
||||
if("PLATFORM".equals(expectedApp)) {
|
||||
if(user.tenantId()!=0L) throw new BizException(ResultCode.FORBIDDEN,"Platform tenant must be zero / 平台作用域必须为 0");
|
||||
return 0L;
|
||||
}
|
||||
if(!"MERCHANT".equals(expectedApp)||user.tenantId()<=0) throw new BizException(ResultCode.FORBIDDEN,"Merchant tenant required / 缺少商户租户");
|
||||
return user.tenantId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +199,7 @@ public class TenantService {
|
||||
}
|
||||
|
||||
public TenantResponse profile(long tenantId) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId)) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
IamTenant tenant = requireMerchant(tenantId);
|
||||
return merchantVisibleResponse(tenant);
|
||||
}
|
||||
|
||||
@@ -285,10 +282,7 @@ public class TenantService {
|
||||
@BusinessAudit(event = "TENANT_PROFILE_UPDATE", resourceType = "TENANT",
|
||||
resourceId = "#tenantId", description = "编辑企业资料")
|
||||
public TenantResponse update(long tenantId, UpdateProfileRequest request) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId)) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Tenant not found / 企业不存在");
|
||||
}
|
||||
IamTenant tenant = requireMerchant(tenantId);
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Baiyue IAM API / 百悦身份与权限 API
|
||||
description: Tenant, member, role, menu, page, button and API permission management. / 租户、成员、角色、菜单、页面、按钮和 API 权限管理。
|
||||
description: Customer master data, members, roles, menus, pages, buttons and API permissions for the current single-tenant product. Historical tenant-named paths, schemas and fields are retained for compatibility; tenantId > 0 currently means customer ID, while 0 is technical scope for platform identities, RBAC, and public warehouse resources. / 当前单租户产品的客户主档、成员、角色、菜单、页面、按钮和 API 权限管理。tenant 命名的路径、模型和字段仅为历史兼容;tenantId 大于 0 当前表示客户 ID,0 表示平台身份、RBAC 和公共仓储资源技术作用域。
|
||||
version: 1.0.1
|
||||
servers:
|
||||
- url: /
|
||||
@@ -9,9 +9,9 @@ security:
|
||||
- bearerAuth: []
|
||||
tags:
|
||||
- name: Platform Tenant / 平台租户
|
||||
description: Enterprise registration and review / 企业注册与审核
|
||||
description: Historical tag for platform customer administration; it does not represent a real tenant layer. / 历史兼容标签,实际用于平台客户管理,不表示真实租户层。
|
||||
- name: Merchant Tenant / 商户企业
|
||||
description: Current tenant profile and members / 当前企业资料与成员
|
||||
description: Current customer profile and members; tenant naming is retained for compatibility. / 当前客户企业资料与成员;tenant 命名仅为兼容。
|
||||
- name: IAM Resources / IAM 资源
|
||||
description: Internal users, addresses and qualifications / 内部用户、地址与资质
|
||||
- name: RBAC
|
||||
@@ -237,7 +237,7 @@ paths:
|
||||
/api/platform/v1/iam/users/options:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询平台租户和 PLATFORM 角色选项
|
||||
summary: 查询平台技术作用域和 PLATFORM 角色选项
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
@@ -363,7 +363,7 @@ paths:
|
||||
put:
|
||||
tags: [IAM 资源]
|
||||
summary: 全量分配平台内部用户角色
|
||||
description: 仅允许分配平台租户下已启用的平台端角色;空集合表示清空角色。
|
||||
description: 仅允许分配平台技术作用域(tenant_id=0)下已启用的平台端角色;空集合表示清空角色。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -382,7 +382,7 @@ paths:
|
||||
get:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: Get current tenant profile / 查询当前企业资料
|
||||
description: Tenant ID comes only from the verified JWT. / 租户 ID 仅取自已验证 JWT。
|
||||
description: The compatibility field tenant ID comes only from the verified JWT and currently identifies the customer. / 客户隔离兼容字段 tenant ID 仅取自已验证 JWT,当前表示登录客户 ID。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'401': {$ref: '#/components/responses/Unauthorized'}
|
||||
@@ -432,7 +432,7 @@ paths:
|
||||
get:
|
||||
tags: [Merchant Tenant / 商户企业]
|
||||
summary: 查询成员详情及当前角色
|
||||
description: 返回当前租户成员资料,roleIds 为服务端实时查询的当前角色集合;跨租户成员按不存在处理。
|
||||
description: 返回当前客户成员资料,roleIds 为服务端实时查询的当前角色集合;跨客户成员按不存在处理。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
@@ -493,7 +493,7 @@ paths:
|
||||
get:
|
||||
tags: [RBAC]
|
||||
summary: List roles / 查询角色
|
||||
description: List roles in the selected application and tenant scope. / 查询指定应用及租户范围内的角色。
|
||||
description: List roles in the selected application and current customer or platform technical scope. / 查询指定应用及当前客户范围或平台技术作用域内的角色。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
@@ -553,7 +553,7 @@ paths:
|
||||
put:
|
||||
tags: [RBAC]
|
||||
summary: Grant permissions to role / 设置角色权限
|
||||
description: Replace role permissions; cross-tenant and cross-application grants are rejected. / 替换角色权限,拒绝跨租户和跨应用授权。
|
||||
description: Replace role permissions; cross-customer and cross-application grants are rejected. / 替换角色权限,拒绝跨客户和跨应用授权。
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/App'}
|
||||
requestBody:
|
||||
@@ -719,7 +719,7 @@ components:
|
||||
name: tenantId
|
||||
in: path
|
||||
required: true
|
||||
description: 企业客户租户 ID
|
||||
description: 企业客户 ID;tenantId 为历史兼容参数名
|
||||
schema: {type: string, pattern: '^\d+$'}
|
||||
PermissionId:
|
||||
name: permissionId
|
||||
@@ -732,7 +732,7 @@ components:
|
||||
TenantSuccess: {description: Enterprise tenant returned / 返回企业客户档案, content: {application/json: {schema: {$ref: '#/components/schemas/TenantApiResponse'}}}}
|
||||
BadRequest: {description: Invalid request / 请求参数错误, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Unauthorized: {description: Authentication required / 需要登录, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Forbidden: {description: Permission or tenant scope rejected / 权限或租户范围不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
Forbidden: {description: Permission, customer scope or platform technical 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'}}}}
|
||||
@@ -977,7 +977,7 @@ components:
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, pattern: '^1\\d{10}$'}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台租户 0}
|
||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
||||
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
||||
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
|
||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||
@@ -988,7 +988,7 @@ components:
|
||||
displayName: {type: string, maxLength: 64}
|
||||
phone: {type: string, pattern: '^1\\d{10}$'}
|
||||
email: {type: string, format: email, maxLength: 128}
|
||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台租户 0}
|
||||
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
|
||||
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
|
||||
CreateInternalUserRequest:
|
||||
type: object
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(CustomerListExportProviderController.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=test-internal-service-token-at-least-32-bytes")
|
||||
class CustomerListExportProviderControllerTest {
|
||||
@MockBean private CustomerListExportProviderService service;
|
||||
@Resource private CustomerListExportProviderController controller;
|
||||
@@ -48,7 +48,7 @@ class CustomerListExportProviderControllerTest {
|
||||
ExportBatchResponse batch = new ExportBatchResponse(1, "sha256:test", "snapshot",
|
||||
List.of(), List.of(), null, false, 0L);
|
||||
when(service.exportBatch(request)).thenReturn(batch);
|
||||
var response = controller.batch("internal-token", request);
|
||||
var response = controller.batch("test-internal-service-token-at-least-32-bytes", request);
|
||||
|
||||
assertEquals(ResultCode.SUCCESS.code(), response.code());
|
||||
assertEquals(batch, response.data());
|
||||
|
||||
@@ -21,7 +21,7 @@ import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(InternalTenantController.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=test-internal-service-token-at-least-32-bytes")
|
||||
class InternalCustomerExportContractTest {
|
||||
@MockBean private MerchantOnboardingService onboardingService;
|
||||
@MockBean private ProfileResourceService profileResourceService;
|
||||
@@ -36,7 +36,7 @@ class InternalCustomerExportContractTest {
|
||||
|
||||
when(service.exportMerchants(null, 100, "柏悦", "ACTIVE"))
|
||||
.thenReturn(new CustomerExportBatch(List.of(), null, false));
|
||||
var response = controller.exportCustomers("internal-token", null, 100, "柏悦", "ACTIVE");
|
||||
var response = controller.exportCustomers("test-internal-service-token-at-least-32-bytes", null, 100, "柏悦", "ACTIVE");
|
||||
assertEquals(ResultCode.SUCCESS.code(), response.code());
|
||||
verify(service).exportMerchants(null, 100, "柏悦", "ACTIVE");
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class InternalServiceTokenConfigurationTest {
|
||||
@Test
|
||||
void internalTenantControllerRejectsEmptyAndShortTokens() {
|
||||
assertInternalTenantTokenRejected(null);
|
||||
assertInternalTenantTokenRejected("");
|
||||
assertInternalTenantTokenRejected("short-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
void customerExportControllerRejectsEmptyAndShortTokens() {
|
||||
assertCustomerExportTokenRejected(null);
|
||||
assertCustomerExportTokenRejected("");
|
||||
assertCustomerExportTokenRejected("short-token");
|
||||
}
|
||||
|
||||
private void assertInternalTenantTokenRejected(String token) {
|
||||
InternalTenantController controller = new InternalTenantController();
|
||||
ReflectionTestUtils.setField(controller, "internalTokenValue", token);
|
||||
assertThrows(IllegalStateException.class, controller::initializeToken);
|
||||
}
|
||||
|
||||
private void assertCustomerExportTokenRejected(String token) {
|
||||
CustomerListExportProviderController controller = new CustomerListExportProviderController();
|
||||
ReflectionTestUtils.setField(controller, "internalTokenValue", token);
|
||||
assertThrows(IllegalStateException.class, controller::initializeToken);
|
||||
}
|
||||
}
|
||||
+7
-16
@@ -51,7 +51,7 @@ class CustomerProfileMigrationContractTest {
|
||||
@Test
|
||||
void shouldHandleCase03() throws IOException {
|
||||
Path repositoryRoot = Path.of(System.getProperty("maven.multiModuleProjectDirectory", ".."));
|
||||
Path migration = repositoryRoot.resolve("deploy/sql/2026-07-31一期回归测试环境增量迁移.sql");
|
||||
Path migration = repositoryRoot.resolve("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
Path readme = repositoryRoot.resolve("deploy/sql/README.md");
|
||||
|
||||
assertThat(migration).exists();
|
||||
@@ -60,27 +60,18 @@ class CustomerProfileMigrationContractTest {
|
||||
String instructions = Files.readString(readme, StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CALL regression_assert_table('iam_tenant')")
|
||||
.contains("CALL regression_assert_table('iam_tenant_review')")
|
||||
.contains("CALL regression_assert_table('iam_tenant_qualification')")
|
||||
.contains("ALTER TABLE iam_tenant MODIFY COLUMN status")
|
||||
.contains("DEFAULT 'PROFILE_REQUIRED'")
|
||||
.contains("CREATE PROCEDURE regression_add_iam_column")
|
||||
.contains("regression_add_iam_column('approved_at'")
|
||||
.contains("regression_add_iam_column('approved_by'")
|
||||
.contains("regression_add_iam_column('reject_reason'")
|
||||
.contains("regression_add_iam_related_column('iam_tenant_qualification', 'review_comment'")
|
||||
.contains("'IAM注册审核字段'")
|
||||
.contains("'IAM资质与成员审计字段'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_at'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'approved_by'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant', 'reject_reason'")
|
||||
.contains("CALL phase1_iam_add_column('iam_tenant_qualification', 'review_comment'")
|
||||
.contains("platform:customer:view")
|
||||
.contains("platform:customer:export")
|
||||
.contains("platform:download:list")
|
||||
.contains("'MERCHANT_OWNER'")
|
||||
.contains("IAM全局企业管理员模板")
|
||||
.contains("owner_role.id, permission.id, 0")
|
||||
.contains("'IAM客户档案字段' AS check_item")
|
||||
.contains("10 AS expected_count")
|
||||
.contains("47 AS expected_count")
|
||||
.contains("MIGRATION_COMPLETED");
|
||||
.contains("统一增量全部执行完成");
|
||||
assertThat(instructions)
|
||||
.contains("phase1-platform-master-data-and-permission-management.sql")
|
||||
.contains("phase1-customer-profile.sql")
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class TenantGuardTest {
|
||||
@Test
|
||||
void acceptsPlatformOnlyAtZeroTenant() {
|
||||
assertEquals(0L, TenantGuard.tenant(user(0L, "PLATFORM"), "PLATFORM"));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> TenantGuard.tenant(user(10001L, "PLATFORM"), "PLATFORM"));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsMerchantOnlyAtPositiveCustomerTenant() {
|
||||
assertEquals(10001L, TenantGuard.tenant(user(10001L, "MERCHANT"), "MERCHANT"));
|
||||
|
||||
BizException exception = assertThrows(BizException.class,
|
||||
() -> TenantGuard.tenant(user(0L, "MERCHANT"), "MERCHANT"));
|
||||
|
||||
assertEquals(ResultCode.FORBIDDEN, exception.getResultCode());
|
||||
}
|
||||
|
||||
private AuthenticatedUser user(long tenantId, String appCode) {
|
||||
return new AuthenticatedUser(42L, tenantId, tenantId, 9L,
|
||||
"operator", appCode, appCode, Set.of());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.RegisterRequest;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
@@ -122,6 +123,23 @@ class TenantServiceTest {
|
||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void merchantProfileCannotReadOrUpdatePlatformTenant() {
|
||||
IamTenant platform = new IamTenant();
|
||||
platform.setId(0L);
|
||||
platform.setTenantType("PLATFORM");
|
||||
when(repository.findTenant(0L)).thenReturn(platform);
|
||||
|
||||
BizException readFailure = assertThrows(BizException.class, () -> service.profile(0L));
|
||||
BizException updateFailure = assertThrows(BizException.class, () -> service.update(0L,
|
||||
new UpdateProfileRequest("Platform", "Platform", "Owner", "Contact",
|
||||
"13800138000", "platform@example.com")));
|
||||
|
||||
assertEquals(ResultCode.NOT_FOUND, readFailure.getResultCode());
|
||||
assertEquals(ResultCode.NOT_FOUND, updateFailure.getResultCode());
|
||||
verify(repository, never()).saveTenant(platform);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRegistrationCreatesDraftWithoutCreatingReviewTicket() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user