This commit is contained in:
2026-08-08 13:32:20 +08:00
parent 96788e10de
commit 3203adb5a7
73 changed files with 44331 additions and 26 deletions
@@ -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 被未知权限占用';