优化
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/** Firstleg internal contract for provisioning one customer dimension rule. */
|
||||
@FeignClient(name = "firstleg-service", contextId = "customerDimensionRuleClient",
|
||||
url = "${baiyue.clients.firstleg.url:}",
|
||||
path = "/internal/firstleg/v1/customer-dimension-configs")
|
||||
public interface CustomerDimensionRuleClient {
|
||||
@PostMapping("/defaults")
|
||||
ApiResponse<InitializationResponse> initialize(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody InitializationRequest request);
|
||||
|
||||
@DeleteMapping("/defaults")
|
||||
ApiResponse<Void> compensate(
|
||||
@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam long tenantId,
|
||||
@RequestParam String initializationKey);
|
||||
|
||||
record InitializationRequest(long tenantId, long operatorId, String initializationKey) { }
|
||||
record InitializationResponse(Long configId, boolean createdByThisInitialization) { }
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.client.CustomerDimensionRuleClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
/** Provisions the customer dimension rule and compensates it when local customer creation rolls back. */
|
||||
@Service
|
||||
public class CustomerDimensionRuleInitializationService {
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomerDimensionRuleInitializationService.class);
|
||||
private static final int MAX_ATTEMPTS = 3;
|
||||
private static final String KEY_PREFIX = "iam-customer-create:";
|
||||
|
||||
@Resource
|
||||
private CustomerDimensionRuleClient client;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public InitializationResult initialize(long tenantId, long operatorId) {
|
||||
String initializationKey = initializationKey(tenantId);
|
||||
RuntimeException lastFailure = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
ApiResponse<CustomerDimensionRuleClient.InitializationResponse> response = client.initialize(
|
||||
internalToken,
|
||||
new CustomerDimensionRuleClient.InitializationRequest(
|
||||
tenantId, operatorId, initializationKey));
|
||||
if (response != null && response.successful() && response.data() != null) {
|
||||
InitializationResult result = new InitializationResult(
|
||||
response.data().createdByThisInitialization(), initializationKey);
|
||||
registerRollbackCompensation(tenantId, result);
|
||||
return result;
|
||||
}
|
||||
lastFailure = unavailable(response);
|
||||
log.warn("客户尺寸规则初始化失败,tenantId={}, attempt={}, code={}, requestId={}, traceId={}",
|
||||
tenantId, attempt, response == null ? null : response.code(),
|
||||
response == null ? null : response.requestId(),
|
||||
response == null ? null : response.traceId());
|
||||
} catch (RuntimeException failure) {
|
||||
lastFailure = failure;
|
||||
log.warn("客户尺寸规则初始化调用异常,tenantId={}, attempt={}", tenantId, attempt, failure);
|
||||
}
|
||||
}
|
||||
compensateAfterUncertainInitialization(tenantId, initializationKey);
|
||||
throw unavailable(lastFailure);
|
||||
}
|
||||
|
||||
/** Used when Auth compensates a committed client registration. */
|
||||
public void compensateRegistration(long tenantId) {
|
||||
compensateRequired(tenantId, initializationKey(tenantId));
|
||||
}
|
||||
|
||||
private void registerRollbackCompensation(long tenantId, InitializationResult result) {
|
||||
if (!result.created() || !TransactionSynchronizationManager.isSynchronizationActive()) return;
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
if (status != STATUS_ROLLED_BACK) return;
|
||||
try {
|
||||
compensateRequired(tenantId, result.initializationKey());
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("客户创建事务回滚后尺寸规则补偿失败,tenantId={}, initializationKey={}",
|
||||
tenantId, result.initializationKey(), compensationFailure);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void compensateAfterUncertainInitialization(long tenantId, String initializationKey) {
|
||||
try {
|
||||
compensateRequired(tenantId, initializationKey);
|
||||
} catch (RuntimeException compensationFailure) {
|
||||
log.error("客户尺寸规则初始化结果不确定且补偿失败,tenantId={}, initializationKey={}",
|
||||
tenantId, initializationKey, compensationFailure);
|
||||
}
|
||||
}
|
||||
|
||||
private void compensateRequired(long tenantId, String initializationKey) {
|
||||
RuntimeException lastFailure = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
ApiResponse<Void> response = client.compensate(internalToken, tenantId, initializationKey);
|
||||
if (response != null && response.successful()) return;
|
||||
lastFailure = unavailable(response);
|
||||
} catch (RuntimeException failure) {
|
||||
lastFailure = failure;
|
||||
}
|
||||
}
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"客户尺寸规则补偿失败,请根据客户ID修复数据一致性: " + tenantId);
|
||||
}
|
||||
|
||||
private static BizException unavailable(ApiResponse<?> response) {
|
||||
String detail = response == null || response.message() == null || response.message().isBlank()
|
||||
? "Firstleg 服务未返回有效响应" : response.message();
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "默认尺寸规则初始化失败: " + detail);
|
||||
}
|
||||
|
||||
private static BizException unavailable(RuntimeException cause) {
|
||||
String detail = cause == null || cause.getMessage() == null || cause.getMessage().isBlank()
|
||||
? "Firstleg 服务暂时不可用" : cause.getMessage();
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"默认尺寸规则初始化失败,已重试 3 次: " + detail);
|
||||
}
|
||||
|
||||
private static String initializationKey(long tenantId) {
|
||||
return KEY_PREFIX + tenantId;
|
||||
}
|
||||
|
||||
public record InitializationResult(boolean created, String initializationKey) { }
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private IamTenantAddressMapper addressMapper;
|
||||
@Resource private IamTenantQualificationMapper qualificationMapper;
|
||||
@Resource private IamTenantReviewMapper reviewMapper;
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
@@ -118,6 +119,7 @@ public class MerchantOnboardingService {
|
||||
address.setIsDefault(true);
|
||||
addressMapper.insert(address);
|
||||
}
|
||||
dimensionRuleInitialization.initialize(tenantId, request.authUserId());
|
||||
return new MerchantRegistrationResponse(tenantId, membershipId, tenant.getStatus());
|
||||
}
|
||||
|
||||
@@ -159,6 +161,8 @@ public class MerchantOnboardingService {
|
||||
"IAM registration compensation is unsafe: onboarding has progressed");
|
||||
}
|
||||
|
||||
dimensionRuleInitialization.compensateRegistration(tenant.getId());
|
||||
|
||||
membershipRoleMapper.delete(new LambdaQueryWrapper<IamMembershipRole>()
|
||||
.eq(IamMembershipRole::getMembershipId, membership.getId()));
|
||||
membershipMapper.deleteById(membership.getId());
|
||||
|
||||
@@ -50,6 +50,8 @@ public class TenantService {
|
||||
private String internalToken;
|
||||
@Resource
|
||||
private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
@Resource
|
||||
private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
|
||||
|
||||
@BusinessAudit(
|
||||
@@ -88,6 +90,7 @@ public class TenantService {
|
||||
tenant.setCreatedBy(operatorId);
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
|
||||
return response(tenant);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- 客户尺寸计算配置权限;仅用于平台后台查看和维护,不控制内部初始化接口。
|
||||
USE baiyue_iam;
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
DROP PROCEDURE IF EXISTS migrate_customer_dimension_config_permissions;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE migrate_customer_dimension_config_permissions()
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM iam_permission
|
||||
WHERE (id = 5102111 AND permission_code <> 'firstleg:dimension-config:view')
|
||||
OR (id = 5102112 AND permission_code <> 'firstleg:dimension-config:edit')
|
||||
OR (permission_code = 'firstleg:dimension-config:view' AND id <> 5102111)
|
||||
OR (permission_code = 'firstleg:dimension-config:edit' AND id <> 5102112)
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = 'customer dimension config permission id/code conflict';
|
||||
END IF;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code,
|
||||
permission_name, route_path, http_method, sort_no, status, built_in, deleted)
|
||||
VALUES
|
||||
(5102111, 0, 'PLATFORM', 'BUTTON', 5100010,
|
||||
'firstleg:dimension-config:view', '查看客户尺寸计算配置', NULL, NULL, 90, 'ACTIVE', 1, 0),
|
||||
(5102112, 0, 'PLATFORM', 'BUTTON', 5100010,
|
||||
'firstleg:dimension-config:edit', '维护客户尺寸计算配置', NULL, NULL, 100, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name = VALUES(permission_name), parent_id = VALUES(parent_id),
|
||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
||||
|
||||
INSERT IGNORE INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT role.id, permission.id, role.tenant_id
|
||||
FROM iam_role role
|
||||
JOIN iam_permission permission
|
||||
ON permission.tenant_id = 0
|
||||
AND permission.app_code = 'PLATFORM'
|
||||
WHERE role.tenant_id = 0
|
||||
AND role.app_code = 'PLATFORM'
|
||||
AND (
|
||||
(role.role_code = 'PLATFORM_ADMIN'
|
||||
AND permission.permission_code IN (
|
||||
'firstleg:dimension-config:view', 'firstleg:dimension-config:edit'))
|
||||
OR (role.role_code = 'ORDER_OPERATOR'
|
||||
AND permission.permission_code = 'firstleg:dimension-config:view')
|
||||
);
|
||||
END$$
|
||||
DELIMITER ;
|
||||
|
||||
CALL migrate_customer_dimension_config_permissions();
|
||||
DROP PROCEDURE IF EXISTS migrate_customer_dimension_config_permissions;
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.iam.client;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerDimensionRuleClientContractTest {
|
||||
@Test
|
||||
void usesTheFirstlegInternalDimensionConfigContract() throws Exception {
|
||||
FeignClient feign = CustomerDimensionRuleClient.class.getAnnotation(FeignClient.class);
|
||||
PostMapping initialize = CustomerDimensionRuleClient.class
|
||||
.getMethod("initialize", String.class,
|
||||
CustomerDimensionRuleClient.InitializationRequest.class)
|
||||
.getAnnotation(PostMapping.class);
|
||||
DeleteMapping compensate = CustomerDimensionRuleClient.class
|
||||
.getMethod("compensate", String.class, long.class, String.class)
|
||||
.getAnnotation(DeleteMapping.class);
|
||||
|
||||
assertThat(feign.name()).isEqualTo("firstleg-service");
|
||||
assertThat(feign.path()).isEqualTo("/internal/firstleg/v1/customer-dimension-configs");
|
||||
assertThat(initialize.value()).containsExactly("/defaults");
|
||||
assertThat(compensate.value()).containsExactly("/defaults");
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerDimensionConfigPermissionSqlContractTest {
|
||||
@Test
|
||||
void declaresIndependentPlatformViewAndEditPermissions() throws Exception {
|
||||
String sql = new ClassPathResource("db/customer-dimension-config-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("(5102112, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("'firstleg:dimension-config:view'")
|
||||
.contains("'firstleg:dimension-config:edit'")
|
||||
.contains("SIGNAL SQLSTATE '45000'")
|
||||
.contains("id = 5102111 AND permission_code <> 'firstleg:dimension-config:view'")
|
||||
.contains("permission_code = 'firstleg:dimension-config:edit' AND id <> 5102112");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolvesRoleGrantsByBusinessKeys() throws Exception {
|
||||
String sql = new ClassPathResource("db/customer-dimension-config-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("SELECT role.id, permission.id, role.tenant_id")
|
||||
.contains("role.role_code = 'PLATFORM_ADMIN'")
|
||||
.contains("permission.permission_code IN (")
|
||||
.contains("role.role_code = 'ORDER_OPERATOR'")
|
||||
.contains("permission.permission_code = 'firstleg:dimension-config:view'")
|
||||
.doesNotContain("SELECT 4000001")
|
||||
.doesNotContain("SELECT 4000003");
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.client.CustomerDimensionRuleClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(CustomerDimensionRuleInitializationService.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
class CustomerDimensionRuleInitializationServiceTest {
|
||||
@MockBean private CustomerDimensionRuleClient client;
|
||||
@Resource private CustomerDimensionRuleInitializationService service;
|
||||
|
||||
@AfterEach
|
||||
void clearSynchronization() {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) {
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void retriesWithOneStableKeyAndReturnsTheIdempotentResult() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenThrow(new RuntimeException("timeout"))
|
||||
.thenReturn(ApiResponse.failure(ResultCode.SERVICE_UNAVAILABLE,
|
||||
"temporarily unavailable", "request-1", "trace-1"))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
|
||||
|
||||
var result = service.initialize(10001L, 9001L);
|
||||
|
||||
assertFalse(result.created());
|
||||
ArgumentCaptor<CustomerDimensionRuleClient.InitializationRequest> requests =
|
||||
ArgumentCaptor.forClass(CustomerDimensionRuleClient.InitializationRequest.class);
|
||||
verify(client, times(3)).initialize(anyString(), requests.capture());
|
||||
assertEquals(List.of("iam-customer-create:10001", "iam-customer-create:10001",
|
||||
"iam-customer-create:10001"),
|
||||
requests.getAllValues().stream().map(
|
||||
CustomerDimensionRuleClient.InitializationRequest::initializationKey).toList());
|
||||
assertEquals(List.of(9001L, 9001L, 9001L), requests.getAllValues().stream()
|
||||
.map(CustomerDimensionRuleClient.InitializationRequest::operatorId).toList());
|
||||
assertEquals(List.of(10001L, 10001L, 10001L), requests.getAllValues().stream()
|
||||
.map(CustomerDimensionRuleClient.InitializationRequest::tenantId).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportsServiceUnavailableAfterThreeFailedAttemptsAndTriesCompensation() {
|
||||
when(client.initialize(anyString(), any())).thenThrow(new RuntimeException("timeout"));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
BizException failure = assertThrows(BizException.class, () -> service.initialize(10001L, 9001L));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
|
||||
verify(client, times(3)).initialize(anyString(), any());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRollbackCompensatesOnlyAConfigurationCreatedByThisRegistration() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
var result = service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
assertTrue(result.created());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void localRollbackDoesNotDeleteAConfigurationThatAlreadyExisted() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, false)));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
verify(client, never()).compensate(anyString(), anyLong(), anyString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseTimeoutThenIdempotentSuccessStillCompensatesOnLocalRollback() {
|
||||
when(client.initialize(anyString(), any()))
|
||||
.thenThrow(new RuntimeException("response timeout"))
|
||||
.thenReturn(ApiResponse.success(new CustomerDimensionRuleClient.InitializationResponse(91L, true)));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
TransactionSynchronizationManager.initSynchronization();
|
||||
|
||||
service.initialize(10001L, 9001L);
|
||||
List<TransactionSynchronization> synchronizations =
|
||||
TransactionSynchronizationManager.getSynchronizations();
|
||||
TransactionSynchronizationManager.clearSynchronization();
|
||||
synchronizations.forEach(value -> value.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK));
|
||||
|
||||
verify(client, times(2)).initialize(anyString(), any());
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void committedClientRegistrationCanBeCompensatedWithItsDeterministicKey() {
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.compensateRegistration(10001L);
|
||||
|
||||
verify(client).compensate("internal-token", 10001L, "iam-customer-create:10001");
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private IamTenantAddressMapper addresses;
|
||||
@MockBean private IamTenantQualificationMapper qualifications;
|
||||
@MockBean private IamTenantReviewMapper reviews;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
@Test
|
||||
void clientRegistrationCreatesOwnerAndAssignsGlobalMerchantOwnerRole() {
|
||||
@@ -63,6 +64,7 @@ class MerchantOnboardingServiceTest {
|
||||
verify(users).insert(user.capture());
|
||||
verify(memberships).insert(membership.capture());
|
||||
verify(membershipRoles).insert(roleGrant.capture());
|
||||
verify(dimensionRuleInitialization).initialize(result.tenantId(), 9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals(result.tenantId(), tenant.getValue().getId());
|
||||
@@ -104,6 +106,7 @@ class MerchantOnboardingServiceTest {
|
||||
|
||||
service.compensateRegistration(9001L);
|
||||
|
||||
verify(dimensionRuleInitialization).compensateRegistration(10001L);
|
||||
verify(membershipRoles).delete(any());
|
||||
verify(memberships).deleteById(30001L);
|
||||
verify(addresses).delete(any());
|
||||
@@ -111,6 +114,28 @@ class MerchantOnboardingServiceTest {
|
||||
verify(tenants).deleteById((java.io.Serializable) Long.valueOf(10001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void clientRegistrationFailsWhenDefaultDimensionRuleCannotBeInitialized() {
|
||||
IamRole ownerRole = new IamRole();
|
||||
ownerRole.setId(4000100L);
|
||||
ownerRole.setTenantId(0L);
|
||||
ownerRole.setAppCode("MERCHANT");
|
||||
ownerRole.setRoleCode("MERCHANT_OWNER");
|
||||
ownerRole.setStatus("ACTIVE");
|
||||
when(roles.selectOne(any())).thenReturn(ownerRole);
|
||||
org.mockito.Mockito.doThrow(new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
|
||||
.when(dimensionRuleInitialization).initialize(
|
||||
org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong());
|
||||
|
||||
var failure = assertThrows(com.baiyue.common.core.exception.BizException.class,
|
||||
() -> service.register(new MerchantRegistrationRequest(
|
||||
9001L, "注册人", "测试企业", "张三", "13800138000",
|
||||
"owner@example.com", "CN", "深圳市宝安区")));
|
||||
|
||||
assertEquals(com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationRefusesAfterQualificationUpload() {
|
||||
|
||||
@@ -136,4 +161,34 @@ class MerchantOnboardingServiceTest {
|
||||
verify(users, never()).deleteById((java.io.Serializable) any());
|
||||
verify(tenants, never()).deleteById((java.io.Serializable) any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationKeepsIamRegistrationWhenDimensionRuleCompensationFails() {
|
||||
IamUser user = new IamUser();
|
||||
user.setId(20001L);
|
||||
user.setAuthUserId(9001L);
|
||||
IamMembership membership = new IamMembership();
|
||||
membership.setId(30001L);
|
||||
membership.setTenantId(10001L);
|
||||
membership.setUserId(20001L);
|
||||
membership.setMemberType("OWNER");
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(10001L);
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
when(users.selectOne(any())).thenReturn(user);
|
||||
when(memberships.selectList(any())).thenReturn(java.util.List.of(membership));
|
||||
when(tenants.selectOne(any())).thenReturn(tenant);
|
||||
when(qualifications.selectCount(any())).thenReturn(0L);
|
||||
when(reviews.selectCount(any())).thenReturn(0L);
|
||||
org.mockito.Mockito.doThrow(new com.baiyue.common.core.exception.BizException(
|
||||
com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "补偿失败"))
|
||||
.when(dimensionRuleInitialization).compensateRegistration(10001L);
|
||||
|
||||
assertThrows(com.baiyue.common.core.exception.BizException.class,
|
||||
() -> service.compensateRegistration(9001L));
|
||||
|
||||
verify(membershipRoles, never()).delete(any());
|
||||
verify(users, never()).deleteById((java.io.Serializable) any());
|
||||
verify(tenants, never()).deleteById((java.io.Serializable) any());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ class TenantCustomerExportTest {
|
||||
@MockBean private ProfileResourceRepository profileRepository;
|
||||
@MockBean private AuthAccountClient authAccountClient;
|
||||
@MockBean private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private TenantService service;
|
||||
@Test
|
||||
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
|
||||
|
||||
@@ -48,6 +48,7 @@ class TenantServiceTest {
|
||||
@MockBean private MemberRepository members;
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@SpyBean private TenantAdministratorProvisioningService provisioning;
|
||||
@Resource private TenantService service;
|
||||
|
||||
@@ -177,9 +178,23 @@ class TenantServiceTest {
|
||||
assertEquals("基础版", created.servicePackage());
|
||||
verify(repository).insertTenant(argThat(tenant -> Long.valueOf(9001L).equals(tenant.getCreatedBy())
|
||||
&& Long.valueOf(9001L).equals(tenant.getUpdatedBy())));
|
||||
verify(dimensionRuleInitialization).initialize(created.id(), 9001L);
|
||||
verify(repository, never()).insertReview(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRegistrationFailsWhenDefaultDimensionRuleCannotBeInitialized() {
|
||||
doThrow(new BizException(ResultCode.SERVICE_UNAVAILABLE, "初始化失败"))
|
||||
.when(dimensionRuleInitialization).initialize(any(Long.class), any(Long.class));
|
||||
|
||||
BizException failure = assertThrows(BizException.class, () -> service.register(new RegisterRequest(
|
||||
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||
"alice@example.com", "基础版", null, null, 30, null,
|
||||
null, null, "CNY", null, null), 9001L));
|
||||
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, failure.getResultCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformRegistrationAlwaysCreatesTheDraftWithoutACreditCode() {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user