优化
This commit is contained in:
@@ -10,6 +10,7 @@ import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ResetCustomerOwnerPasswordRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ApprovalResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.CreationResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationResponse;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
@@ -98,15 +99,15 @@ public class PlatformTenantController {
|
||||
|
||||
@PostMapping("/tenants")
|
||||
@PreAuthorize("hasAuthority('platform:customer:create')")
|
||||
@Operation(summary = "Register enterprise tenant / 创建企业客户草稿", description = "Create a PROFILE_REQUIRED merchant draft only. Saving the customer does not create a review ticket. / 仅创建待完善企业客户草稿,保存客户不会创建审核单。")
|
||||
@Operation(summary = "Register enterprise tenant / 创建企业客户草稿", description = "Create a PROFILE_REQUIRED merchant draft, its OWNER account and one-time credentials. Saving the customer does not create a review ticket. / 创建待完善企业客户草稿及 OWNER 账号并返回一次性凭据,保存客户不会创建审核单。")
|
||||
@ApiResponses({
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Registration created / 注册已创建"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "Invalid enterprise data / 企业资料不合法"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域"),
|
||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "409", description = "Enterprise already exists / 企业已存在")
|
||||
})
|
||||
public ApiResponse<TenantResponse> register(@Valid @RequestBody RegisterRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
public ApiResponse<CreationResponse> register(@Valid @RequestBody RegisterRequest request,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.register(request, user.userId()));
|
||||
}
|
||||
|
||||
@@ -109,6 +109,13 @@ public final class TenantDtos {
|
||||
@Schema(description = "One-time temporary password; only returned when created / 一次性初始密码,仅新建时返回", format = "password", nullable = true) String temporaryPassword
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantCreationResponse", description = "Created enterprise draft and one-time administrator credentials / 新建企业草稿及一次性管理员凭据")
|
||||
public record CreationResponse(
|
||||
@Schema(description = "Created enterprise draft / 新建企业草稿") TenantResponse tenant,
|
||||
@Schema(description = "Contact phone used as login username / 使用联系电话作为登录账号") String administratorUsername,
|
||||
@Schema(description = "One-time temporary password / 一次性初始密码", format = "password") String temporaryPassword
|
||||
) { }
|
||||
|
||||
@Schema(name = "TenantResponse", description = "Enterprise tenant summary / 企业租户概要")
|
||||
public record TenantResponse(Long id, String tenantCode, String tenantName, String status,
|
||||
String legalName, String legalRepresentative, String unifiedSocialCreditCode, String contactName,
|
||||
|
||||
@@ -217,7 +217,16 @@ public class RbacService {
|
||||
throw new BizException(ResultCode.CONFLICT, "停用角色不能分配给成员");
|
||||
}
|
||||
}
|
||||
Set<Long> currentRoleIds = new LinkedHashSet<>();
|
||||
for (Long roleId : repository.findMemberRoleIds(tenantId, request.membershipId())) {
|
||||
if (repository.findRole(roleId, tenantId, appCode) != null) currentRoleIds.add(roleId);
|
||||
}
|
||||
repository.replaceMemberRoles(tenantId, appCode, request.membershipId(), request.roleIds());
|
||||
if (!currentRoleIds.equals(request.roleIds())) {
|
||||
var user = memberRepository.findUser(membership.getUserId());
|
||||
if (user == null || user.getAuthUserId() == null) throw sessionRevocationUnavailable();
|
||||
revokeSessions(List.of(user.getAuthUserId()));
|
||||
}
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "ROLE_PERMISSION_GRANT", resourceType = "ROLE",
|
||||
@@ -238,7 +247,10 @@ public class RbacService {
|
||||
}
|
||||
|
||||
private void revokeAssignedSessions(long tenantId, Long roleId) {
|
||||
List<Long> accountIds = repository.findAssignedAuthUserIds(tenantId, roleId);
|
||||
revokeSessions(repository.findAssignedAuthUserIds(tenantId, roleId));
|
||||
}
|
||||
|
||||
private void revokeSessions(List<Long> accountIds) {
|
||||
for (int start = 0; start < accountIds.size(); start += SESSION_REVOCATION_BATCH_SIZE) {
|
||||
Set<Long> batch = Set.copyOf(accountIds.subList(start,
|
||||
Math.min(start + SESSION_REVOCATION_BATCH_SIZE, accountIds.size())));
|
||||
|
||||
+25
-2
@@ -19,6 +19,8 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -48,6 +50,27 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
@Override
|
||||
public String name() { return "tenant-administrator"; }
|
||||
|
||||
/** Creates the owner during platform customer creation and compensates Auth if the IAM transaction rolls back. */
|
||||
public ProvisioningResult provisionPlatformCreatedTenant(IamTenant tenant, long operatorId) {
|
||||
ApprovalContext context = new ApprovalContext(tenant, 0L, operatorId, "platform customer creation");
|
||||
ProvisioningResult result = initialize(context);
|
||||
if (result.created() && TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
if (status != STATUS_ROLLED_BACK) return;
|
||||
try {
|
||||
compensate(context, result);
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("平台创建客户事务回滚后认证账号补偿失败,tenantId={}, authUserId={}",
|
||||
tenant.getId(), result.authUserId(), compensationFailure);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProvisioningResult initialize(ApprovalContext context) {
|
||||
IamTenant tenant = context.tenant();
|
||||
@@ -104,7 +127,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
.eq(IamRole::getStatus, "ACTIVE")
|
||||
.last("LIMIT 1"));
|
||||
if (ownerRole == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "企业管理员角色模板未初始化,不能审核通过");
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "企业管理员角色模板未初始化,不能创建或确认企业管理员");
|
||||
}
|
||||
return ownerRole;
|
||||
}
|
||||
@@ -239,7 +262,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
|
||||
private char pick(char[] source) { return source[random.nextInt(source.length)]; }
|
||||
private BizException unavailable() {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能审核通过");
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能创建或确认企业管理员");
|
||||
}
|
||||
private BizException inconsistentOwner(String message) {
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, message);
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ApprovalResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.CreationResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationResponse;
|
||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
||||
@@ -61,6 +62,8 @@ public class TenantService {
|
||||
private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
@Resource
|
||||
private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource
|
||||
private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_CUSTOMER_OWNER_PASSWORD_RESET", resourceType = "TENANT",
|
||||
@@ -148,7 +151,7 @@ public class TenantService {
|
||||
captureAfter = false
|
||||
)
|
||||
@Transactional
|
||||
public TenantResponse register(RegisterRequest request, long operatorId) {
|
||||
public CreationResponse register(RegisterRequest request, long operatorId) {
|
||||
String contactPhone = normalizeContactPhone(request.contactPhone());
|
||||
ensureContactPhoneAvailable(contactPhone, null);
|
||||
IamTenant tenant = new IamTenant();
|
||||
@@ -177,8 +180,9 @@ public class TenantService {
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
|
||||
|
||||
return response(tenant);
|
||||
return new CreationResponse(response(tenant), administrator.username(), administrator.temporaryPassword());
|
||||
}
|
||||
|
||||
public List<IamTenantReview> pendingReviews() {
|
||||
|
||||
@@ -59,14 +59,14 @@ paths:
|
||||
post:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: Register enterprise tenant / 创建企业客户草稿
|
||||
description: Create a `PROFILE_REQUIRED` enterprise draft only. Saving the customer does not create a review task. / 仅创建待完善企业客户草稿,保存客户不会创建审核单。
|
||||
description: Create a `PROFILE_REQUIRED` enterprise draft and its OWNER account, returning one-time credentials. Saving the customer does not create a review task. / 创建待完善企业客户草稿及 OWNER 账号并返回一次性凭据,保存客户不会创建审核单。
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/RegisterRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'200': {$ref: '#/components/responses/TenantCreationSuccess'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
@@ -822,6 +822,7 @@ components:
|
||||
responses:
|
||||
Success: {description: Operation succeeded / 操作成功, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
TenantSuccess: {description: Enterprise tenant returned / 返回企业客户档案, content: {application/json: {schema: {$ref: '#/components/schemas/TenantApiResponse'}}}}
|
||||
TenantCreationSuccess: {description: Enterprise tenant and one-time owner credentials returned / 返回企业客户及一次性管理员凭据, content: {application/json: {schema: {$ref: '#/components/schemas/TenantCreationApiResponse'}}}}
|
||||
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, customer scope or platform technical scope rejected / 权限、客户范围或平台技术作用域不匹配, content: {application/json: {schema: {$ref: '#/components/schemas/ApiResponse'}}}}
|
||||
@@ -869,6 +870,19 @@ components:
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/TenantApprovalResponse'}
|
||||
TenantCreationApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/TenantCreationResponse'}
|
||||
TenantCreationResponse:
|
||||
type: object
|
||||
required: [tenant, administratorUsername, temporaryPassword]
|
||||
properties:
|
||||
tenant: {$ref: '#/components/schemas/TenantResponse'}
|
||||
administratorUsername: {type: string, example: '13800138000'}
|
||||
temporaryPassword: {type: string, format: password, description: 一次性初始密码,仅本次创建响应返回}
|
||||
TenantApprovalResponse:
|
||||
type: object
|
||||
required: [tenantId, administratorCreated]
|
||||
|
||||
Reference in New Issue
Block a user