This commit is contained in:
2026-09-02 10:04:18 +08:00
parent b85e021cbe
commit e83dff6ac7
12 changed files with 67 additions and 96 deletions
@@ -22,7 +22,7 @@ public interface IamOnboardingClient {
@PathVariable("authUserId") Long authUserId); @PathVariable("authUserId") Long authUserId);
record OnboardingRequest(Long authUserId, String displayName, String companyName, record OnboardingRequest(Long authUserId, String displayName, String companyName,
String legalPerson, String legalPhone, String contactEmail, String legalPerson, String administratorPhone, String contactEmail,
String countryCode, String address) { } String countryCode, String address) { }
record OnboardingResponse(Long tenantId, Long membershipId, String tenantStatus, record OnboardingResponse(Long tenantId, Long membershipId, String tenantStatus,
@@ -1,10 +1,10 @@
package com.baiyue.auth.dto; package com.baiyue.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration") @Schema(name = "MerchantRegisterRequest", description = "Phase-one merchant account registration")
public record MerchantRegisterRequest( public record MerchantRegisterRequest(
@@ -13,7 +13,8 @@ public record MerchantRegisterRequest(
@NotBlank @Size(max = 64) String displayName, @NotBlank @Size(max = 64) String displayName,
@NotBlank @Size(max = 128) String companyName, @NotBlank @Size(max = 128) String companyName,
@NotBlank @Size(max = 64) String legalPerson, @NotBlank @Size(max = 64) String legalPerson,
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String legalPhone, @Schema(description = "首个企业管理员的登录手机号;注册时同时初始化企业联系电话,创建后两者独立维护")
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@Email @Size(max = 128) String contactEmail, @Email @Size(max = 128) String contactEmail,
@Size(min = 2, max = 2) String countryCode, @Size(min = 2, max = 2) String countryCode,
@Size(max = 255) String address @Size(max = 255) String address
@@ -38,7 +38,7 @@ public class MerchantRegistrationService {
if (accountRepository.existsByUsername(request.username())) { if (accountRepository.existsByUsername(request.username())) {
throw new BizException(ResultCode.CONFLICT, "用户名已存在"); throw new BizException(ResultCode.CONFLICT, "用户名已存在");
} }
if (accountRepository.existsByPhone(request.legalPhone())) { if (accountRepository.existsByPhone(request.phone())) {
throw new BizException(ResultCode.CONFLICT, "手机号已存在"); throw new BizException(ResultCode.CONFLICT, "手机号已存在");
} }
long accountId = IdWorker.getId(); long accountId = IdWorker.getId();
@@ -49,7 +49,7 @@ public class MerchantRegistrationService {
try { try {
var response = onboardingClient.register(internalToken, var response = onboardingClient.register(internalToken,
new IamOnboardingClient.OnboardingRequest(accountId, request.displayName(), new IamOnboardingClient.OnboardingRequest(accountId, request.displayName(),
request.companyName(), request.legalPerson(), request.legalPhone(), request.companyName(), request.legalPerson(), request.phone(),
request.contactEmail(), request.countryCode(), request.address())); request.contactEmail(), request.countryCode(), request.address()));
if (response == null) { if (response == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM registration failed"); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM registration failed");
@@ -84,7 +84,7 @@ public class MerchantRegistrationService {
AuthAccount account = new AuthAccount(); AuthAccount account = new AuthAccount();
account.setId(accountId); account.setId(accountId);
account.setUsername(request.username()); account.setUsername(request.username());
account.setPhone(request.legalPhone()); account.setPhone(request.phone());
account.setPasswordHash(passwordHash); account.setPasswordHash(passwordHash);
account.setDisplayName(request.displayName()); account.setDisplayName(request.displayName());
account.setAccountStatus("ACTIVE"); account.setAccountStatus("ACTIVE");
@@ -96,15 +96,15 @@ public class PlatformTenantController {
return ApiResponse.success(service.dimensionInitializationStatus(tenantId)); return ApiResponse.success(service.dimensionInitializationStatus(tenantId));
} }
@GetMapping("/tenants/contact-phone-availability") @GetMapping("/tenants/administrator-phone-availability")
@PreAuthorize("hasAnyAuthority('platform:customer:create','platform:customer:edit')") @PreAuthorize("hasAuthority('platform:customer:create')")
@Operation(summary = "校验客户联系电话唯一性") @Operation(summary = "校验首个管理员登录手机号唯一性",
public ApiResponse<Boolean> contactPhoneAvailability( description = "仅用于创建客户前校验 Auth 登录手机号;企业联系电话本身不要求唯一。")
@RequestParam String contactPhone, public ApiResponse<Boolean> administratorPhoneAvailability(
@RequestParam(required = false) Long excludeTenantId, @RequestParam @Pattern(regexp = "^1[3-9]\\d{9}$") String phone,
@AuthenticationPrincipal AuthenticatedUser user) { @AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM"); TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.contactPhoneAndUsernameAvailable(contactPhone, excludeTenantId)); return ApiResponse.success(service.administratorPhoneAvailable(phone));
} }
@GetMapping("/tenants/{tenantId:\\d+}/members") @GetMapping("/tenants/{tenantId:\\d+}/members")
@@ -14,7 +14,7 @@ public final class OnboardingDtos {
@NotBlank String displayName, @NotBlank String displayName,
@NotBlank String companyName, @NotBlank String companyName,
@NotBlank String legalPerson, @NotBlank String legalPerson,
@NotBlank String legalPhone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String administratorPhone,
String contactEmail, String contactEmail,
String countryCode, String countryCode,
String address String address
@@ -27,8 +27,8 @@ public final class TenantDtos {
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName, @Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative, @Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName, @Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
@Schema(description = "Contact phone and administrator username / 联系电话及管理员用户名", example = "13800138000") @Schema(description = "Company contact phone; also initializes the first administrator login phone at creation, then maintained independently / 公司联系电话;创建时同时初始化首个管理员登录手机号,之后独立维护", example = "13800138000")
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone, @NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String contactPhone,
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail, @Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
@NotBlank @Size(max = 64) String servicePackage, @NotBlank @Size(max = 64) String servicePackage,
@Size(max = 64) String contactWechat, @Size(max = 64) String contactWechat,
@@ -48,7 +48,8 @@ public final class TenantDtos {
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName, @Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative, @Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
@Schema(description = "Primary contact / 联系人", example = "李四") @NotBlank String contactName, @Schema(description = "Primary contact / 联系人", example = "李四") @NotBlank String contactName,
@Schema(description = "Contact phone / 联系电话", example = "13900139000") @NotBlank String contactPhone, @Schema(description = "Company contact phone, never used to resolve login identity / 公司联系电话,不作为登录标识", example = "0755-12345678")
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
@Schema(description = "Contact email / 联系邮箱", example = "service@example.com") @Email String contactEmail @Schema(description = "Contact email / 联系邮箱", example = "service@example.com") @Email String contactEmail
) { } ) { }
@@ -59,6 +60,7 @@ public final class TenantDtos {
@NotBlank String legalName, @NotBlank String legalName,
@NotBlank @Size(max = 64) String legalRepresentative, @NotBlank @Size(max = 64) String legalRepresentative,
@NotBlank String contactName, @NotBlank String contactName,
@Schema(description = "Company contact phone, independent from every member login phone / 公司联系电话,与所有成员登录手机号独立")
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone, @NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
@Email String contactEmail, @Email String contactEmail,
@NotBlank @Size(max = 64) String servicePackage, @NotBlank @Size(max = 64) String servicePackage,
@@ -120,14 +122,14 @@ public final class TenantDtos {
public record ApprovalResponse( public record ApprovalResponse(
@Schema(description = "Tenant ID / 企业租户 ID", example = "190000000000000001") Long tenantId, @Schema(description = "Tenant ID / 企业租户 ID", example = "190000000000000001") Long tenantId,
@Schema(description = "Whether this approval created the administrator / 本次审核是否新建管理员") boolean administratorCreated, @Schema(description = "Whether this approval created the administrator / 本次审核是否新建管理员") boolean administratorCreated,
@Schema(description = "Contact phone used as login username; null when an owner already exists / 使用联系电话作为登录账号,已有所有者时为空", nullable = true) String administratorUsername, @Schema(description = "Generated first-administrator username; independent from company contact phone / 自动生成的首个管理员用户名,与公司联系电话独立", nullable = true) String administratorUsername,
@Schema(description = "One-time temporary password; only returned when created / 一次性初始密码,仅新建时返回", format = "password", nullable = true) String temporaryPassword @Schema(description = "One-time temporary password; only returned when created / 一次性初始密码,仅新建时返回", format = "password", nullable = true) String temporaryPassword
) { } ) { }
@Schema(name = "TenantCreationResponse", description = "Created enterprise draft and one-time administrator credentials / 新建企业草稿及一次性管理员凭据") @Schema(name = "TenantCreationResponse", description = "Created enterprise draft and one-time administrator credentials / 新建企业草稿及一次性管理员凭据")
public record CreationResponse( public record CreationResponse(
@Schema(description = "Created enterprise draft / 新建企业草稿") TenantResponse tenant, @Schema(description = "Created enterprise draft / 新建企业草稿") TenantResponse tenant,
@Schema(description = "Contact phone used as login username / 使用联系电话作为登录账号") String administratorUsername, @Schema(description = "Generated administrator username, independent from company contact phone / 自动生成的管理员用户名,与公司联系电话独立") String administratorUsername,
@Schema(description = "One-time temporary password / 一次性初始密码", format = "password") String temporaryPassword @Schema(description = "One-time temporary password / 一次性初始密码", format = "password") String temporaryPassword
) { } ) { }
@@ -35,19 +35,6 @@ public class TenantRepository {
.ne(IamTenant::getId, tenantId)) > 0; .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); } public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
public List<IamTenant> findMerchantsByIds(Collection<Long> tenantIds) { public List<IamTenant> findMerchantsByIds(Collection<Long> tenantIds) {
if (tenantIds == null || tenantIds.isEmpty()) return List.of(); if (tenantIds == null || tenantIds.isEmpty()) return List.of();
@@ -74,7 +74,7 @@ public class MerchantOnboardingService {
tenant.setLegalName(request.companyName()); tenant.setLegalName(request.companyName());
tenant.setLegalRepresentative(request.legalPerson()); tenant.setLegalRepresentative(request.legalPerson());
tenant.setContactName(request.displayName()); tenant.setContactName(request.displayName());
tenant.setContactPhone(request.legalPhone()); tenant.setContactPhone(request.administratorPhone());
tenant.setContactEmail(request.contactEmail()); tenant.setContactEmail(request.contactEmail());
tenant.setDeleted(false); tenant.setDeleted(false);
tenant.setVersion(0); tenant.setVersion(0);
@@ -86,7 +86,7 @@ public class MerchantOnboardingService {
user.setAuthUserId(request.authUserId()); user.setAuthUserId(request.authUserId());
user.setUserType("ENTERPRISE"); user.setUserType("ENTERPRISE");
user.setDisplayName(request.displayName()); user.setDisplayName(request.displayName());
user.setPhone(request.legalPhone()); user.setPhone(request.administratorPhone());
user.setEmail(request.contactEmail()); user.setEmail(request.contactEmail());
user.setStatus("ACTIVE"); user.setStatus("ACTIVE");
userMapper.insert(user); userMapper.insert(user);
@@ -88,7 +88,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
long userId = IdWorker.getId(); long userId = IdWorker.getId();
long membershipId = IdWorker.getId(); long membershipId = IdWorker.getId();
String password = temporaryPassword(); String password = temporaryPassword();
String username = createAuthAccountWithUniqueUsername(authUserId, password, tenant, membershipId); String username = createAuthAccount(authUserId, password, tenant, membershipId,
tenant.getContactPhone(), tenant.getContactName());
ProvisioningResult result = ProvisioningResult.created(authUserId, username, password); ProvisioningResult result = ProvisioningResult.created(authUserId, username, password);
try { try {
@@ -153,8 +154,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
AuthAccountClient.AccountSummary account = findAuthAccount(user.getAuthUserId()); AuthAccountClient.AccountSummary account = findAuthAccount(user.getAuthUserId());
if (account == null) { if (account == null) {
String password = temporaryPassword(); String password = temporaryPassword();
String username = createAuthAccountWithUniqueUsername(user.getAuthUserId(), password, String username = createAuthAccount(user.getAuthUserId(), password, tenant, owner.getId(),
tenant, owner.getId()); user.getPhone(), user.getDisplayName());
publishInitialSecurityProjection(user.getAuthUserId(), tenant.getId()); publishInitialSecurityProjection(user.getAuthUserId(), tenant.getId());
return ProvisioningResult.created(user.getAuthUserId(), username, password); return ProvisioningResult.created(user.getAuthUserId(), username, password);
} }
@@ -204,19 +205,13 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
if (result.created()) compensate(result.authUserId()); if (result.created()) compensate(result.authUserId());
} }
private String createAuthAccountWithUniqueUsername(long authUserId, String password, private String createAuthAccount(long authUserId, String password, IamTenant tenant, long membershipId,
IamTenant tenant, long membershipId) { String loginPhone, String displayName) {
String username = username(tenant.getContactPhone()); String username = generatedUsername(authUserId);
createAuthAccount(authUserId, username, password, tenant, membershipId);
return username;
}
private void createAuthAccount(long authUserId, String username, String password,
IamTenant tenant, long membershipId) {
try { try {
ApiResponse<AuthAccountClient.AccountSummary> response = authClient.create(internalToken, ApiResponse<AuthAccountClient.AccountSummary> response = authClient.create(internalToken,
new AuthAccountClient.CreateAccountRequest(authUserId, username, tenant.getContactPhone(), password, new AuthAccountClient.CreateAccountRequest(authUserId, username, loginPhone, password,
tenant.getContactName(), tenant.getId(), membershipId, "MERCHANT", "ACTIVE")); displayName, tenant.getId(), membershipId, "MERCHANT", "ACTIVE"));
if (response == null) throw unavailable(); if (response == null) throw unavailable();
if (!response.successful()) { if (!response.successful()) {
ResultCode code = ResultCode.of(response.code()); ResultCode code = ResultCode.of(response.code());
@@ -224,9 +219,10 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
? code.message() : response.message()); ? code.message() : response.message());
} }
if (response.data() == null) throw unavailable(); if (response.data() == null) throw unavailable();
return username;
} catch (FeignException exception) { } catch (FeignException exception) {
if (exception.status() == 409) { if (exception.status() == 409) {
throw new BizException(ResultCode.CONFLICT, "联系电话已被其他账号使用,不能作为企业管理员登录用户名"); throw new BizException(ResultCode.CONFLICT, "管理员用户名或登录手机号已被其他账号使用");
} }
throw unavailable(); throw unavailable();
} catch (BizException exception) { } catch (BizException exception) {
@@ -267,8 +263,8 @@ public class TenantAdministratorProvisioningService implements TenantApprovalIni
return new String(value); return new String(value);
} }
private String username(String contactPhone) { private String generatedUsername(long authUserId) {
return contactPhone == null ? "" : contactPhone.trim(); return "merchant_" + authUserId;
} }
private char pick(char[] source) { return source[random.nextInt(source.length)]; } private char pick(char[] source) { return source[random.nextInt(source.length)]; }
@@ -162,7 +162,7 @@ public class TenantService {
@Transactional @Transactional
public CreationResponse register(RegisterRequest request, long operatorId) { public CreationResponse register(RegisterRequest request, long operatorId) {
String contactPhone = normalizeContactPhone(request.contactPhone()); String contactPhone = normalizeContactPhone(request.contactPhone());
ensureContactPhoneAvailable(contactPhone, null); ensureAdministratorPhoneAvailable(contactPhone);
IamTenant tenant = new IamTenant(); IamTenant tenant = new IamTenant();
tenant.setId(IdWorker.getId()); tenant.setId(IdWorker.getId());
tenant.setTenantCode("TEN" + tenant.getId()); tenant.setTenantCode("TEN" + tenant.getId());
@@ -364,24 +364,17 @@ public class TenantService {
return dimensionRuleInitialization.status(tenantId); return dimensionRuleInitialization.status(tenantId);
} }
public boolean contactPhoneAvailable(String contactPhone, Long tenantId) { /** Platform creation uses the same initial value for company contact and owner login phone. */
String normalized = normalizeContactPhone(contactPhone); public boolean administratorPhoneAvailable(String phone) {
return tenantId == null String normalized = normalizeContactPhone(phone);
? !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; ApiResponse<Boolean> response;
try { try {
response = authClient.availability(internalToken, normalized); response = authClient.availability(internalToken, normalized);
} catch (FeignException exception) { } catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验联系电话"); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验管理员登录手机号");
} }
if (response == null || !response.successful() || response.data() == null) { if (response == null || !response.successful() || response.data() == null) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验联系电话"); throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "认证账号服务暂时不可用,不能校验管理员登录手机号");
} }
return response.data(); return response.data();
} }
@@ -393,13 +386,6 @@ public class TenantService {
IamTenant tenant = requireMerchant(tenantId); IamTenant tenant = requireMerchant(tenantId);
ensureNotArchived(tenant); ensureNotArchived(tenant);
String contactPhone = normalizeContactPhone(request.contactPhone()); 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.setTenantName(request.tenantName());
tenant.setLegalName(request.legalName()); tenant.setLegalName(request.legalName());
tenant.setLegalRepresentative(request.legalRepresentative()); tenant.setLegalRepresentative(request.legalRepresentative());
@@ -446,7 +432,6 @@ public class TenantService {
if (repository.findPendingReviewByTenant(tenantId) != null) { if (repository.findPendingReviewByTenant(tenantId) != null) {
throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review / 客户资料已在审核中"); throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review / 客户资料已在审核中");
} }
ensureContactPhoneAvailable(normalizeContactPhone(tenant.getContactPhone()), tenantId);
if (tenant.getUnifiedSocialCreditCode() == null if (tenant.getUnifiedSocialCreditCode() == null
|| !tenant.getUnifiedSocialCreditCode().matches("[0-9A-Z]{18}")) { || !tenant.getUnifiedSocialCreditCode().matches("[0-9A-Z]{18}")) {
throw new BizException(ResultCode.BAD_REQUEST, throw new BizException(ResultCode.BAD_REQUEST,
@@ -477,7 +462,7 @@ public class TenantService {
tenant.setLegalName(request.legalName()); tenant.setLegalName(request.legalName());
tenant.setLegalRepresentative(request.legalRepresentative()); tenant.setLegalRepresentative(request.legalRepresentative());
tenant.setContactName(request.contactName()); tenant.setContactName(request.contactName());
tenant.setContactPhone(request.contactPhone()); tenant.setContactPhone(normalizeContactPhone(request.contactPhone()));
tenant.setContactEmail(request.contactEmail()); tenant.setContactEmail(request.contactEmail());
repository.saveTenant(tenant); repository.saveTenant(tenant);
return merchantVisibleResponse(tenant); return merchantVisibleResponse(tenant);
@@ -497,9 +482,9 @@ public class TenantService {
return contactPhone == null ? "" : contactPhone.trim(); return contactPhone == null ? "" : contactPhone.trim();
} }
private void ensureContactPhoneAvailable(String contactPhone, Long tenantId) { private void ensureAdministratorPhoneAvailable(String phone) {
if (!contactPhoneAvailable(contactPhone, tenantId)) { if (!administratorPhoneAvailable(phone)) {
throw new BizException(ResultCode.CONFLICT, "联系电话已被其他客户使用,不能作为登录用户名"); throw new BizException(ResultCode.CONFLICT, "管理员登录手机号已被其他账号使用");
} }
} }
+2 -2
View File
@@ -16,7 +16,7 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
legal_representative VARCHAR(64) NULL COMMENT '法定代表人', legal_representative VARCHAR(64) NULL COMMENT '法定代表人',
unified_social_credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码', unified_social_credit_code VARCHAR(32) NULL COMMENT '统一社会信用代码',
contact_name VARCHAR(64) NULL COMMENT '企业联系人', contact_name VARCHAR(64) NULL COMMENT '企业联系人',
contact_phone VARCHAR(32) NULL COMMENT '联系电话', contact_phone VARCHAR(32) NULL COMMENT '公司联系电话,非登录标识且不要求跨企业唯一',
contact_email VARCHAR(128) NULL COMMENT '联系邮箱', contact_email VARCHAR(128) NULL COMMENT '联系邮箱',
service_package VARCHAR(64) NOT NULL DEFAULT '基础版' COMMENT '客户服务套餐', service_package VARCHAR(64) NOT NULL DEFAULT '基础版' COMMENT '客户服务套餐',
contact_wechat VARCHAR(64) NULL COMMENT '联系微信', contact_wechat VARCHAR(64) NULL COMMENT '联系微信',
@@ -119,7 +119,7 @@ CREATE TABLE IF NOT EXISTS iam_user (
auth_user_id BIGINT NOT NULL COMMENT '认证服务账号逻辑ID', auth_user_id BIGINT NOT NULL COMMENT '认证服务账号逻辑ID',
user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型:ENTERPRISE企业用户、PLATFORM平台用户', user_type VARCHAR(20) NOT NULL DEFAULT 'ENTERPRISE' COMMENT '用户类型:ENTERPRISE企业用户、PLATFORM平台用户',
display_name VARCHAR(64) NOT NULL COMMENT '用户展示名称', display_name VARCHAR(64) NOT NULL COMMENT '用户展示名称',
phone VARCHAR(32) NULL COMMENT '手机号', phone VARCHAR(32) NULL COMMENT '用户登录手机号,与企业联系电话独立',
email VARCHAR(128) NULL COMMENT '邮箱', email VARCHAR(128) NULL COMMENT '邮箱',
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态:ACTIVE正常、DISABLED停用', status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' COMMENT '用户资料状态:ACTIVE正常、DISABLED停用',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
+18 -18
View File
@@ -67,16 +67,16 @@ paths:
'401': {$ref: '#/components/responses/Unauthorized'} '401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'} '403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'} '409': {$ref: '#/components/responses/Conflict'}
/api/platform/v1/iam/tenants/contact-phone-availability: /api/platform/v1/iam/tenants/administrator-phone-availability:
get: get:
tags: [Platform Tenant / 平台租户] tags: [Platform Tenant / 平台租户]
summary: 校验客户联系电话唯一性 summary: 校验首个管理员登录手机号唯一性
description: 仅在创建客户前校验 Auth 登录手机号;公司联系电话不要求跨企业唯一。
parameters: parameters:
- {name: contactPhone, in: query, required: true, schema: {type: string}} - {name: phone, in: query, required: true, schema: {type: string, pattern: '^1[3-9]\d{9}$'}}
- {name: excludeTenantId, in: query, required: false, schema: {type: integer, format: int64}}
responses: responses:
'200': '200':
description: 返回联系电话是否可作为登录用户名 description: 返回手机号是否可作为首个管理员登录手机号
content: content:
application/json: application/json:
schema: schema:
@@ -211,7 +211,7 @@ paths:
post: post:
tags: [Platform Tenant / 平台租户] tags: [Platform Tenant / 平台租户]
summary: 提交企业客户审核 summary: 提交企业客户审核
description: 校验营业执照和法人身份证已上传后创建审核单,并把客户状态改为 `PENDING`;保存资料和上传文件均不会自动调用本接口。 description: 校验营业执照和统一社会信用代码后创建审核单,并把客户状态改为 `PENDING`;法人身份证选填,保存资料和上传文件均不会自动调用本接口。
responses: responses:
'200': {$ref: '#/components/responses/TenantSuccess'} '200': {$ref: '#/components/responses/TenantSuccess'}
'400': {$ref: '#/components/responses/BadRequest'} '400': {$ref: '#/components/responses/BadRequest'}
@@ -683,7 +683,7 @@ paths:
post: post:
tags: [IAM Resources / IAM 资源] tags: [IAM Resources / IAM 资源]
summary: Submit enterprise qualifications for review / 提交企业资质审核 summary: Submit enterprise qualifications for review / 提交企业资质审核
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review after the business license and legal-person ID have been provided; the freight-forwarder certificate and bank account license are optional. / 上传或替换资质不会自动送审;营业执照和法人身份证齐全后由此接口创建审核单,货代资质证书和银行开户许可证选填。 description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint requires only the business license and unified social credit code; legal-person ID, freight-forwarder certificate and bank account license are optional. / 上传或替换资质不会自动送审;本接口只要求营业执照和统一社会信用代码,法人身份证、货代资质证书和银行开户许可证选填。
responses: responses:
'200': {$ref: '#/components/responses/TenantSuccess'} '200': {$ref: '#/components/responses/TenantSuccess'}
'400': {$ref: '#/components/responses/BadRequest'} '400': {$ref: '#/components/responses/BadRequest'}
@@ -813,8 +813,8 @@ paths:
/internal/iam/required-qualification-packages: /internal/iam/required-qualification-packages:
post: post:
tags: [IAM Resources / IAM 资源] tags: [IAM Resources / IAM 资源]
summary: 商户必填资质包登记 summary: 商户营业执照资质包登记
description: 仅供 File Service 调用;在一个 IAM 事务中登记营业执照、统一社会信用代码和法人身份证。 description: 仅供 File Service 调用;在一个 IAM 事务中登记必填营业执照与统一社会信用代码,法人身份证可选。
security: [] security: []
parameters: parameters:
- {$ref: '#/components/parameters/InternalServiceToken'} - {$ref: '#/components/parameters/InternalServiceToken'}
@@ -833,8 +833,8 @@ paths:
/internal/iam/platform-required-qualification-packages: /internal/iam/platform-required-qualification-packages:
post: post:
tags: [IAM Resources / IAM 资源] tags: [IAM Resources / IAM 资源]
summary: 平台代客户必填资质包登记 summary: 平台代客户登记营业执照资质包
description: 仅供 File Service 调用;与商户入口使用相同的三项事务保存规则。 description: 仅供 File Service 调用;与商户入口使用相同规则,营业执照与统一社会信用代码必填,法人身份证可选。
security: [] security: []
parameters: parameters:
- {$ref: '#/components/parameters/InternalServiceToken'} - {$ref: '#/components/parameters/InternalServiceToken'}
@@ -1318,7 +1318,7 @@ components:
legalName: {type: string, example: 深圳虫虫出海科技有限公司} legalName: {type: string, example: 深圳虫虫出海科技有限公司}
legalRepresentative: {type: string, maxLength: 64, example: 张三} legalRepresentative: {type: string, maxLength: 64, example: 张三}
contactName: {type: string, example: 李四} contactName: {type: string, example: 李四}
contactPhone: {type: string, example: '13800138000'} contactPhone: {type: string, pattern: '^1[3-9]\d{9}$', example: '13800138000', description: 创建时同时初始化公司联系电话与首个管理员登录手机号;创建后二者独立维护}
contactEmail: {type: string, format: email, example: contact@example.com} contactEmail: {type: string, format: email, example: contact@example.com}
contactWechat: {type: string, maxLength: 64} contactWechat: {type: string, maxLength: 64}
servicePackage: {type: string, maxLength: 64, example: 基础版} servicePackage: {type: string, maxLength: 64, example: 基础版}
@@ -1338,7 +1338,7 @@ components:
legalName: {type: string} legalName: {type: string}
legalRepresentative: {type: string, maxLength: 64} legalRepresentative: {type: string, maxLength: 64}
contactName: {type: string} contactName: {type: string}
contactPhone: {type: string} contactPhone: {type: string, pattern: '^[0-9-]{4,32}$', description: 公司联系电话,不作为登录标识且不要求跨企业唯一}
contactEmail: {type: string, format: email} contactEmail: {type: string, format: email}
UpdateTenantRequest: UpdateTenantRequest:
type: object type: object
@@ -1348,7 +1348,7 @@ components:
legalName: {type: string} legalName: {type: string}
legalRepresentative: {type: string, maxLength: 64} legalRepresentative: {type: string, maxLength: 64}
contactName: {type: string} contactName: {type: string}
contactPhone: {type: string} contactPhone: {type: string, pattern: '^[0-9-]{4,32}$', description: 公司联系电话,与管理员及成员登录手机号独立且不要求跨企业唯一}
contactEmail: {type: string, format: email} contactEmail: {type: string, format: email}
contactWechat: {type: string, maxLength: 64} contactWechat: {type: string, maxLength: 64}
servicePackage: {type: string, maxLength: 64, example: 基础版} servicePackage: {type: string, maxLength: 64, example: 基础版}
@@ -1398,7 +1398,7 @@ components:
properties: properties:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1[3-9]\\d{9}$'} phone: {type: string, pattern: '^1[3-9]\\d{9}$', description: 平台账号登录手机号;修改时同步 Auth、撤销该账号会话并审计}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}} roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
@@ -1410,7 +1410,7 @@ components:
required: [displayName, phone, tenantId, roleIds] required: [displayName, phone, tenantId, roleIds]
properties: properties:
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1[3-9]\\d{9}$'} phone: {type: string, pattern: '^1[3-9]\\d{9}$', description: 平台账号登录手机号;与任何企业联系电话无关}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0} tenantId: {type: integer, format: int64, enum: [0], description: 保留的兼容字段,只允许平台技术作用域 0}
roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}} roleIds: {type: array, minItems: 1, items: {type: integer, format: int64}}
@@ -1424,7 +1424,7 @@ components:
username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'} username: {type: string, pattern: '^[A-Za-z][A-Za-z0-9_.@-]{3,63}$'}
password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true} password: {type: string, format: password, minLength: 8, maxLength: 64, writeOnly: true}
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1[3-9]\d{9}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$', description: 企业成员登录手机号;修改时同步 Auth、撤销该账号会话并审计}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberNo: {type: string, maxLength: 64} memberNo: {type: string, maxLength: 64}
roleIds: {type: array, items: {type: integer, format: int64}} roleIds: {type: array, items: {type: integer, format: int64}}
@@ -1433,7 +1433,7 @@ components:
required: [displayName, memberNo, roleIds] required: [displayName, memberNo, roleIds]
properties: properties:
displayName: {type: string, maxLength: 64} displayName: {type: string, maxLength: 64}
phone: {type: string, pattern: '^1[3-9]\d{9}$'} phone: {type: string, pattern: '^1[3-9]\d{9}$', description: 企业成员登录手机号;与公司联系电话无关}
email: {type: string, format: email, maxLength: 128} email: {type: string, format: email, maxLength: 128}
memberNo: {type: string, maxLength: 64} memberNo: {type: string, maxLength: 64}
roleIds: {type: array, items: {type: integer, format: int64}} roleIds: {type: array, items: {type: integer, format: int64}}