Merge test into dev and resolve conflicts
This commit is contained in:
@@ -59,6 +59,14 @@ public class InternalTenantController {
|
||||
return ApiResponse.success(onboardingService.register(request));
|
||||
}
|
||||
|
||||
@GetMapping("/merchant-registrations/{authUserId}")
|
||||
public ApiResponse<MerchantRegistrationResponse> registration(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@PathVariable long authUserId) {
|
||||
verify(token);
|
||||
return ApiResponse.success(onboardingService.registration(authUserId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/merchant-registrations/{authUserId}/compensation")
|
||||
public ApiResponse<Void> compensateRegistration(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@@ -127,7 +135,7 @@ public class InternalTenantController {
|
||||
@RequestParam(defaultValue = "500") @Min(1) @Max(500) int limit,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false)
|
||||
@Pattern(regexp = "PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED|ARCHIVED") String status) {
|
||||
@Pattern(regexp = "PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED") String status) {
|
||||
verify(token);
|
||||
return ApiResponse.success(tenantService.exportMerchants(cursor, limit, keyword, status));
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ public class PlatformTenantController {
|
||||
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size,
|
||||
@RequestParam(required = false) String keyword,
|
||||
@RequestParam(required = false)
|
||||
@Pattern(regexp = "PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED|ARCHIVED") String status,
|
||||
@Pattern(regexp = "PROFILE_REQUIRED|PENDING|ACTIVE|REJECTED|DISABLED") String status,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.pageMerchants(page, size, keyword, status));
|
||||
|
||||
@@ -3,9 +3,10 @@ package com.baiyue.iam.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.iam.dto.ProfileDtos.AddressRequest;
|
||||
import com.baiyue.iam.dto.ProfileDtos.QualificationDeletionResponse;
|
||||
import com.baiyue.iam.dto.ProfileDtos.QualificationResponse;
|
||||
import com.baiyue.iam.dto.PlatformInternalUserDtos.*;
|
||||
import com.baiyue.iam.entity.IamTenantAddress;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.service.ProfileResourceService;
|
||||
import com.baiyue.iam.service.PlatformInternalUserService;
|
||||
import com.baiyue.iam.service.TenantGuard;
|
||||
@@ -108,21 +109,48 @@ public class ProfileResourceController {
|
||||
}
|
||||
|
||||
@GetMapping("/api/merchant/v1/iam/tenant/qualifications")
|
||||
@PreAuthorize("hasAuthority('merchant:profile:view')")
|
||||
@Operation(summary = "List enterprise qualifications / 查询企业资质", description = "List qualification files owned by the current tenant. / 查询当前租户的资质文件。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Qualification list returned / 查询成功")})
|
||||
public ApiResponse<List<IamTenantQualification>> qualifications(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
public ApiResponse<List<QualificationResponse>> qualifications(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.qualifications(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@GetMapping("/api/merchant/v1/iam/tenant/qualifications/{qualificationId}")
|
||||
@PreAuthorize("hasAuthority('merchant:profile:view')")
|
||||
@Operation(summary = "查询企业资质详情", description = "客户范围来自 JWT,跨客户资质按不存在处理。")
|
||||
public ApiResponse<QualificationResponse> qualification(
|
||||
@PathVariable long qualificationId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.qualification(TenantGuard.tenant(user, "MERCHANT"), qualificationId));
|
||||
}
|
||||
|
||||
@DeleteMapping("/api/merchant/v1/iam/tenant/qualifications/{qualificationId}")
|
||||
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
||||
@Operation(summary = "删除送审前企业资质", description = "仅待完善或已驳回客户可删除;提交审核后服务端拒绝。重复删除幂等成功。")
|
||||
public ApiResponse<QualificationDeletionResponse> deleteQualification(
|
||||
@PathVariable long qualificationId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(service.deleteQualification(TenantGuard.tenant(user, "MERCHANT"), qualificationId));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/qualifications")
|
||||
@PreAuthorize("hasAuthority('platform:customer:list')")
|
||||
@Operation(summary = "查询企业客户资质", description = "只读查询企业客户资质元数据,不授予审核能力。")
|
||||
public ApiResponse<List<IamTenantQualification>> platformQualifications(
|
||||
public ApiResponse<List<QualificationResponse>> platformQualifications(
|
||||
@PathVariable long tenantId, @AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.platformQualifications(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/qualifications/{qualificationId}")
|
||||
@PreAuthorize("hasAuthority('platform:customer:list')")
|
||||
@Operation(summary = "查询企业客户资质详情", description = "需要客户列表权限,且同时校验客户与资质归属。")
|
||||
public ApiResponse<QualificationResponse> platformQualification(
|
||||
@PathVariable long tenantId, @PathVariable long qualificationId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
TenantGuard.tenant(user, "PLATFORM");
|
||||
return ApiResponse.success(service.platformQualification(tenantId, qualificationId));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/iam/tenants/{tenantId}/addresses")
|
||||
@PreAuthorize("hasAuthority('platform:customer:list')")
|
||||
@Operation(summary = "查询企业客户地址", description = "只读查询企业客户地址,不开放平台端写入。")
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.baiyue.iam.dto;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@@ -13,7 +14,7 @@ public final class OnboardingDtos {
|
||||
@NotNull Long authUserId,
|
||||
@NotBlank String displayName,
|
||||
@NotBlank String companyName,
|
||||
@NotBlank String legalPerson,
|
||||
@Size(max = 64) String legalPerson,
|
||||
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String administratorPhone,
|
||||
String contactEmail,
|
||||
String countryCode,
|
||||
@@ -28,6 +29,7 @@ public final class OnboardingDtos {
|
||||
@NotBlank @Pattern(regexp = "BUSINESS_LICENSE|LEGAL_PERSON_ID|FREIGHT_FORWARDER_CERTIFICATE|BANK_ACCOUNT_LICENSE") String qualificationType,
|
||||
String certificateNo,
|
||||
@NotBlank String documentUrl,
|
||||
@NotBlank @Size(max = 255) String originalFileName,
|
||||
LocalDate validTo,
|
||||
boolean replace
|
||||
) { }
|
||||
@@ -40,6 +42,7 @@ public final class OnboardingDtos {
|
||||
@NotBlank @Pattern(regexp = "BUSINESS_LICENSE|LEGAL_PERSON_ID|FREIGHT_FORWARDER_CERTIFICATE|BANK_ACCOUNT_LICENSE") String qualificationType,
|
||||
String certificateNo,
|
||||
@NotBlank String documentUrl,
|
||||
@NotBlank @Size(max = 255) String originalFileName,
|
||||
LocalDate validTo,
|
||||
boolean replace
|
||||
) { }
|
||||
@@ -55,9 +58,11 @@ public final class OnboardingDtos {
|
||||
@NotNull Long tenantId,
|
||||
@NotBlank @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
|
||||
@NotBlank String businessLicenseDocumentUrl,
|
||||
@NotBlank @Size(max = 255) String businessLicenseOriginalFileName,
|
||||
LocalDate businessLicenseValidTo,
|
||||
boolean replaceBusinessLicense,
|
||||
String legalPersonIdDocumentUrl,
|
||||
@Size(max = 255) String legalPersonIdOriginalFileName,
|
||||
LocalDate legalPersonIdValidTo,
|
||||
Boolean replaceLegalPersonId
|
||||
) { }
|
||||
|
||||
@@ -5,6 +5,9 @@ import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** 用户档案、地址和资质的数据契约。 */
|
||||
public final class ProfileDtos {
|
||||
private ProfileDtos() { }
|
||||
@@ -24,4 +27,12 @@ public final class ProfileDtos {
|
||||
String city, String district, @NotBlank String detail,
|
||||
String postalCode, Boolean isDefault) { }
|
||||
|
||||
public record QualificationResponse(Long id, Long tenantId, String qualificationType,
|
||||
String certificateNo, String documentUrl, String originalFileName,
|
||||
LocalDate validFrom, LocalDate validTo, String status,
|
||||
String reviewComment, LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt) { }
|
||||
|
||||
public record QualificationDeletionResponse(Long qualificationId, String deletedDocumentUrl) { }
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public final class TenantDtos {
|
||||
public record RegisterRequest(
|
||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
||||
@Schema(description = "Legal representative (optional) / 法定代表人(选填)", example = "张三") @Size(max = 64) String legalRepresentative,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||
@Schema(description = "Company contact phone; also initializes the first administrator login phone at creation, then maintained independently / 公司联系电话;创建时同时初始化首个管理员登录手机号,之后独立维护", example = "13800138000")
|
||||
@NotBlank @Pattern(regexp = "^1[3-9]\\d{9}$") String contactPhone,
|
||||
@@ -46,7 +46,7 @@ public final class TenantDtos {
|
||||
public record UpdateProfileRequest(
|
||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
||||
@Schema(description = "Legal representative (optional) / 法定代表人(选填)", example = "张三") @Size(max = 64) String legalRepresentative,
|
||||
@Schema(description = "Primary contact / 联系人", example = "李四") @NotBlank String contactName,
|
||||
@Schema(description = "Company contact phone, never used to resolve login identity / 公司联系电话,不作为登录标识", example = "0755-12345678")
|
||||
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
|
||||
@@ -58,7 +58,7 @@ public final class TenantDtos {
|
||||
public record UpdateTenantRequest(
|
||||
@NotBlank String tenantName,
|
||||
@NotBlank String legalName,
|
||||
@NotBlank @Size(max = 64) String legalRepresentative,
|
||||
@Size(max = 64) String legalRepresentative,
|
||||
@NotBlank String contactName,
|
||||
@Schema(description = "Company contact phone, independent from every member login phone / 公司联系电话,与所有成员登录手机号独立")
|
||||
@NotBlank @Pattern(regexp = "[0-9-]{4,32}") String contactPhone,
|
||||
@@ -110,7 +110,7 @@ public final class TenantDtos {
|
||||
@Schema(description = "Review type / 审核类型", example = "REGISTER") String reviewType,
|
||||
@Schema(description = "Review status / 审核状态", example = "PENDING") String status,
|
||||
@Schema(description = "Customer name / 客户名称", example = "深圳虫虫出海科技有限公司") String tenantName,
|
||||
@Schema(description = "Customer code / 客户编号", example = "TEN190000000000000002") String tenantCode,
|
||||
@Schema(description = "Customer code / 五位客户编号", example = "00001") String tenantCode,
|
||||
@Schema(description = "Primary contact / 联系人", example = "张三") String contactName,
|
||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") String contactPhone,
|
||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") String contactEmail,
|
||||
|
||||
@@ -2,10 +2,10 @@ package com.baiyue.iam.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDate;
|
||||
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import java.time.LocalDate; import java.time.LocalDateTime;
|
||||
/**
|
||||
* 租户资质实体,对应表 {@code iam_tenant_qualification};按租户保存营业执照等证照、有效期、文件位置和审核状态。
|
||||
*/
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("iam_tenant_qualification") public class IamTenantQualification { @TableId private Long id; private Long tenantId; private String qualificationType; private String certificateNo; private String documentUrl; private LocalDate validFrom; private LocalDate validTo; private String status; private String reviewComment; }
|
||||
@TableName("iam_tenant_qualification") public class IamTenantQualification { @TableId private Long id; private Long tenantId; private String qualificationType; private String certificateNo; private String documentUrl; private String originalFileName; private LocalDate validFrom; private LocalDate validTo; private String status; private String reviewComment; private LocalDateTime createdAt; private LocalDateTime updatedAt; @TableLogic private Boolean deleted; }
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.baiyue.iam.repository;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/** 在客户创建事务内串行分配固定五位客户编号。 */
|
||||
@Repository
|
||||
public class CustomerCodeRepository {
|
||||
private static final String SEQUENCE_KEY = "CUSTOMER_CODE";
|
||||
private static final long MAX_CUSTOMER_CODE = 99_999L;
|
||||
|
||||
@Resource
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Transactional(propagation = Propagation.MANDATORY)
|
||||
public String nextCode() {
|
||||
jdbc.update("INSERT IGNORE INTO iam_number_sequence(sequence_key,next_value) VALUES (?,1)",
|
||||
SEQUENCE_KEY);
|
||||
Long value = jdbc.queryForObject(
|
||||
"SELECT next_value FROM iam_number_sequence WHERE sequence_key=? FOR UPDATE",
|
||||
Long.class, SEQUENCE_KEY);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户编号序列状态异常");
|
||||
}
|
||||
String code = format(value);
|
||||
int updated = jdbc.update(
|
||||
"UPDATE iam_number_sequence SET next_value=next_value+1 WHERE sequence_key=? AND next_value=?",
|
||||
SEQUENCE_KEY, value);
|
||||
if (updated != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "客户编号分配冲突,请重试");
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
static String format(long value) {
|
||||
if (value < 1) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户编号序列状态异常");
|
||||
}
|
||||
if (value > MAX_CUSTOMER_CODE) {
|
||||
throw new BizException(ResultCode.CONFLICT, "五位客户编号已用完");
|
||||
}
|
||||
return String.format(Locale.ROOT, "%05d", value);
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,18 @@ public class ProfileResourceRepository {
|
||||
public void saveQualification(IamTenantQualification qualification) { qualificationMapper.updateById(qualification); }
|
||||
public List<IamTenantAddress> findAddresses(long tenantId) { return addressMapper.selectList(new LambdaQueryWrapper<IamTenantAddress>().eq(IamTenantAddress::getTenantId, tenantId)); }
|
||||
public List<IamTenantQualification> findQualifications(long tenantId) { return qualificationMapper.selectList(new LambdaQueryWrapper<IamTenantQualification>().eq(IamTenantQualification::getTenantId, tenantId)); }
|
||||
public IamTenantQualification findQualification(long tenantId, long qualificationId) {
|
||||
return qualificationMapper.selectOne(new LambdaQueryWrapper<IamTenantQualification>()
|
||||
.eq(IamTenantQualification::getTenantId, tenantId)
|
||||
.eq(IamTenantQualification::getId, qualificationId));
|
||||
}
|
||||
public IamTenantQualification findQualificationForUpdate(long tenantId, long qualificationId) {
|
||||
return qualificationMapper.selectOne(new LambdaQueryWrapper<IamTenantQualification>()
|
||||
.eq(IamTenantQualification::getTenantId, tenantId)
|
||||
.eq(IamTenantQualification::getId, qualificationId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
public void deleteQualification(IamTenantQualification qualification) { qualificationMapper.deleteById(qualification); }
|
||||
public List<IamTenantQualification> findQualificationsForUpdate(long tenantId) {
|
||||
return qualificationMapper.selectList(new LambdaQueryWrapper<IamTenantQualification>()
|
||||
.eq(IamTenantQualification::getTenantId, tenantId)
|
||||
|
||||
@@ -83,6 +83,7 @@ public class TenantRepository {
|
||||
public PageResult<IamTenant> pageMerchants(long page, long size, String keyword, String status) {
|
||||
LambdaQueryWrapper<IamTenant> wrapper = new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.ne(IamTenant::getStatus, "ARCHIVED")
|
||||
.eq(StringUtils.hasText(status), IamTenant::getStatus, status)
|
||||
.orderByDesc(IamTenant::getCreatedAt);
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
@@ -114,6 +115,7 @@ public class TenantRepository {
|
||||
public List<IamTenant> exportMerchants(Long cursor, int limit, String keyword, String status) {
|
||||
LambdaQueryWrapper<IamTenant> wrapper = new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.ne(IamTenant::getStatus, "ARCHIVED")
|
||||
.gt(cursor != null, IamTenant::getId, cursor)
|
||||
.eq(StringUtils.hasText(status), IamTenant::getStatus, status)
|
||||
.orderByAsc(IamTenant::getId)
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.baiyue.iam.mapper.IamTenantMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantQualificationMapper;
|
||||
import com.baiyue.iam.mapper.IamTenantReviewMapper;
|
||||
import com.baiyue.iam.mapper.IamUserMapper;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -43,6 +44,7 @@ public class MerchantOnboardingService {
|
||||
@Resource private CustomerDimensionRuleInitializationService dimensionRuleInitialization;
|
||||
@Resource private OverseasStorageFeeInitializationService overseasStorageFeeInitialization;
|
||||
@Resource private SecurityEventOutboxService securityEvents;
|
||||
@Resource private CustomerCodeRepository customerCodes;
|
||||
|
||||
@Transactional
|
||||
public MerchantRegistrationResponse register(MerchantRegistrationRequest request) {
|
||||
@@ -66,13 +68,13 @@ public class MerchantOnboardingService {
|
||||
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(tenantId);
|
||||
tenant.setTenantCode("TEN" + tenantId);
|
||||
tenant.setTenantCode(customerCodes.nextCode());
|
||||
tenant.setTenantName(request.companyName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
tenant.setDimensionInitializationStatus("PENDING");
|
||||
tenant.setLegalName(request.companyName());
|
||||
tenant.setLegalRepresentative(request.legalPerson());
|
||||
tenant.setLegalRepresentative(normalizeOptional(request.legalPerson()));
|
||||
tenant.setContactName(request.displayName());
|
||||
tenant.setContactPhone(request.administratorPhone());
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
@@ -131,6 +133,32 @@ public class MerchantOnboardingService {
|
||||
tenant.getDimensionInitializationStatus());
|
||||
}
|
||||
|
||||
public MerchantRegistrationResponse registration(long authUserId) {
|
||||
IamUser user = userMapper.selectOne(new LambdaQueryWrapper<IamUser>()
|
||||
.eq(IamUser::getAuthUserId, authUserId)
|
||||
.last("LIMIT 1"));
|
||||
if (user == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Registration not found");
|
||||
}
|
||||
IamMembership membership = membershipMapper.selectOne(new LambdaQueryWrapper<IamMembership>()
|
||||
.eq(IamMembership::getUserId, user.getId())
|
||||
.eq(IamMembership::getMemberType, "OWNER")
|
||||
.last("LIMIT 1"));
|
||||
if (membership == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Registration not found");
|
||||
}
|
||||
IamTenant tenant = tenantMapper.selectById(membership.getTenantId());
|
||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Registration not found");
|
||||
}
|
||||
return new MerchantRegistrationResponse(tenant.getId(), membership.getId(), tenant.getStatus(),
|
||||
tenant.getDimensionInitializationStatus());
|
||||
}
|
||||
|
||||
private String normalizeOptional(String value) {
|
||||
return StringUtils.hasText(value) ? value.trim() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a just-created IAM registration when Auth could not persist the paired login account.
|
||||
* Repeated calls are safe, while progressed onboarding data is deliberately protected.
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRespon
|
||||
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
|
||||
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageResponse;
|
||||
import com.baiyue.iam.dto.ProfileDtos.AddressRequest;
|
||||
import com.baiyue.iam.dto.ProfileDtos.QualificationDeletionResponse;
|
||||
import com.baiyue.iam.dto.ProfileDtos.QualificationResponse;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.entity.IamTenantAddress;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
@@ -43,16 +45,50 @@ public class ProfileResourceService {
|
||||
repository.insertAddress(a); return a;
|
||||
}
|
||||
|
||||
public List<IamTenantQualification> qualifications(long tenant) { return repository.findQualifications(tenant); }
|
||||
public List<QualificationResponse> qualifications(long tenant) {
|
||||
return repository.findQualifications(tenant).stream().map(this::toQualificationResponse).toList();
|
||||
}
|
||||
|
||||
public QualificationResponse qualification(long tenant, long qualificationId) {
|
||||
IamTenantQualification qualification = repository.findQualification(tenant, qualificationId);
|
||||
if (qualification == null) throw new BizException(ResultCode.NOT_FOUND, "Qualification not found / 企业资质不存在");
|
||||
return toQualificationResponse(qualification);
|
||||
}
|
||||
|
||||
public List<IamTenantAddress> platformAddresses(long tenantId) {
|
||||
requireMerchant(tenantId);
|
||||
return repository.findAddresses(tenantId);
|
||||
}
|
||||
|
||||
public List<IamTenantQualification> platformQualifications(long tenantId) {
|
||||
public List<QualificationResponse> platformQualifications(long tenantId) {
|
||||
requireMerchant(tenantId);
|
||||
return repository.findQualifications(tenantId);
|
||||
return qualifications(tenantId);
|
||||
}
|
||||
|
||||
public QualificationResponse platformQualification(long tenantId, long qualificationId) {
|
||||
requireMerchant(tenantId);
|
||||
return qualification(tenantId, qualificationId);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "TENANT_QUALIFICATION_DELETE", resourceType = "TENANT_QUALIFICATION",
|
||||
resourceId = "#qualificationId", description = "删除送审前企业资质", captureAfter = false)
|
||||
public QualificationDeletionResponse deleteQualification(long tenantId, long qualificationId) {
|
||||
IamTenant tenant = tenantRepository.findTenantForUpdate(tenantId);
|
||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found / 企业客户不存在");
|
||||
}
|
||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Qualifications cannot be deleted after review submission / 资质提交审核后不可删除");
|
||||
}
|
||||
IamTenantQualification qualification = repository.findQualificationForUpdate(tenantId, qualificationId);
|
||||
if (qualification == null) {
|
||||
return new QualificationDeletionResponse(qualificationId, null);
|
||||
}
|
||||
String documentUrl = qualification.getDocumentUrl();
|
||||
repository.deleteQualification(qualification);
|
||||
return new QualificationDeletionResponse(qualificationId, documentUrl);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -86,7 +122,7 @@ public class ProfileResourceService {
|
||||
repository.saveQualification(existing);
|
||||
}
|
||||
IamTenantQualification qualification = createQualification(tenant.getId(),
|
||||
request.qualificationType(), certificateNo, request.documentUrl());
|
||||
request.qualificationType(), certificateNo, request.documentUrl(), request.originalFileName());
|
||||
qualification.setValidTo(request.validTo());
|
||||
repository.saveQualification(qualification);
|
||||
return new QualificationSubmissionResponse(qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
||||
@@ -122,7 +158,7 @@ public class ProfileResourceService {
|
||||
repository.saveQualification(existing);
|
||||
}
|
||||
IamTenantQualification qualification = createQualification(tenant.getId(), request.qualificationType(),
|
||||
certificateNo, request.documentUrl());
|
||||
certificateNo, request.documentUrl(), request.originalFileName());
|
||||
qualification.setValidTo(request.validTo());
|
||||
repository.saveQualification(qualification);
|
||||
return new PlatformQualificationRegistrationResponse(
|
||||
@@ -162,6 +198,7 @@ public class ProfileResourceService {
|
||||
requireReplaceAllowed(businessLicenses, request.replaceBusinessLicense(), "营业执照");
|
||||
boolean hasLegalPersonId = StringUtils.hasText(request.legalPersonIdDocumentUrl());
|
||||
if (hasLegalPersonId) {
|
||||
normalizeOriginalFileName(request.legalPersonIdOriginalFileName());
|
||||
requireReplaceAllowed(legalPersonIds, Boolean.TRUE.equals(request.replaceLegalPersonId()), "法人身份证");
|
||||
} else if (Boolean.TRUE.equals(request.replaceLegalPersonId())) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "未上传法人身份证时不能选择替换");
|
||||
@@ -170,13 +207,13 @@ public class ProfileResourceService {
|
||||
String replacedBusinessLicenseUrl = supersede(businessLicenses, replacementComment);
|
||||
String replacedLegalPersonIdUrl = hasLegalPersonId ? supersede(legalPersonIds, replacementComment) : null;
|
||||
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
||||
creditCode, request.businessLicenseDocumentUrl());
|
||||
creditCode, request.businessLicenseDocumentUrl(), request.businessLicenseOriginalFileName());
|
||||
businessLicense.setValidTo(request.businessLicenseValidTo());
|
||||
repository.saveQualification(businessLicense);
|
||||
IamTenantQualification legalPersonId = null;
|
||||
if (hasLegalPersonId) {
|
||||
legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
||||
null, request.legalPersonIdDocumentUrl().trim());
|
||||
null, request.legalPersonIdDocumentUrl().trim(), request.legalPersonIdOriginalFileName());
|
||||
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
||||
repository.saveQualification(legalPersonId);
|
||||
}
|
||||
@@ -243,13 +280,32 @@ public class ProfileResourceService {
|
||||
return certificateNo;
|
||||
}
|
||||
|
||||
private IamTenantQualification createQualification(long tenant, String type, String certificateNo, String url) {
|
||||
private IamTenantQualification createQualification(long tenant, String type, String certificateNo, String url,
|
||||
String originalFileName) {
|
||||
IamTenantQualification x = new IamTenantQualification();
|
||||
x.setId(IdWorker.getId()); x.setTenantId(tenant); x.setQualificationType(type);
|
||||
x.setCertificateNo(certificateNo); x.setDocumentUrl(url); x.setStatus("PENDING");
|
||||
x.setCertificateNo(certificateNo); x.setDocumentUrl(url);
|
||||
x.setOriginalFileName(normalizeOriginalFileName(originalFileName)); x.setStatus("PENDING");
|
||||
repository.insertQualification(x); return x;
|
||||
}
|
||||
|
||||
private String normalizeOriginalFileName(String originalFileName) {
|
||||
if (!StringUtils.hasText(originalFileName)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Original file name is required / 必须提供上传时原始文件名");
|
||||
}
|
||||
String normalized = originalFileName.trim();
|
||||
if (normalized.length() > 255) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Original file name is too long / 原始文件名不能超过255个字符");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private QualificationResponse toQualificationResponse(IamTenantQualification item) {
|
||||
return new QualificationResponse(item.getId(), item.getTenantId(), item.getQualificationType(),
|
||||
item.getCertificateNo(), item.getDocumentUrl(), item.getOriginalFileName(), item.getValidFrom(),
|
||||
item.getValidTo(), item.getStatus(), item.getReviewComment(), item.getCreatedAt(), item.getUpdatedAt());
|
||||
}
|
||||
|
||||
private boolean isCurrentQualification(IamTenantQualification qualification) {
|
||||
return !"REJECTED".equals(qualification.getStatus())
|
||||
&& !"SUPERSEDED".equals(qualification.getStatus());
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.stream.Collectors;
|
||||
public class SecurityEventOutboxService {
|
||||
private static final List<String> OVERSEAS_AUTHORITY_PREFIXES = List.of(
|
||||
"platform:overseas-warehouse:", "platform:overseas-business:",
|
||||
"platform:overseas-master-data:", "merchant:warehouse:",
|
||||
"platform:overseas-master-data:", "merchant:warehouse:", "merchant:overseas-warehouse:",
|
||||
"merchant:cross-border:", "platform:finance:", "lastleg:");
|
||||
@Resource private JdbcTemplate jdbc;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.entity.IamTenantQualification;
|
||||
import com.baiyue.iam.entity.IamTenantReview;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import com.baiyue.iam.repository.CustomerCodeRepository;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.client.AuthAccountClient;
|
||||
import com.baiyue.iam.repository.MemberRepository;
|
||||
@@ -56,6 +57,8 @@ public class TenantService {
|
||||
@Resource
|
||||
private TenantRepository repository;
|
||||
@Resource
|
||||
private CustomerCodeRepository customerCodes;
|
||||
@Resource
|
||||
private ProfileResourceRepository profileRepository;
|
||||
@Resource
|
||||
private AuthAccountClient authClient;
|
||||
@@ -165,13 +168,13 @@ public class TenantService {
|
||||
ensureAdministratorPhoneAvailable(contactPhone);
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(IdWorker.getId());
|
||||
tenant.setTenantCode("TEN" + tenant.getId());
|
||||
tenant.setTenantCode(customerCodes.nextCode());
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
tenant.setDimensionInitializationStatus("PENDING");
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
tenant.setLegalRepresentative(normalizeOptional(request.legalRepresentative()));
|
||||
tenant.setUnifiedSocialCreditCode(null);
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(contactPhone);
|
||||
@@ -388,7 +391,7 @@ public class TenantService {
|
||||
String contactPhone = normalizeContactPhone(request.contactPhone());
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
tenant.setLegalRepresentative(normalizeOptional(request.legalRepresentative()));
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(contactPhone);
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
@@ -460,7 +463,7 @@ public class TenantService {
|
||||
ensureNotArchived(tenant);
|
||||
tenant.setTenantName(request.tenantName());
|
||||
tenant.setLegalName(request.legalName());
|
||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||
tenant.setLegalRepresentative(normalizeOptional(request.legalRepresentative()));
|
||||
tenant.setContactName(request.contactName());
|
||||
tenant.setContactPhone(normalizeContactPhone(request.contactPhone()));
|
||||
tenant.setContactEmail(request.contactEmail());
|
||||
@@ -482,6 +485,10 @@ public class TenantService {
|
||||
return contactPhone == null ? "" : contactPhone.trim();
|
||||
}
|
||||
|
||||
private String normalizeOptional(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
|
||||
private void ensureAdministratorPhoneAvailable(String phone) {
|
||||
if (!administratorPhoneAvailable(phone)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "管理员登录手机号已被其他账号使用");
|
||||
|
||||
@@ -7,7 +7,7 @@ USE baiyue_iam;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
id BIGINT NOT NULL COMMENT '客户主档ID(雪花ID);0保留为平台技术作用域',
|
||||
tenant_code VARCHAR(64) NOT NULL COMMENT '全局唯一客户编码',
|
||||
tenant_code VARCHAR(8) NOT NULL COMMENT '客户编号:企业客户固定五位数字,平台技术主体为PLATFORM',
|
||||
tenant_name VARCHAR(128) NOT NULL COMMENT '客户企业名称',
|
||||
tenant_type VARCHAR(20) NOT NULL DEFAULT 'MERCHANT' COMMENT '主体类型:MERCHANT企业客户',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PROFILE_REQUIRED' COMMENT '客户状态:PROFILE_REQUIRED待完善资料、PENDING待审核、ACTIVE正常、REJECTED已驳回、DISABLED已停用、ARCHIVED已归档',
|
||||
@@ -43,6 +43,15 @@ CREATE TABLE IF NOT EXISTS iam_tenant (
|
||||
KEY idx_iam_tenant_status (status, created_at)
|
||||
) ENGINE=InnoDB COMMENT='客户企业主档表:保存企业资料、审核结果和启停状态;ID同时作为客户数据隔离键';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_number_sequence (
|
||||
sequence_key VARCHAR(64) NOT NULL COMMENT '业务编号类型',
|
||||
next_value BIGINT UNSIGNED NOT NULL COMMENT '下一待分配数字',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
PRIMARY KEY (sequence_key)
|
||||
) ENGINE=InnoDB COMMENT='IAM业务编号序列表';
|
||||
|
||||
INSERT IGNORE INTO iam_number_sequence(sequence_key, next_value) VALUES ('CUSTOMER_CODE', 1);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS iam_customer_dimension_initialization_task (
|
||||
tenant_id BIGINT NOT NULL COMMENT '客户ID及任务聚合ID',
|
||||
operator_id BIGINT NOT NULL COMMENT '触发初始化的操作人ID;系统补偿为0',
|
||||
@@ -103,6 +112,7 @@ CREATE TABLE IF NOT EXISTS iam_tenant_qualification (
|
||||
qualification_type VARCHAR(32) NOT NULL COMMENT '客户资质类型',
|
||||
certificate_no VARCHAR(128) NULL COMMENT '证书编号',
|
||||
document_url VARCHAR(500) NULL COMMENT '资质文件存储地址',
|
||||
original_file_name VARCHAR(255) NOT NULL COMMENT '上传时原始文件名',
|
||||
valid_from DATE NULL COMMENT '有效期开始日期',
|
||||
valid_to DATE NULL COMMENT '有效期结束日期',
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '资质审核状态:PENDING待审核、APPROVED已通过、REJECTED已驳回',
|
||||
|
||||
@@ -95,7 +95,7 @@ paths:
|
||||
- {name: page, in: query, required: false, schema: {type: integer, minimum: 1, default: 1}}
|
||||
- {name: size, in: query, required: false, schema: {type: integer, minimum: 1, maximum: 200, default: 20}}
|
||||
- {name: keyword, in: query, required: false, schema: {type: string}, description: 客户名称关键字,不支持客户编号、信用代码或联系人搜索}
|
||||
- {name: status, in: query, required: false, schema: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}}
|
||||
- {name: status, in: query, required: false, schema: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED]}}
|
||||
responses:
|
||||
'200':
|
||||
description: 企业客户分页查询成功
|
||||
@@ -248,6 +248,22 @@ paths:
|
||||
schema: {$ref: '#/components/schemas/TenantQualificationListApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/platform/v1/iam/tenants/{tenantId}/qualifications/{qualificationId}:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
- {name: qualificationId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
get:
|
||||
tags: [Platform Tenant / 平台租户]
|
||||
summary: 查询企业客户资质详情
|
||||
description: 需要 `platform:customer:list` 权限,并同时校验客户与资质归属。
|
||||
responses:
|
||||
'200':
|
||||
description: 企业客户资质详情查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantQualificationApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/api/platform/v1/iam/tenants/{tenantId}/status:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/TenantId'}
|
||||
@@ -677,8 +693,39 @@ paths:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: List tenant qualifications / 查询企业资质
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'200':
|
||||
description: 当前客户资质列表
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantQualificationListApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/merchant/v1/iam/tenant/qualifications/{qualificationId}:
|
||||
parameters:
|
||||
- {name: qualificationId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询当前客户资质详情
|
||||
description: 需要 `merchant:profile:view` 权限;客户范围来自 JWT,跨客户资质按不存在处理。
|
||||
responses:
|
||||
'200':
|
||||
description: 当前客户资质详情
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/TenantQualificationApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
delete:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 删除送审前企业资质
|
||||
description: 需要 `merchant:qualification:upload` 权限;仅客户状态为 `PROFILE_REQUIRED` 或 `REJECTED` 时允许,`PENDING`、`ACTIVE` 等提交审核后的状态由服务端拒绝。客户范围来自 JWT;重复删除或跨客户 ID 返回相同幂等结果,不泄露归属。
|
||||
responses:
|
||||
'200':
|
||||
description: 删除成功或目标已不存在
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/QualificationDeletionApiResponse'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/merchant/v1/iam/tenant/qualifications/submit-review:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -810,6 +857,69 @@ paths:
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/internal/iam/merchant-registrations/{authUserId}:
|
||||
get:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 查询已创建的企业注册状态
|
||||
description: 仅供 Auth Service 对相同账号、手机号和密码的注册重试进行幂等确认,不创建或修改企业资料。
|
||||
security: []
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||
- {name: authUserId, in: path, required: true, schema: {type: integer, format: int64}}
|
||||
responses:
|
||||
'200':
|
||||
description: 注册状态查询成功
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required: [tenantId, membershipId, tenantStatus, dimensionInitializationStatus]
|
||||
properties:
|
||||
tenantId: {type: integer, format: int64}
|
||||
membershipId: {type: integer, format: int64}
|
||||
tenantStatus: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}
|
||||
dimensionInitializationStatus: {type: string, enum: [PENDING, READY, FAILED]}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'404': {$ref: '#/components/responses/NotFound'}
|
||||
/internal/iam/qualifications:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 登记客户单个选填资质
|
||||
description: 仅供 File Service 调用;原始文件名必须由上传请求传入,不得从存储 URL 推断。
|
||||
security: []
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/QualificationSubmissionRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/internal/iam/platform-qualifications:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: 平台代客户登记单个选填资质
|
||||
description: 仅供 File Service 调用;原始文件名必须由上传请求传入,不得从存储 URL 推断。
|
||||
security: []
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/QualificationSubmissionRequest'}
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/internal/iam/required-qualification-packages:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
@@ -1113,7 +1223,7 @@ components:
|
||||
reviewType: {type: string, example: REGISTER}
|
||||
status: {type: string, enum: [PENDING]}
|
||||
tenantName: {type: string, description: 客户名称}
|
||||
tenantCode: {type: string, description: 客户编号}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001', description: 五位客户编号}
|
||||
contactName: {type: string, description: 联系人}
|
||||
contactPhone: {type: string, nullable: true, description: 联系电话}
|
||||
contactEmail: {type: string, format: email, nullable: true, description: 联系邮箱}
|
||||
@@ -1199,12 +1309,12 @@ components:
|
||||
required: [id, tenantCode, tenantName, status, dimensionInitializationStatus, servicePackage]
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$', example: '190000000000000001'}
|
||||
tenantCode: {type: string}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [PROFILE_REQUIRED, PENDING, ACTIVE, REJECTED, DISABLED, ARCHIVED]}
|
||||
dimensionInitializationStatus: {type: string, enum: [PENDING, READY, FAILED], description: 客户默认尺寸初始化状态;未 READY 时依赖该配置的业务失败关闭}
|
||||
legalName: {type: string}
|
||||
legalRepresentative: {type: string, maxLength: 64}
|
||||
legalRepresentative: {type: string, maxLength: 64, nullable: true}
|
||||
unifiedSocialCreditCode: {type: string}
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string}
|
||||
@@ -1237,7 +1347,7 @@ components:
|
||||
required: [id, tenantCode, tenantName, status]
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$'}
|
||||
tenantCode: {type: string}
|
||||
tenantCode: {type: string, pattern: '^\d{5}$', example: '00001'}
|
||||
tenantName: {type: string}
|
||||
status: {type: string, enum: [ACTIVE, DISABLED]}
|
||||
contactName: {type: string, nullable: true}
|
||||
@@ -1258,6 +1368,23 @@ components:
|
||||
data:
|
||||
type: array
|
||||
items: {$ref: '#/components/schemas/TenantQualification'}
|
||||
TenantQualificationApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data: {$ref: '#/components/schemas/TenantQualification'}
|
||||
QualificationDeletionApiResponse:
|
||||
allOf:
|
||||
- {$ref: '#/components/schemas/ApiResponse'}
|
||||
- type: object
|
||||
properties:
|
||||
data:
|
||||
type: object
|
||||
required: [qualificationId]
|
||||
properties:
|
||||
qualificationId: {type: string, pattern: '^\d+$'}
|
||||
deletedDocumentUrl: {type: string, nullable: true, description: 首次删除时返回,供 File Service 回收文件;重复删除为空}
|
||||
TenantAddress:
|
||||
type: object
|
||||
properties:
|
||||
@@ -1273,26 +1400,43 @@ components:
|
||||
isDefault: {type: boolean}
|
||||
TenantQualification:
|
||||
type: object
|
||||
required: [id, tenantId, qualificationType, documentUrl, originalFileName, status, createdAt, updatedAt]
|
||||
properties:
|
||||
id: {type: string, pattern: '^\d+$'}
|
||||
tenantId: {type: string, pattern: '^\d+$'}
|
||||
qualificationType: {type: string, enum: [BUSINESS_LICENSE, LEGAL_PERSON_ID, FREIGHT_FORWARDER_CERTIFICATE, BANK_ACCOUNT_LICENSE]}
|
||||
certificateNo: {type: string}
|
||||
documentUrl: {type: string}
|
||||
originalFileName: {type: string, description: 上传时原始文件名;前端不得从 URL 或内部 ID 推断}
|
||||
validFrom: {type: string, format: date}
|
||||
validTo: {type: string, format: date, nullable: true}
|
||||
status: {type: string, enum: [PENDING, APPROVED, REJECTED, SUPERSEDED]}
|
||||
reviewComment: {type: string}
|
||||
createdAt: {type: string, format: date-time}
|
||||
updatedAt: {type: string, format: date-time}
|
||||
QualificationSubmissionRequest:
|
||||
type: object
|
||||
required: [tenantId, qualificationType, documentUrl, originalFileName, replace]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^\d+$'}
|
||||
qualificationType: {type: string, enum: [LEGAL_PERSON_ID, FREIGHT_FORWARDER_CERTIFICATE, BANK_ACCOUNT_LICENSE]}
|
||||
certificateNo: {type: string, nullable: true}
|
||||
documentUrl: {type: string, minLength: 1}
|
||||
originalFileName: {type: string, minLength: 1, maxLength: 255}
|
||||
validTo: {type: string, format: date, nullable: true}
|
||||
replace: {type: boolean}
|
||||
RequiredQualificationPackageRequest:
|
||||
type: object
|
||||
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, replaceBusinessLicense]
|
||||
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, businessLicenseOriginalFileName, replaceBusinessLicense]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^\d+$'}
|
||||
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
|
||||
businessLicenseDocumentUrl: {type: string, minLength: 1}
|
||||
businessLicenseOriginalFileName: {type: string, minLength: 1, maxLength: 255}
|
||||
businessLicenseValidTo: {type: string, format: date, nullable: true}
|
||||
replaceBusinessLicense: {type: boolean}
|
||||
legalPersonIdDocumentUrl: {type: string, minLength: 1, nullable: true, description: 法人身份证选填}
|
||||
legalPersonIdOriginalFileName: {type: string, maxLength: 255, nullable: true, description: 上传法人身份证时必传}
|
||||
legalPersonIdValidTo: {type: string, format: date, nullable: true}
|
||||
replaceLegalPersonId: {type: boolean, nullable: true, default: false}
|
||||
RequiredQualificationPackageResponse:
|
||||
@@ -1312,11 +1456,11 @@ components:
|
||||
data: {$ref: '#/components/schemas/RequiredQualificationPackageResponse'}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone, servicePackage]
|
||||
required: [tenantName, legalName, contactName, contactPhone, servicePackage]
|
||||
properties:
|
||||
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||
legalRepresentative: {type: string, maxLength: 64, example: 张三}
|
||||
legalRepresentative: {type: string, maxLength: 64, nullable: true, example: 张三, description: 选填}
|
||||
contactName: {type: string, example: 李四}
|
||||
contactPhone: {type: string, pattern: '^1[3-9]\d{9}$', example: '13800138000', description: 创建时同时初始化公司联系电话与首个管理员登录手机号;创建后二者独立维护}
|
||||
contactEmail: {type: string, format: email, example: contact@example.com}
|
||||
@@ -1332,21 +1476,21 @@ components:
|
||||
internalRemark: {type: string, maxLength: 1000}
|
||||
UpdateProfileRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone]
|
||||
required: [tenantName, legalName, contactName, contactPhone]
|
||||
properties:
|
||||
tenantName: {type: string}
|
||||
legalName: {type: string}
|
||||
legalRepresentative: {type: string, maxLength: 64}
|
||||
legalRepresentative: {type: string, maxLength: 64, nullable: true, description: 选填}
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string, pattern: '^[0-9-]{4,32}$', description: 公司联系电话,不作为登录标识且不要求跨企业唯一}
|
||||
contactEmail: {type: string, format: email}
|
||||
UpdateTenantRequest:
|
||||
type: object
|
||||
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone]
|
||||
required: [tenantName, legalName, contactName, contactPhone]
|
||||
properties:
|
||||
tenantName: {type: string}
|
||||
legalName: {type: string}
|
||||
legalRepresentative: {type: string, maxLength: 64}
|
||||
legalRepresentative: {type: string, maxLength: 64, nullable: true, description: 选填}
|
||||
contactName: {type: string}
|
||||
contactPhone: {type: string, pattern: '^[0-9-]{4,32}$', description: 公司联系电话,与管理员及成员登录手机号独立且不要求跨企业唯一}
|
||||
contactEmail: {type: string, format: email}
|
||||
|
||||
Reference in New Issue
Block a user