优化
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
|
||||
/** Internal Warehouse contract for a customer's default overseas storage fees. */
|
||||
@FeignClient(name = "warehouse-service", contextId = "overseasStorageFeeClient",
|
||||
url = "${baiyue.clients.warehouse.url:}",
|
||||
path = "/internal/warehouse/v1/customer-overseas-storage-fees")
|
||||
public interface OverseasStorageFeeClient {
|
||||
@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) { }
|
||||
}
|
||||
@@ -41,6 +41,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private IamTenantQualificationMapper qualificationMapper;
|
||||
@Resource private IamTenantReviewMapper reviewMapper;
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
@@ -120,6 +121,7 @@ public class MerchantOnboardingService {
|
||||
addressMapper.insert(address);
|
||||
}
|
||||
dimensionRuleInitialization.initialize(tenantId, request.authUserId());
|
||||
overseasStorageFeeInitialization.initialize(tenantId, request.authUserId());
|
||||
return new MerchantRegistrationResponse(tenantId, membershipId, tenant.getStatus());
|
||||
}
|
||||
|
||||
@@ -161,6 +163,7 @@ public class MerchantOnboardingService {
|
||||
"IAM registration compensation is unsafe: onboarding has progressed");
|
||||
}
|
||||
|
||||
overseasStorageFeeInitialization.compensateRegistration(tenant.getId());
|
||||
dimensionRuleInitialization.compensateRegistration(tenant.getId());
|
||||
|
||||
membershipRoleMapper.delete(new LambdaQueryWrapper<IamMembershipRole>()
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
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.OverseasStorageFeeClient;
|
||||
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;
|
||||
|
||||
/** Creates the nine editable default fee tiers for a customer. */
|
||||
@Service
|
||||
public class OverseasStorageFeeInitializationService {
|
||||
private static final Logger log = LoggerFactory.getLogger(OverseasStorageFeeInitializationService.class);
|
||||
private static final int MAX_ATTEMPTS = 3;
|
||||
private static final String KEY_PREFIX = "iam-customer-create-overseas-fee:";
|
||||
|
||||
@Resource private OverseasStorageFeeClient client;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
|
||||
public InitializationResult initialize(long tenantId, long operatorId) {
|
||||
String key = initializationKey(tenantId);
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
ApiResponse<OverseasStorageFeeClient.InitializationResponse> response = client.initialize(
|
||||
internalToken, new OverseasStorageFeeClient.InitializationRequest(tenantId, operatorId, key));
|
||||
if (response != null && response.successful() && response.data() != null) {
|
||||
InitializationResult result = new InitializationResult(
|
||||
response.data().createdByThisInitialization(), key);
|
||||
registerRollbackCompensation(tenantId, result);
|
||||
return result;
|
||||
}
|
||||
last = unavailable(response);
|
||||
} catch (RuntimeException failure) {
|
||||
last = failure;
|
||||
log.warn("海外仓费用初始化调用异常,tenantId={}, attempt={}", tenantId, attempt, failure);
|
||||
}
|
||||
}
|
||||
compensateAfterUncertainInitialization(tenantId, key);
|
||||
throw unavailable(last);
|
||||
}
|
||||
|
||||
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 ex) {
|
||||
log.error("客户创建事务回滚后海外仓费用补偿失败,tenantId={}, initializationKey={}", tenantId,
|
||||
result.initializationKey(), ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void compensateAfterUncertainInitialization(long tenantId, String key) {
|
||||
try { compensateRequired(tenantId, key); }
|
||||
catch (RuntimeException ex) { log.error("海外仓费用初始化结果不确定且补偿失败,tenantId={}, initializationKey={}", tenantId, key, ex); }
|
||||
}
|
||||
|
||||
private void compensateRequired(long tenantId, String key) {
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
ApiResponse<Void> response = client.compensate(internalToken, tenantId, key);
|
||||
if (response != null && response.successful()) return;
|
||||
last = unavailable(response);
|
||||
} catch (RuntimeException ex) { last = ex; }
|
||||
}
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "海外仓费用补偿失败,请根据客户ID修复数据一致性: " + tenantId);
|
||||
}
|
||||
|
||||
private static String initializationKey(long tenantId) { return KEY_PREFIX + tenantId; }
|
||||
private static BizException unavailable(ApiResponse<?> response) {
|
||||
String detail = response == null || response.message() == null || response.message().isBlank()
|
||||
? "Warehouse 服务未返回有效响应" : 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()
|
||||
? "Warehouse 服务暂时不可用" : cause.getMessage();
|
||||
return new BizException(ResultCode.SERVICE_UNAVAILABLE, "海外仓费用初始化失败,已重试 3 次: " + detail);
|
||||
}
|
||||
|
||||
public record InitializationResult(boolean created, String initializationKey) { }
|
||||
}
|
||||
@@ -63,6 +63,8 @@ public class TenantService {
|
||||
@Resource
|
||||
private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource
|
||||
private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource
|
||||
private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
|
||||
@Transactional
|
||||
@@ -180,6 +182,7 @@ public class TenantService {
|
||||
tenant.setUpdatedBy(operatorId);
|
||||
repository.insertTenant(tenant);
|
||||
dimensionRuleInitialization.initialize(tenant.getId(), operatorId);
|
||||
overseasStorageFeeInitialization.initialize(tenant.getId(), operatorId);
|
||||
var administrator = administratorProvisioning.provisionPlatformCreatedTenant(tenant, operatorId);
|
||||
|
||||
return new CreationResponse(response(tenant), administrator.username(), administrator.temporaryPassword());
|
||||
|
||||
@@ -23,8 +23,12 @@ BEGIN
|
||||
END IF;
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM iam_permission
|
||||
WHERE (id = 5103040 AND permission_code <> 'platform:download:list')
|
||||
OR (id = 5103041 AND permission_code <> 'platform:download:all')
|
||||
WHERE (id = 5103040 AND NOT (
|
||||
(permission_code = 'platform:download:list' AND parent_id = 5103000)
|
||||
OR (permission_code = 'platform:location:list' AND parent_id = 5103000)))
|
||||
OR (id = 5103041 AND NOT (
|
||||
(permission_code = 'platform:download:all' AND parent_id = 5103040)
|
||||
OR (permission_code = 'platform:location:create' AND parent_id = 5103040)))
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '库位权限迁移失败:下载中心稳定 ID 被未知权限占用';
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class LocationPermissionSqlContractTest {
|
||||
@Test
|
||||
void acceptsKnownPollutionAndRestoresCanonicalPermissionIds() throws Exception {
|
||||
assertRepairContract(moduleSql());
|
||||
assertRepairContract(unifiedSql());
|
||||
}
|
||||
|
||||
private static void assertRepairContract(String sql) {
|
||||
assertThat(sql)
|
||||
.contains("permission_code = 'platform:download:list' AND parent_id = 5103000")
|
||||
.contains("permission_code = 'platform:location:list' AND parent_id = 5103000")
|
||||
.contains("permission_code = 'platform:download:all' AND parent_id = 5103040")
|
||||
.contains("permission_code = 'platform:location:create' AND parent_id = 5103040")
|
||||
.contains("库位权限迁移失败:下载中心稳定 ID 被未知权限占用")
|
||||
.contains("(5103040, 0, 'PLATFORM', 'MENU', 5103000, 'platform:download:list'")
|
||||
.contains("(5103041, 0, 'PLATFORM', 'BUTTON', 5103040, 'platform:download:all'")
|
||||
.contains("(5103060, 0, 'PLATFORM', 'MENU', 5103000, 'platform:location:list'")
|
||||
.contains("(5103061, 0, 'PLATFORM', 'BUTTON', 5103060, 'platform:location:create'");
|
||||
|
||||
int snapshot = sql.indexOf("INSERT IGNORE INTO tmp_location_role_grant");
|
||||
int delete = firstPositive(
|
||||
sql.indexOf("DELETE role_permission", snapshot),
|
||||
sql.indexOf("DELETE rp", snapshot));
|
||||
int canonicalLocation = sql.indexOf("(5103060, 0, 'PLATFORM'", delete);
|
||||
int restore = sql.indexOf("FROM tmp_location_role_grant", canonicalLocation);
|
||||
int cleanup = sql.indexOf("DROP TEMPORARY TABLE IF EXISTS tmp_location_role_grant", restore);
|
||||
assertThat(snapshot).isGreaterThan(0);
|
||||
assertThat(delete).isGreaterThan(snapshot);
|
||||
assertThat(canonicalLocation).isGreaterThan(delete);
|
||||
assertThat(restore).isGreaterThan(canonicalLocation);
|
||||
assertThat(cleanup).isGreaterThan(restore);
|
||||
}
|
||||
|
||||
private static String moduleSql() throws Exception {
|
||||
return new ClassPathResource("db/phase2-location-permissions.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static int firstPositive(int left, int right) {
|
||||
if (left < 0) {
|
||||
return right;
|
||||
}
|
||||
if (right < 0) {
|
||||
return left;
|
||||
}
|
||||
return Math.min(left, right);
|
||||
}
|
||||
|
||||
private static String unifiedSql() throws Exception {
|
||||
Path path = Path.of("deploy/sql/头程数据库统一增量-MySQL8.sql");
|
||||
if (!Files.exists(path)) {
|
||||
path = Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql").normalize();
|
||||
}
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ class MerchantOnboardingServiceTest {
|
||||
@MockBean private IamTenantQualificationMapper qualifications;
|
||||
@MockBean private IamTenantReviewMapper reviews;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private MerchantOnboardingService service;
|
||||
@Test
|
||||
void clientRegistrationCreatesOwnerAndAssignsGlobalMerchantOwnerRole() {
|
||||
@@ -65,6 +66,7 @@ class MerchantOnboardingServiceTest {
|
||||
verify(memberships).insert(membership.capture());
|
||||
verify(membershipRoles).insert(roleGrant.capture());
|
||||
verify(dimensionRuleInitialization).initialize(result.tenantId(), 9001L);
|
||||
verify(overseasStorageFeeInitialization).initialize(result.tenantId(), 9001L);
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", result.tenantStatus());
|
||||
assertEquals(result.tenantId(), tenant.getValue().getId());
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
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.OverseasStorageFeeClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.TransactionSynchronizationManager;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(OverseasStorageFeeInitializationService.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
class OverseasStorageFeeInitializationServiceTest {
|
||||
@MockBean private OverseasStorageFeeClient client;
|
||||
@Resource private OverseasStorageFeeInitializationService service;
|
||||
|
||||
@AfterEach void clearTransaction() {
|
||||
if (TransactionSynchronizationManager.isSynchronizationActive()) TransactionSynchronizationManager.clearSynchronization();
|
||||
}
|
||||
|
||||
@Test void retriesThreeTimesWithDeterministicKey() {
|
||||
when(client.initialize(anyString(), any())).thenThrow(new RuntimeException("timeout"));
|
||||
when(client.compensate(anyString(), anyLong(), anyString())).thenReturn(ApiResponse.success(null));
|
||||
BizException error = assertThrows(BizException.class, () -> service.initialize(208L, 9L));
|
||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, error.getResultCode());
|
||||
verify(client, times(3)).initialize(anyString(), any(OverseasStorageFeeClient.InitializationRequest.class));
|
||||
verify(client).compensate("internal-token", 208L, "iam-customer-create-overseas-fee:208");
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class TenantCustomerExportTest {
|
||||
@MockBean private AuthAccountClient authAccountClient;
|
||||
@MockBean private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@MockBean private TenantAdministratorProvisioningService administratorProvisioning;
|
||||
@Resource private TenantService service;
|
||||
@Test
|
||||
|
||||
@@ -54,6 +54,7 @@ class TenantServiceTest {
|
||||
@MockBean private IamRoleMapper roles;
|
||||
@MockBean private AuthAccountClient authClient;
|
||||
@MockBean private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@MockBean private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@SpyBean private TenantAdministratorProvisioningService provisioning;
|
||||
@Resource private TenantService service;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user