优化
This commit is contained in:
@@ -12,6 +12,8 @@ import java.time.LocalDateTime;
|
||||
public interface AuthAccountClient {
|
||||
@PostMapping ApiResponse<AccountSummary> create(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestBody CreateAccountRequest request);
|
||||
@GetMapping("/availability") ApiResponse<Boolean> availability(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@RequestParam String username);
|
||||
@GetMapping("/{id}") ApiResponse<AccountSummary> summary(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable Long id);
|
||||
@PutMapping("/{id}/status") ApiResponse<Void> status(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
|
||||
@@ -73,6 +73,17 @@ public class PlatformTenantController {
|
||||
return ApiResponse.success(service.merchant(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/tenants/contact-phone-availability")
|
||||
@PreAuthorize("hasAnyAuthority('platform:customer:create','platform:customer:edit')")
|
||||
@Operation(summary = "校验客户联系电话唯一性")
|
||||
public ApiResponse<Boolean> contactPhoneAvailability(
|
||||
@RequestParam String contactPhone,
|
||||
@RequestParam(required = false) Long excludeTenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.contactPhoneAndUsernameAvailable(contactPhone, excludeTenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/tenants/{tenantId:\\d+}/members")
|
||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||
@Operation(summary = "查询企业客户成员及账号", description = "平台只读查询指定企业的全部成员,包含 OWNER 企业管理员,并返回登录账号。")
|
||||
|
||||
@@ -27,7 +27,8 @@ public final class TenantDtos {
|
||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
||||
@Schema(description = "Contact phone and administrator username / 联系电话及管理员用户名", example = "13800138000")
|
||||
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
||||
@NotBlank @Size(max = 64) String servicePackage,
|
||||
@Size(max = 64) String contactWechat,
|
||||
@@ -58,7 +59,7 @@ public final class TenantDtos {
|
||||
@NotBlank String legalName,
|
||||
@NotBlank @Size(max = 64) String legalRepresentative,
|
||||
@NotBlank String contactName,
|
||||
@NotBlank String contactPhone,
|
||||
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
|
||||
@Email String contactEmail,
|
||||
@NotBlank @Size(max = 64) String servicePackage,
|
||||
@Size(max = 64) String contactWechat,
|
||||
@@ -86,7 +87,7 @@ public final class TenantDtos {
|
||||
public record ApprovalResponse(
|
||||
@Schema(description = "Tenant ID / 企业租户 ID", example = "190000000000000001") Long tenantId,
|
||||
@Schema(description = "Whether this approval created the administrator / 本次审核是否新建管理员") boolean administratorCreated,
|
||||
@Schema(description = "Generated login username; null when an owner already exists / 自动生成的登录账号,已有所有者时为空", nullable = true) String administratorUsername,
|
||||
@Schema(description = "Contact phone used as login username; null when an owner already exists / 使用联系电话作为登录账号,已有所有者时为空", nullable = true) String administratorUsername,
|
||||
@Schema(description = "One-time temporary password; only returned when created / 一次性初始密码,仅新建时返回", format = "password", nullable = true) String temporaryPassword
|
||||
) { }
|
||||
|
||||
|
||||
@@ -34,6 +34,19 @@ public class TenantRepository {
|
||||
.ne(IamTenant::getId, tenantId)) > 0;
|
||||
}
|
||||
|
||||
public boolean existsByContactPhone(String contactPhone) {
|
||||
return tenantMapper.selectCount(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.eq(IamTenant::getContactPhone, contactPhone)) > 0;
|
||||
}
|
||||
|
||||
public boolean existsByContactPhoneForOtherTenant(String contactPhone, long tenantId) {
|
||||
return tenantMapper.selectCount(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.eq(IamTenant::getContactPhone, contactPhone)
|
||||
.ne(IamTenant::getId, tenantId)) > 0;
|
||||
}
|
||||
|
||||
public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
|
||||
/**
|
||||
* Serializes the draft qualification workflow for one enterprise. The file service and the
|
||||
|
||||
+6
-19
@@ -34,8 +34,6 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
private static final char[] DIGITS = "23456789".toCharArray();
|
||||
private static final char[] ALL = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789".toCharArray();
|
||||
private static final int PASSWORD_LENGTH = 16;
|
||||
private static final int USERNAME_SUFFIX_LENGTH = 8;
|
||||
private static final int USERNAME_ATTEMPTS = 3;
|
||||
|
||||
@Resource
|
||||
private MemberRepository members;
|
||||
@@ -179,18 +177,9 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
|
||||
private String createAuthAccountWithUniqueUsername(long authUserId, String password,
|
||||
IamTenant tenant, long membershipId) {
|
||||
for (int attempt = 0; attempt < USERNAME_ATTEMPTS; attempt++) {
|
||||
String username = username(tenant.getId());
|
||||
try {
|
||||
createAuthAccount(authUserId, username, password, tenant, membershipId);
|
||||
return username;
|
||||
} catch (BizException exception) {
|
||||
if (exception.getResultCode() != ResultCode.CONFLICT || attempt == USERNAME_ATTEMPTS - 1) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("Username generation attempts exhausted");
|
||||
String username = username(tenant.getContactPhone());
|
||||
createAuthAccount(authUserId, username, password, tenant, membershipId);
|
||||
return username;
|
||||
}
|
||||
|
||||
private void createAuthAccount(long authUserId, String username, String password,
|
||||
@@ -208,7 +197,7 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
if (response.data() == null) throw unavailable();
|
||||
} catch (FeignException exception) {
|
||||
if (exception.status() == 409) {
|
||||
throw new BizException(ResultCode.CONFLICT, "自动生成的企业管理员登录账号已存在");
|
||||
throw new BizException(ResultCode.CONFLICT, "联系电话已被其他账号使用,不能作为企业管理员登录用户名");
|
||||
}
|
||||
throw unavailable();
|
||||
} catch (BizException exception) {
|
||||
@@ -244,10 +233,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
|
||||
return new String(value);
|
||||
}
|
||||
|
||||
private String username(long tenantId) {
|
||||
char[] suffix = new char[USERNAME_SUFFIX_LENGTH];
|
||||
for (int index = 0; index < suffix.length; index++) suffix[index] = pick(LOWER);
|
||||
return "merchant_" + tenantId + "_" + new String(suffix);
|
||||
private String username(String contactPhone) {
|
||||
return contactPhone == null ? "" : contactPhone.trim();
|
||||
}
|
||||
|
||||
private char pick(char[] source) { return source[random.nextInt(source.length)]; }
|
||||
|
||||
@@ -3,6 +3,7 @@ import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
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.entity.IamTenant;
|
||||
@@ -10,6 +11,7 @@ import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.OrderEligibilityResponse;
|
||||
|
||||
import com.baiyue.iam.dto.TenantDtos.RegisterRequest;
|
||||
@@ -23,7 +25,9 @@ import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportRow;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import feign.FeignException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -41,6 +45,10 @@ public class TenantService {
|
||||
@Resource
|
||||
private ProfileResourceRepository profileRepository;
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
@Resource
|
||||
private TenantApprovalInitializationCoordinator approvalInitialization;
|
||||
|
||||
|
||||
@@ -53,6 +61,8 @@ public class TenantService {
|
||||
)
|
||||
@Transactional
|
||||
public TenantResponse register(RegisterRequest request, long operatorId) {
|
||||
String contactPhone = normalizeContactPhone(request.contactPhone());
|
||||
ensureContactPhoneAvailable(contactPhone, null);
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(IdWorker.getId());
|
||||
tenant.setTenantCode("TEN" + tenant.getId());
|
||||
@@ -63,7 +73,7 @@ public class TenantService {
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
tenant.setUnifiedSocialCreditCode(null);
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactPhone(contactPhone);
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
tenant.setServicePackage(request.servicePackage());
|
||||
tenant.setContactWechat(request.contactWechat());
|
||||
@@ -208,16 +218,46 @@ public class TenantService {
|
||||
return response(requireMerchant(tenantId));
|
||||
}
|
||||
|
||||
public boolean contactPhoneAvailable(String contactPhone, Long tenantId) {
|
||||
String normalized = normalizeContactPhone(contactPhone);
|
||||
return tenantId == null
|
||||
? !repository.existsByContactPhone(normalized)
|
||||
: !repository.existsByContactPhoneForOtherTenant(normalized, tenantId);
|
||||
}
|
||||
|
||||
public boolean contactPhoneAndUsernameAvailable(String contactPhone, Long tenantId) {
|
||||
String normalized = normalizeContactPhone(contactPhone);
|
||||
if (!contactPhoneAvailable(normalized, tenantId)) return false;
|
||||
ApiResponse<Boolean> response;
|
||||
try {
|
||||
response = authClient.availability(internalToken, normalized);
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验联系电话");
|
||||
}
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验联系电话");
|
||||
}
|
||||
return response.data();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_TENANT_UPDATE", resourceType = "TENANT", resourceId = "#tenantId",
|
||||
description = "平台编辑企业客户档案")
|
||||
public TenantResponse updateMerchant(long tenantId, UpdateTenantRequest request) {
|
||||
IamTenant tenant = requireMerchant(tenantId);
|
||||
String contactPhone = normalizeContactPhone(request.contactPhone());
|
||||
String existingContactPhone = normalizeContactPhone(tenant.getContactPhone());
|
||||
if (!contactPhone.equals(existingContactPhone)) {
|
||||
if ("ACTIVE".equals(tenant.getStatus()) || "DISABLED".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户已开户,联系电话作为登录用户名不能直接修改");
|
||||
}
|
||||
ensureContactPhoneAvailable(contactPhone, tenantId);
|
||||
}
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(request.contactPhone());
|
||||
tenant.setContactPhone(contactPhone);
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
tenant.setServicePackage(request.servicePackage());
|
||||
tenant.setContactWechat(request.contactWechat());
|
||||
@@ -258,6 +298,7 @@ public class TenantService {
|
||||
if (repository.findPendingReviewByTenant(tenantId) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review / 客户资料已在审核中");
|
||||
}
|
||||
ensureContactPhoneAvailable(normalizeContactPhone(tenant.getContactPhone()), tenantId);
|
||||
if (tenant.getUnifiedSocialCreditCode() == null
|
||||
|| !tenant.getUnifiedSocialCreditCode().matches("[0-9A-Z]{18}")) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
@@ -297,6 +338,16 @@ public class TenantService {
|
||||
return tenant.getId() != null && tenant.getId().longValue() == id;
|
||||
}
|
||||
|
||||
private String normalizeContactPhone(String contactPhone) {
|
||||
return contactPhone == null ? "" : contactPhone.trim();
|
||||
}
|
||||
|
||||
private void ensureContactPhoneAvailable(String contactPhone, Long tenantId) {
|
||||
if (!contactPhoneAvailable(contactPhone, tenantId)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "联系电话已被其他客户使用,不能作为登录用户名");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasAllPendingQualifications(List<IamTenantQualification> qualifications) {
|
||||
return hasAllQualificationsWithStatus(qualifications, "PENDING");
|
||||
}
|
||||
|
||||
@@ -17,6 +17,25 @@ tags:
|
||||
- name: RBAC
|
||||
description: Platform and merchant role-permission management / 平台端与商户端角色权限管理
|
||||
paths:
|
||||
/api/platform/v1/iam/tenants/contact-phone-availability:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 校验客户联系电话唯一性
|
||||
parameters:
|
||||
- {name: contactPhone, in: query, required: true, schema: {type: string}}
|
||||
- {name: excludeTenantId, in: query, required: false, schema: {type: integer, format: int64}}
|
||||
responses:
|
||||
'200':
|
||||
description: 返回联系电话是否可作为登录用户名
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
code: {type: string}
|
||||
message: {type: string}
|
||||
data: {type: boolean}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/platform/v1/iam/tenants:
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
@@ -798,7 +817,7 @@ components:
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
||||
administratorCreated: {type: boolean, description: 本次审核是否新建企业管理员}
|
||||
administratorUsername: {type: string, nullable: true, example: merchant_190000000000000001_k3mnpqrs}
|
||||
administratorUsername: {type: string, nullable: true, example: '13800138000'}
|
||||
temporaryPassword: {type: string, format: password, nullable: true, description: 一次性初始密码,仅新建管理员时返回}
|
||||
TenantApiResponse:
|
||||
allOf:
|
||||
|
||||
Reference in New Issue
Block a user