This commit is contained in:
2026-08-07 19:23:40 +08:00
parent 37fec1e012
commit c698290106
39 changed files with 1897 additions and 13 deletions
@@ -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) { }
}
@@ -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;