优化
This commit is contained in:
@@ -13,9 +13,14 @@ import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRespon
|
||||
import com.baiyue.iam.service.MerchantOnboardingService;
|
||||
import com.baiyue.iam.service.ProfileResourceService;
|
||||
import com.baiyue.iam.service.TenantService;
|
||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -24,6 +29,7 @@ import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/iam")
|
||||
@Validated
|
||||
public class InternalTenantController {
|
||||
@Resource private MerchantOnboardingService onboardingService;
|
||||
@Resource private ProfileResourceService profileResourceService;
|
||||
@@ -66,6 +72,18 @@ public class InternalTenantController {
|
||||
return ApiResponse.success(tenantService.orderEligibility(tenantId));
|
||||
}
|
||||
|
||||
@GetMapping("/exports/customers")
|
||||
public ApiResponse<CustomerExportBatch> exportCustomers(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestParam(required = false) Long cursor,
|
||||
@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") String status) {
|
||||
verify(token);
|
||||
return ApiResponse.success(tenantService.exportMerchants(cursor, limit, keyword, status));
|
||||
}
|
||||
|
||||
private void verify(String token) {
|
||||
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
|
||||
if (!MessageDigest.isEqual(internalToken, supplied)) {
|
||||
|
||||
@@ -70,6 +70,13 @@ public class MerchantTenantController {
|
||||
return ApiResponse.success(tenantService.update(TenantGuard.tenant(user, "MERCHANT"), request));
|
||||
}
|
||||
|
||||
@PostMapping("/tenant/qualifications/submit-review")
|
||||
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
||||
@Operation(summary = "提交当前企业资质审核", description = "校验四类必填资质齐全后创建审核单。上传或替换文件不会自动送审。")
|
||||
public ApiResponse<TenantResponse> submitQualificationReview(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||
return ApiResponse.success(tenantService.submitMerchantReview(TenantGuard.tenant(user, "MERCHANT")));
|
||||
}
|
||||
|
||||
@GetMapping("/members")
|
||||
@Operation(summary = "List enterprise members / 查询企业成员", description = "List members belonging to the current JWT tenant. / 查询属于当前 JWT 租户的成员。")
|
||||
@ApiResponses({@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Member list returned / 查询成功")})
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.iam.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class CustomerExportDtos {
|
||||
private CustomerExportDtos() { }
|
||||
|
||||
public record CustomerExportBatch(List<CustomerExportRow> records, Long nextCursor, boolean hasMore) { }
|
||||
|
||||
public record CustomerExportRow(
|
||||
String id, String tenantCode, String tenantName, String legalName,
|
||||
String unifiedSocialCreditCode, String enterpriseType, LocalDate establishedDate,
|
||||
BigDecimal registeredCapital, String registeredCapitalCurrency, String registeredAddress,
|
||||
String contactName, String contactPhone, String contactEmail, String contactWechat,
|
||||
String salesOwnerName, String servicePackage, Integer paymentTermDays,
|
||||
String status, LocalDateTime createdAt, LocalDateTime approvedAt) { }
|
||||
}
|
||||
@@ -24,12 +24,15 @@ public final class OnboardingDtos {
|
||||
|
||||
public record QualificationSubmissionRequest(
|
||||
@NotNull Long tenantId,
|
||||
@NotBlank String qualificationType,
|
||||
@NotBlank @Pattern(regexp = "BUSINESS_LICENSE|LEGAL_PERSON_ID|FREIGHT_FORWARDER_CERTIFICATE|BANK_ACCOUNT_LICENSE") String qualificationType,
|
||||
String certificateNo,
|
||||
@NotBlank String documentUrl
|
||||
@NotBlank String documentUrl,
|
||||
LocalDate validTo,
|
||||
boolean replace
|
||||
) { }
|
||||
|
||||
public record QualificationSubmissionResponse(Long qualificationId, Long reviewId, String tenantStatus) { }
|
||||
public record QualificationSubmissionResponse(Long qualificationId, Long reviewId, String tenantStatus,
|
||||
String replacedDocumentUrl) { }
|
||||
|
||||
public record PlatformQualificationRegistrationRequest(
|
||||
@NotNull Long tenantId,
|
||||
|
||||
@@ -31,6 +31,16 @@ public class TenantRepository {
|
||||
}
|
||||
|
||||
public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
|
||||
/**
|
||||
* Serializes the draft qualification workflow for one enterprise. The file service and the
|
||||
* merchant submit action run in separate requests, so qualification-row locks alone cannot
|
||||
* protect the empty-set case.
|
||||
*/
|
||||
public IamTenant findTenantForUpdate(Long tenantId) {
|
||||
return tenantMapper.selectOne(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getId, tenantId)
|
||||
.last("FOR UPDATE"));
|
||||
}
|
||||
public List<IamTenant> listMerchants() {
|
||||
return tenantMapper.selectList(new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
@@ -71,4 +81,24 @@ public class TenantRepository {
|
||||
}
|
||||
return PageResult.from(tenantMapper.selectPage(new Page<>(page, size), wrapper));
|
||||
}
|
||||
|
||||
public List<IamTenant> exportMerchants(Long cursor, int limit, String keyword, String status) {
|
||||
LambdaQueryWrapper<IamTenant> wrapper = new LambdaQueryWrapper<IamTenant>()
|
||||
.eq(IamTenant::getTenantType, "MERCHANT")
|
||||
.gt(cursor != null, IamTenant::getId, cursor)
|
||||
.eq(StringUtils.hasText(status), IamTenant::getStatus, status)
|
||||
.orderByAsc(IamTenant::getId)
|
||||
.last("LIMIT " + (limit + 1));
|
||||
if (StringUtils.hasText(keyword)) {
|
||||
wrapper.and(condition -> condition
|
||||
.like(IamTenant::getTenantCode, keyword)
|
||||
.or().like(IamTenant::getTenantName, keyword)
|
||||
.or().like(IamTenant::getLegalName, keyword)
|
||||
.or().like(IamTenant::getUnifiedSocialCreditCode, keyword)
|
||||
.or().like(IamTenant::getContactName, keyword)
|
||||
.or().like(IamTenant::getContactPhone, keyword)
|
||||
.or().like(IamTenant::getSalesOwnerName, keyword));
|
||||
}
|
||||
return tenantMapper.selectList(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,64 +54,69 @@ public class ProfileResourceService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "TENANT_QUALIFICATION_SUBMIT", resourceType = "TENANT_QUALIFICATION",
|
||||
resourceId = "#request.tenantId", description = "提交企业资质", captureAfter = false)
|
||||
@BusinessAudit(event = "TENANT_QUALIFICATION_UPLOAD", resourceType = "TENANT_QUALIFICATION",
|
||||
resourceId = "#request.tenantId", description = "上传企业资质", captureAfter = false)
|
||||
public QualificationSubmissionResponse submitQualification(QualificationSubmissionRequest request) {
|
||||
IamTenant tenant = tenantRepository.findTenant(request.tenantId());
|
||||
IamTenant tenant = tenantRepository.findTenantForUpdate(request.tenantId());
|
||||
if (tenant == null || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Enterprise not found");
|
||||
}
|
||||
if ("ACTIVE".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise is already approved");
|
||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT,
|
||||
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
||||
}
|
||||
if (tenantRepository.findPendingReviewByTenant(tenant.getId()) != null) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review");
|
||||
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
||||
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
||||
&& isCurrentQualification(item))
|
||||
.toList();
|
||||
if (!activeQualifications.isEmpty() && !request.replace()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Qualification type already uploaded / 该类资质已上传");
|
||||
}
|
||||
if (repository.findQualifications(tenant.getId()).stream()
|
||||
.anyMatch(item -> !"REJECTED".equals(item.getStatus()))) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Phase one permits one enterprise qualification image");
|
||||
String replacedDocumentUrl = null;
|
||||
for (IamTenantQualification existing : activeQualifications) {
|
||||
if (replacedDocumentUrl == null) {
|
||||
replacedDocumentUrl = existing.getDocumentUrl();
|
||||
}
|
||||
existing.setStatus("SUPERSEDED");
|
||||
existing.setReviewComment("商户在送审前替换文件");
|
||||
repository.saveQualification(existing);
|
||||
}
|
||||
|
||||
IamTenantQualification qualification = createQualification(tenant.getId(),
|
||||
request.qualificationType(), request.certificateNo(), request.documentUrl());
|
||||
qualification.setValidTo(request.validTo());
|
||||
repository.saveQualification(qualification);
|
||||
if (StringUtils.hasText(request.certificateNo())
|
||||
&& request.certificateNo().trim().toUpperCase().matches("[0-9A-Z]{18}")) {
|
||||
tenant.setUnifiedSocialCreditCode(request.certificateNo().trim().toUpperCase());
|
||||
tenantRepository.saveTenant(tenant);
|
||||
}
|
||||
tenant.setStatus("PENDING");
|
||||
tenant.setRejectReason(null);
|
||||
tenantRepository.saveTenant(tenant);
|
||||
|
||||
IamTenantReview review = new IamTenantReview();
|
||||
review.setId(IdWorker.getId()); review.setTenantId(tenant.getId());
|
||||
review.setReviewType("REGISTER"); review.setStatus("PENDING");
|
||||
review.setCreatedAt(LocalDateTime.now());
|
||||
tenantRepository.insertReview(review);
|
||||
return new QualificationSubmissionResponse(qualification.getId(), review.getId(), tenant.getStatus());
|
||||
return new QualificationSubmissionResponse(qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PLATFORM_TENANT_QUALIFICATION_UPLOAD", resourceType = "TENANT_QUALIFICATION",
|
||||
resourceId = "#request.tenantId", description = "平台代客户上传资质", captureAfter = false)
|
||||
public PlatformQualificationRegistrationResponse registerPlatformQualification(PlatformQualificationRegistrationRequest request) {
|
||||
IamTenant tenant = tenantRepository.findTenant(request.tenantId());
|
||||
IamTenant tenant = tenantRepository.findTenantForUpdate(request.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 can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
||||
}
|
||||
IamTenantQualification existing = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
||||
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
||||
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
||||
&& !"REJECTED".equals(item.getStatus()))
|
||||
.findFirst().orElse(null);
|
||||
if (existing != null && !request.replace()) {
|
||||
&& isCurrentQualification(item))
|
||||
.toList();
|
||||
if (!activeQualifications.isEmpty() && !request.replace()) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Qualification type already uploaded / 该类资质已上传");
|
||||
}
|
||||
String replacedDocumentUrl = null;
|
||||
if (existing != null) {
|
||||
replacedDocumentUrl = existing.getDocumentUrl();
|
||||
existing.setStatus("REJECTED");
|
||||
for (IamTenantQualification existing : activeQualifications) {
|
||||
if (replacedDocumentUrl == null) {
|
||||
replacedDocumentUrl = existing.getDocumentUrl();
|
||||
}
|
||||
existing.setStatus("SUPERSEDED");
|
||||
existing.setReviewComment("平台在送审前替换文件");
|
||||
repository.saveQualification(existing);
|
||||
}
|
||||
@@ -130,6 +135,11 @@ public class ProfileResourceService {
|
||||
repository.insertQualification(x); return x;
|
||||
}
|
||||
|
||||
private boolean isCurrentQualification(IamTenantQualification qualification) {
|
||||
return !"REJECTED".equals(qualification.getStatus())
|
||||
&& !"SUPERSEDED".equals(qualification.getStatus());
|
||||
}
|
||||
|
||||
private void requireMerchant(long tenantId) {
|
||||
IamTenant tenant = tenantRepository.findTenant(tenantId);
|
||||
if (tenant == null || tenant.getId() == null || tenant.getId().longValue() != tenantId
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
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.ProfileResourceRepository;
|
||||
@@ -16,6 +17,8 @@ import com.baiyue.iam.dto.TenantDtos.TenantResponse;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
|
||||
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
|
||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportRow;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -23,12 +26,15 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 租户生命周期应用服务,编排商户入驻、资料维护、审核流转和租户状态变更事务。
|
||||
*/
|
||||
@Service
|
||||
public class TenantService {
|
||||
private static final Set<String> REQUIRED_QUALIFICATION_TYPES = Set.of(
|
||||
"BUSINESS_LICENSE", "LEGAL_PERSON_ID", "FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE");
|
||||
@Resource
|
||||
private TenantRepository repository;
|
||||
@Resource
|
||||
@@ -84,6 +90,26 @@ public class TenantService {
|
||||
source.page(), source.size(), source.total(), source.pages());
|
||||
}
|
||||
|
||||
public CustomerExportBatch exportMerchants(Long cursor, int limit, String keyword, String status) {
|
||||
int boundedLimit = Math.min(Math.max(limit, 1), 500);
|
||||
List<IamTenant> source = repository.exportMerchants(cursor, boundedLimit, keyword, status);
|
||||
boolean hasMore = source.size() > boundedLimit;
|
||||
List<IamTenant> page = hasMore ? source.subList(0, boundedLimit) : source;
|
||||
List<CustomerExportRow> rows = page.stream().map(this::exportRow).toList();
|
||||
Long nextCursor = page.isEmpty() ? cursor : page.get(page.size() - 1).getId();
|
||||
return new CustomerExportBatch(rows, nextCursor, hasMore);
|
||||
}
|
||||
|
||||
private CustomerExportRow exportRow(IamTenant tenant) {
|
||||
return new CustomerExportRow(String.valueOf(tenant.getId()), tenant.getTenantCode(),
|
||||
tenant.getTenantName(), tenant.getLegalName(), tenant.getUnifiedSocialCreditCode(),
|
||||
tenant.getEnterpriseType(), tenant.getEstablishedDate(), tenant.getRegisteredCapital(),
|
||||
tenant.getRegisteredCapitalCurrency(), tenant.getRegisteredAddress(), tenant.getContactName(),
|
||||
tenant.getContactPhone(), tenant.getContactEmail(), tenant.getContactWechat(),
|
||||
tenant.getSalesOwnerName(), tenant.getServicePackage(), tenant.getPaymentTermDays(),
|
||||
tenant.getStatus(), tenant.getCreatedAt(), tenant.getApprovedAt());
|
||||
}
|
||||
|
||||
@BusinessAudit(
|
||||
event = "TENANT_REVIEW",
|
||||
resourceType = "TENANT_REVIEW",
|
||||
@@ -104,9 +130,7 @@ public class TenantService {
|
||||
|
||||
if (approve) {
|
||||
var qualifications = profileRepository.findQualifications(review.getTenantId());
|
||||
boolean hasLicense = qualifications.stream().anyMatch(item -> "BUSINESS_LICENSE".equals(item.getQualificationType()) && "PENDING".equals(item.getStatus()));
|
||||
boolean hasLegalId = qualifications.stream().anyMatch(item -> "LEGAL_PERSON_ID".equals(item.getQualificationType()) && "PENDING".equals(item.getStatus()));
|
||||
if (!hasLicense || !hasLegalId) {
|
||||
if (!hasAllPendingQualifications(qualifications)) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Required qualifications are incomplete / 必填资质不完整,不能审核通过");
|
||||
}
|
||||
}
|
||||
@@ -206,7 +230,7 @@ public class TenantService {
|
||||
@BusinessAudit(event = "PLATFORM_TENANT_SUBMIT_REVIEW", resourceType = "TENANT", resourceId = "#tenantId",
|
||||
description = "平台提交企业客户审核")
|
||||
public TenantResponse submitMerchantReview(long tenantId) {
|
||||
IamTenant tenant = requireMerchant(tenantId);
|
||||
IamTenant tenant = requireMerchantForUpdate(tenantId);
|
||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "Only draft or rejected enterprise can be submitted / 仅待完善或已驳回客户可提交审核");
|
||||
}
|
||||
@@ -214,10 +238,9 @@ public class TenantService {
|
||||
throw new BizException(ResultCode.CONFLICT, "Enterprise information is already under review / 客户资料已在审核中");
|
||||
}
|
||||
var qualifications = profileRepository.findQualifications(tenantId);
|
||||
boolean hasLicense = qualifications.stream().anyMatch(item -> "BUSINESS_LICENSE".equals(item.getQualificationType()) && !"REJECTED".equals(item.getStatus()));
|
||||
boolean hasLegalId = qualifications.stream().anyMatch(item -> "LEGAL_PERSON_ID".equals(item.getQualificationType()) && !"REJECTED".equals(item.getStatus()));
|
||||
if (!hasLicense || !hasLegalId) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "Business license and legal-person ID are required / 营业执照和法人身份证为必填资质");
|
||||
if (!hasAllPendingQualifications(qualifications)) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST,
|
||||
"Business license, legal-person ID, freight-forwarder certificate and bank account license are required / 营业执照、法人身份证、货代资质证书和银行开户许可证均为必填资质");
|
||||
}
|
||||
IamTenantReview review = new IamTenantReview();
|
||||
review.setId(IdWorker.getId()); review.setTenantId(tenantId);
|
||||
@@ -250,6 +273,11 @@ public class TenantService {
|
||||
return tenant.getId() != null && tenant.getId().longValue() == id;
|
||||
}
|
||||
|
||||
private boolean hasAllPendingQualifications(List<IamTenantQualification> qualifications) {
|
||||
return REQUIRED_QUALIFICATION_TYPES.stream().allMatch(type -> qualifications.stream()
|
||||
.anyMatch(item -> type.equals(item.getQualificationType()) && "PENDING".equals(item.getStatus())));
|
||||
}
|
||||
|
||||
private IamTenant requireMerchant(long tenantId) {
|
||||
IamTenant tenant = repository.findTenant(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId) || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
@@ -258,6 +286,14 @@ public class TenantService {
|
||||
return tenant;
|
||||
}
|
||||
|
||||
private IamTenant requireMerchantForUpdate(long tenantId) {
|
||||
IamTenant tenant = repository.findTenantForUpdate(tenantId);
|
||||
if (tenant == null || !tenantIdEquals(tenant, tenantId) || !"MERCHANT".equals(tenant.getTenantType())) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "Merchant tenant not found / 企业客户不存在");
|
||||
}
|
||||
return tenant;
|
||||
}
|
||||
|
||||
private TenantResponse response(IamTenant tenant) {
|
||||
return new TenantResponse(
|
||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
||||
|
||||
@@ -88,10 +88,13 @@ VALUES
|
||||
(5103010, 0, 'PLATFORM', 'MENU', 5103000, 'platform:warehouse:list', '国内仓/FBA仓', '/master-data/warehouses', NULL, 10, 'ACTIVE', 0),
|
||||
(5103020, 0, 'PLATFORM', 'MENU', 5103000, 'platform:express:list', '快递公司', '/master-data/express', NULL, 20, 'ACTIVE', 0),
|
||||
(5103030, 0, 'PLATFORM', 'MENU', 5103000, 'platform:counterparty:list', '往来单位', '/master-data/counterparties', NULL, 30, 'ACTIVE', 0),
|
||||
(5103040, 0, 'PLATFORM', 'MENU', 5103000, 'platform:download:list', '下载中心', '/downloads', NULL, 40, 'ACTIVE', 0),
|
||||
(5104010, 0, 'PLATFORM', 'MENU', 5104000, 'platform:audit:list', '操作日志', '/audit/logs', NULL, 10, 'ACTIVE', 0),
|
||||
-- Platform: buttons.
|
||||
(5100021, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:approve', '审核通过', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5100022, 0, 'PLATFORM', 'BUTTON', 5100020, 'platform:customer:reject', '审核驳回', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5100015, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:export', '导出企业客户', NULL, NULL, 40, 'ACTIVE', 0),
|
||||
(5103041, 0, 'PLATFORM', 'BUTTON', 5103040, 'platform:download:all', '查看全部下载任务', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101011, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:create', '新增用户', NULL, NULL, 10, 'ACTIVE', 0),
|
||||
(5101012, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:edit', '编辑用户', NULL, NULL, 20, 'ACTIVE', 0),
|
||||
(5101013, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:status', '启停用户', NULL, NULL, 30, 'ACTIVE', 0),
|
||||
@@ -198,7 +201,8 @@ INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT 4000002, id, 0 FROM iam_permission
|
||||
WHERE permission_code IN (
|
||||
'platform:customer', 'platform:customer:list', 'platform:customer:review',
|
||||
'platform:customer:approve', 'platform:customer:reject'
|
||||
'platform:customer:approve', 'platform:customer:reject', 'platform:customer:export',
|
||||
'platform:master-data', 'platform:download:list'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
|
||||
+28
-2
@@ -18,6 +18,16 @@ UPDATE iam_permission
|
||||
SET built_in = 1
|
||||
WHERE id BETWEEN 5000000 AND 5399999 AND built_in = 0;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name,
|
||||
route_path, sort_no, status, built_in, deleted)
|
||||
VALUES
|
||||
(5103040, 0, 'PLATFORM', 'MENU', 5103000, 'platform:download:list', '下载中心',
|
||||
'/downloads', 40, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name = VALUES(permission_name), route_path = VALUES(route_path), sort_no = VALUES(sort_no),
|
||||
status = 'ACTIVE', built_in = 1, deleted = 0;
|
||||
|
||||
INSERT INTO iam_permission
|
||||
(id, tenant_id, app_code, permission_type, parent_id, permission_code, permission_name,
|
||||
sort_no, status, built_in, deleted)
|
||||
@@ -25,6 +35,7 @@ VALUES
|
||||
(5100014, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:view', '查看企业客户档案', 15, 'ACTIVE', 1, 0),
|
||||
(5100012, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:edit', '编辑企业客户', 20, 'ACTIVE', 1, 0),
|
||||
(5100013, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:status', '启停企业客户', 30, 'ACTIVE', 1, 0),
|
||||
(5100015, 0, 'PLATFORM', 'BUTTON', 5100010, 'platform:customer:export', '导出企业客户', 40, 'ACTIVE', 1, 0),
|
||||
(5101015, 0, 'PLATFORM', 'BUTTON', 5101010, 'platform:user:grant-role', '分配内部用户角色', 50, 'ACTIVE', 1, 0),
|
||||
(5101032, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:edit', '编辑权限资源', 20, 'ACTIVE', 1, 0),
|
||||
(5101033, 0, 'PLATFORM', 'BUTTON', 5101030, 'platform:permission:status', '启停权限资源', 30, 'ACTIVE', 1, 0),
|
||||
@@ -35,7 +46,8 @@ VALUES
|
||||
(5103024, 0, 'PLATFORM', 'BUTTON', 5103020, 'platform:express:import', '批量导入快递公司', 40, 'ACTIVE', 1, 0),
|
||||
(5103031, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:create', '新增往来单位', 10, 'ACTIVE', 1, 0),
|
||||
(5103032, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:edit', '编辑往来单位', 20, 'ACTIVE', 1, 0),
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启停往来单位', 30, 'ACTIVE', 1, 0)
|
||||
(5103033, 0, 'PLATFORM', 'BUTTON', 5103030, 'platform:counterparty:status', '启停往来单位', 30, 'ACTIVE', 1, 0),
|
||||
(5103041, 0, 'PLATFORM', 'BUTTON', 5103040, 'platform:download:all', '查看全部下载任务', 10, 'ACTIVE', 1, 0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
permission_name = VALUES(permission_name), sort_no = VALUES(sort_no), status = 'ACTIVE',
|
||||
built_in = 1, deleted = 0;
|
||||
@@ -49,10 +61,24 @@ WHERE tenant_id = 0 AND app_code = 'PLATFORM'
|
||||
'platform:customer:edit', 'platform:customer:status', 'platform:user:grant-role',
|
||||
'platform:permission:edit', 'platform:permission:status', 'platform:permission:delete',
|
||||
'platform:express:list', 'platform:express:create', 'platform:express:edit', 'platform:express:status', 'platform:express:import',
|
||||
'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status'
|
||||
'platform:counterparty:list', 'platform:counterparty:create', 'platform:counterparty:edit', 'platform:counterparty:status',
|
||||
'platform:customer:export', 'platform:download:list', 'platform:download:all'
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- 已有客户运营角色继续具备客户导出与本人下载任务查询能力,但不授予查看全部任务。
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT DISTINCT existing_grant.role_id, added_permission.id, existing_grant.tenant_id
|
||||
FROM iam_role_permission existing_grant
|
||||
JOIN iam_permission existing_permission ON existing_permission.id = existing_grant.permission_id
|
||||
JOIN iam_permission added_permission
|
||||
ON added_permission.tenant_id = 0 AND added_permission.app_code = 'PLATFORM'
|
||||
AND added_permission.permission_code IN ('platform:customer:export', 'platform:download:list')
|
||||
WHERE existing_permission.tenant_id = 0
|
||||
AND existing_permission.app_code = 'PLATFORM'
|
||||
AND existing_permission.permission_code IN ('platform:customer:list', 'platform:customer:review')
|
||||
ON DUPLICATE KEY UPDATE tenant_id = VALUES(tenant_id);
|
||||
|
||||
-- 详情查看从列表和审核权限中拆分;升级时保留既有运营角色的查询能力。
|
||||
INSERT INTO iam_role_permission (role_id, permission_id, tenant_id)
|
||||
SELECT DISTINCT existing_grant.role_id, view_permission.id, existing_grant.tenant_id
|
||||
|
||||
@@ -464,6 +464,16 @@ paths:
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/Success'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
/api/merchant/v1/iam/tenant/qualifications/submit-review:
|
||||
post:
|
||||
tags: [IAM Resources / IAM 资源]
|
||||
summary: Submit enterprise qualifications for review / 提交企业资质审核
|
||||
description: Uploading or replacing a qualification keeps the tenant in draft state. This endpoint creates the review only after all four required qualification types have been provided. / 上传或替换资质不会自动送审;四类必填资质齐全后由此接口创建审核单。
|
||||
responses:
|
||||
'200': {$ref: '#/components/responses/TenantSuccess'}
|
||||
'400': {$ref: '#/components/responses/BadRequest'}
|
||||
'403': {$ref: '#/components/responses/Forbidden'}
|
||||
'409': {$ref: '#/components/responses/Conflict'}
|
||||
/api/{app}/v1/iam/roles:
|
||||
parameters:
|
||||
- {$ref: '#/components/parameters/App'}
|
||||
@@ -749,7 +759,7 @@ components:
|
||||
documentUrl: {type: string}
|
||||
validFrom: {type: string, format: date}
|
||||
validTo: {type: string, format: date, nullable: true}
|
||||
status: {type: string, enum: [PENDING, APPROVED, REJECTED]}
|
||||
status: {type: string, enum: [PENDING, APPROVED, REJECTED, SUPERSEDED]}
|
||||
reviewComment: {type: string}
|
||||
RegisterRequest:
|
||||
type: object
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.iam.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportBatch;
|
||||
import com.baiyue.iam.service.MerchantOnboardingService;
|
||||
import com.baiyue.iam.service.ProfileResourceService;
|
||||
import com.baiyue.iam.service.TenantService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class InternalCustomerExportContractTest {
|
||||
@Test
|
||||
void rejectsMissingInternalTokenAndAcceptsConfiguredToken() {
|
||||
TenantService service = mock(TenantService.class);
|
||||
InternalTenantController controller = new InternalTenantController();
|
||||
ReflectionTestUtils.setField(controller, "onboardingService", mock(MerchantOnboardingService.class));
|
||||
ReflectionTestUtils.setField(controller, "profileResourceService", mock(ProfileResourceService.class));
|
||||
ReflectionTestUtils.setField(controller, "tenantService", service);
|
||||
ReflectionTestUtils.setField(controller, "internalTokenValue", "internal-token");
|
||||
controller.initializeToken();
|
||||
|
||||
BizException denied = assertThrows(BizException.class,
|
||||
() -> controller.exportCustomers(null, null, 100, null, null));
|
||||
assertEquals(ResultCode.FORBIDDEN, denied.getResultCode());
|
||||
|
||||
when(service.exportMerchants(null, 100, "柏悦", "ACTIVE"))
|
||||
.thenReturn(new CustomerExportBatch(List.of(), null, false));
|
||||
var response = controller.exportCustomers("internal-token", null, 100, "柏悦", "ACTIVE");
|
||||
assertEquals(ResultCode.SUCCESS.code(), response.code());
|
||||
verify(service).exportMerchants(null, 100, "柏悦", "ACTIVE");
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ class QualificationUploadContractTest {
|
||||
|
||||
assertFalse(pathContract.contains("\n post:"));
|
||||
assertFalse(yaml.contains(" QualificationRequest:"));
|
||||
assertTrue(yaml.contains(" /api/merchant/v1/iam/tenant/qualifications/submit-review:"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -48,9 +48,11 @@ class CustomerProfileMigrationContractTest {
|
||||
.contains("CALL regression_assert_table('iam_tenant')")
|
||||
.contains("CREATE PROCEDURE regression_add_iam_column")
|
||||
.contains("platform:customer:view")
|
||||
.contains("platform:customer:export")
|
||||
.contains("platform:download:list")
|
||||
.contains("'IAM客户档案字段' AS check_item")
|
||||
.contains("9 AS expected_count")
|
||||
.contains("44 AS expected_count")
|
||||
.contains("47 AS expected_count")
|
||||
.contains("MIGRATION_COMPLETED");
|
||||
assertThat(instructions)
|
||||
.contains("phase1-platform-master-data-and-permission-management.sql")
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class MerchantQualificationUploadTest {
|
||||
|
||||
@Test
|
||||
void uploadsOneDraftQualificationWithoutCreatingAReview() {
|
||||
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||
ProfileResourceService service = new ProfileResourceService();
|
||||
ReflectionTestUtils.setField(service, "tenantRepository", tenantRepository);
|
||||
ReflectionTestUtils.setField(service, "repository", profileRepository);
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(10001L);
|
||||
tenant.setTenantType("MERCHANT");
|
||||
tenant.setStatus("PROFILE_REQUIRED");
|
||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||
|
||||
var response = service.submitQualification(new QualificationSubmissionRequest(
|
||||
10001L, "LEGAL_PERSON_ID", null,
|
||||
"/api/merchant/v1/files/qualifications/legal-person.png", LocalDate.of(2030, 1, 1), false));
|
||||
|
||||
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
||||
assertNull(response.reviewId());
|
||||
verify(profileRepository).insertQualification(any());
|
||||
verify(tenantRepository, never()).insertReview(any());
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ class PlatformQualificationRegistrationTest {
|
||||
ReflectionTestUtils.setField(service, "tenantRepository", tenantRepository);
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("PROFILE_REQUIRED");
|
||||
when(tenantRepository.findTenant(10001L)).thenReturn(tenant);
|
||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||
|
||||
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
||||
@@ -59,7 +59,7 @@ class PlatformQualificationRegistrationTest {
|
||||
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("LEGAL_PERSON_ID");
|
||||
existing.setStatus("PENDING");
|
||||
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf");
|
||||
when(tenantRepository.findTenant(10001L)).thenReturn(tenant);
|
||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
|
||||
|
||||
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
||||
@@ -68,7 +68,7 @@ class PlatformQualificationRegistrationTest {
|
||||
|
||||
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
||||
verify(repository, times(2)).saveQualification(captor.capture());
|
||||
assertEquals("REJECTED", existing.getStatus());
|
||||
assertEquals("SUPERSEDED", existing.getStatus());
|
||||
assertEquals("平台在送审前替换文件", existing.getReviewComment());
|
||||
assertEquals(existing.getDocumentUrl(), response.replacedDocumentUrl());
|
||||
assertEquals("PENDING", captor.getAllValues().get(1).getStatus());
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.baiyue.iam.service;
|
||||
|
||||
import com.baiyue.iam.entity.IamTenant;
|
||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||
import com.baiyue.iam.repository.TenantRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class TenantCustomerExportTest {
|
||||
@Test
|
||||
void returnsUnmaskedCustomerRowsAndAdvancingCursor() {
|
||||
TenantRepository repository = mock(TenantRepository.class);
|
||||
TenantService service = new TenantService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "profileRepository", mock(ProfileResourceRepository.class));
|
||||
IamTenant tenant = new IamTenant();
|
||||
tenant.setId(101L);
|
||||
tenant.setTenantCode("TEN101");
|
||||
tenant.setTenantName("柏悦物流");
|
||||
tenant.setContactPhone("13800138000");
|
||||
tenant.setContactEmail("ops@example.com");
|
||||
tenant.setStatus("ACTIVE");
|
||||
when(repository.exportMerchants(100L, 50, "柏悦", "ACTIVE")).thenReturn(List.of(tenant));
|
||||
|
||||
var result = service.exportMerchants(100L, 50, "柏悦", "ACTIVE");
|
||||
|
||||
assertEquals("101", result.records().get(0).id());
|
||||
assertEquals("13800138000", result.records().get(0).contactPhone());
|
||||
assertEquals("ops@example.com", result.records().get(0).contactEmail());
|
||||
assertEquals(101L, result.nextCursor());
|
||||
assertFalse(result.hasMore());
|
||||
verify(repository).exportMerchants(100L, 50, "柏悦", "ACTIVE");
|
||||
}
|
||||
}
|
||||
@@ -125,16 +125,17 @@ class TenantServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitSubmissionRequiresBothMandatoryQualificationsAndCreatesReview() {
|
||||
void explicitSubmissionRequiresAllPrototypeQualificationsAndCreatesReview() {
|
||||
TenantRepository repository = mock(TenantRepository.class);
|
||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||
TenantService service = new TenantService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
|
||||
IamTenant tenant = merchant(10001L, "PROFILE_REQUIRED");
|
||||
when(repository.findTenant(10001L)).thenReturn(tenant);
|
||||
when(repository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID"),
|
||||
qualification("FREIGHT_FORWARDER_CERTIFICATE"), qualification("BANK_ACCOUNT_LICENSE")));
|
||||
|
||||
var submitted = service.submitMerchantReview(10001L);
|
||||
|
||||
@@ -143,6 +144,23 @@ class TenantServiceTest {
|
||||
verify(repository).saveTenant(tenant);
|
||||
}
|
||||
|
||||
@Test
|
||||
void explicitSubmissionRejectsAnIncompleteQualificationSet() {
|
||||
TenantRepository repository = mock(TenantRepository.class);
|
||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||
TenantService service = new TenantService();
|
||||
ReflectionTestUtils.setField(service, "repository", repository);
|
||||
ReflectionTestUtils.setField(service, "profileRepository", profileRepository);
|
||||
when(repository.findTenantForUpdate(10001L)).thenReturn(merchant(10001L, "PROFILE_REQUIRED"));
|
||||
when(profileRepository.findQualifications(10001L)).thenReturn(List.of(
|
||||
qualification("BUSINESS_LICENSE"), qualification("LEGAL_PERSON_ID")));
|
||||
|
||||
BizException exception = assertThrows(BizException.class, () -> service.submitMerchantReview(10001L));
|
||||
|
||||
assertEquals(ResultCode.BAD_REQUEST, exception.getResultCode());
|
||||
verify(repository, never()).insertReview(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void 企业激活不等于具备下单资格仍需已审核资质() {
|
||||
TenantRepository repository = mock(TenantRepository.class);
|
||||
|
||||
Reference in New Issue
Block a user