This commit is contained in:
2026-08-17 19:13:08 +08:00
parent 5199146e97
commit cddd23e4c7
40 changed files with 1203 additions and 71 deletions
@@ -13,7 +13,7 @@ import com.baiyue.iam.dto.TenantDtos.ApprovalResponse;
import com.baiyue.iam.dto.TenantDtos.CreationResponse;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationRequest;
import com.baiyue.iam.dto.TenantDtos.CustomerImpersonationResponse;
import com.baiyue.iam.entity.IamTenantReview;
import com.baiyue.iam.dto.TenantDtos.PendingReviewResponse;
import com.baiyue.iam.service.TenantGuard;
import com.baiyue.iam.service.TenantService;
import com.baiyue.iam.service.MemberService;
@@ -179,7 +179,7 @@ public class PlatformTenantController {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "Review list returned / 查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform scope required / 需要平台端权限域")
})
public ApiResponse<List<IamTenantReview>> reviews(@AuthenticationPrincipal AuthenticatedUser user) {
public ApiResponse<List<PendingReviewResponse>> reviews(@AuthenticationPrincipal AuthenticatedUser user) {
TenantGuard.tenant(user, "PLATFORM");
return ApiResponse.success(service.pendingReviews());
}
@@ -101,6 +101,21 @@ public final class TenantDtos {
@Schema(description = "Approval or rejection comment / 审核意见", example = "资料完整,审核通过") String comment
) { }
@Schema(name = "PendingEnterpriseReviewResponse", description = "Pending enterprise review with the customer summary required by platform reviewers / 待审核企业及客户摘要")
public record PendingReviewResponse(
@Schema(description = "Review ID / 审核单 ID", example = "190000000000000001") Long id,
@Schema(description = "Customer ID / 客户 ID", example = "190000000000000002") Long tenantId,
@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 = "Primary contact / 联系人", example = "张三") String contactName,
@Schema(description = "Contact phone / 联系电话", example = "13800138000") String contactPhone,
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") String contactEmail,
@Schema(description = "Review submission time / 提交审核时间") LocalDateTime submittedAt,
@Schema(description = "Review comment / 审核意见", nullable = true) String reviewComment
) { }
@Schema(name = "TenantApprovalResponse", description = "Enterprise approval result and one-time administrator credentials / 企业审核结果及一次性管理员凭据")
public record ApprovalResponse(
@Schema(description = "Tenant ID / 企业租户 ID", example = "190000000000000001") Long tenantId,
@@ -12,6 +12,7 @@ import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Collection;
/**
* 租户仓储,封装租户及审核单的唯一性校验、查询、创建、更新和待审列表操作。
@@ -48,6 +49,12 @@ public class TenantRepository {
}
public IamTenant findTenant(Long tenantId) { return tenantMapper.selectById(tenantId); }
public List<IamTenant> findMerchantsByIds(Collection<Long> tenantIds) {
if (tenantIds == null || tenantIds.isEmpty()) return List.of();
return tenantMapper.selectList(new LambdaQueryWrapper<IamTenant>()
.in(IamTenant::getId, tenantIds)
.eq(IamTenant::getTenantType, "MERCHANT"));
}
/**
* 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
@@ -22,6 +22,7 @@ import com.baiyue.iam.dto.OnboardingDtos.OrderEligibilityResponse;
import com.baiyue.iam.dto.TenantDtos.RegisterRequest;
import com.baiyue.iam.dto.TenantDtos.TenantResponse;
import com.baiyue.iam.dto.TenantDtos.FinanceCustomerSummary;
import com.baiyue.iam.dto.TenantDtos.PendingReviewResponse;
import com.baiyue.iam.dto.TenantDtos.UpdateProfileRequest;
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
@@ -40,7 +41,10 @@ import feign.FeignException;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 租户生命周期应用服务,编排商户入驻、资料维护、审核流转和租户状态变更事务。
@@ -189,8 +193,17 @@ public class TenantService {
return new CreationResponse(response(tenant), administrator.username(), administrator.temporaryPassword());
}
public List<IamTenantReview> pendingReviews() {
return repository.findPendingReviews();
public List<PendingReviewResponse> pendingReviews() {
List<IamTenantReview> reviews = repository.findPendingReviews();
if (reviews.isEmpty()) return List.of();
Map<Long, IamTenant> tenants = repository.findMerchantsByIds(reviews.stream()
.map(IamTenantReview::getTenantId)
.collect(Collectors.toSet()))
.stream()
.collect(Collectors.toMap(IamTenant::getId, Function.identity()));
return reviews.stream()
.map(review -> pendingReviewResponse(review, tenants.get(review.getTenantId())))
.toList();
}
public PageResult<TenantResponse> pageMerchants(long page, long size, String keyword, String status) {
@@ -497,6 +510,17 @@ public class TenantService {
);
}
private PendingReviewResponse pendingReviewResponse(IamTenantReview review, IamTenant tenant) {
return new PendingReviewResponse(
review.getId(), review.getTenantId(), review.getReviewType(), review.getStatus(),
tenant == null ? null : tenant.getTenantName(),
tenant == null ? null : tenant.getTenantCode(),
tenant == null ? null : tenant.getContactName(),
tenant == null ? null : tenant.getContactPhone(),
tenant == null ? null : tenant.getContactEmail(),
review.getCreatedAt(), review.getReviewComment());
}
private TenantResponse summaryResponse(IamTenant tenant) {
TenantResponse response = response(tenant);
return new TenantResponse(response.id(), response.tenantCode(), response.tenantName(), response.status(),
+28 -1
View File
@@ -259,7 +259,11 @@ paths:
summary: List pending tenant reviews / 查询待审核企业
description: Return enterprise registrations waiting for a platform decision. / 返回等待平台处理的企业注册审核记录。
responses:
'200': {$ref: '#/components/responses/Success'}
'200':
description: 待审核企业及客户摘要查询成功
content:
application/json:
schema: {$ref: '#/components/schemas/PendingEnterpriseReviewListApiResponse'}
'403': {$ref: '#/components/responses/Forbidden'}
/api/platform/v1/iam/reviews/{id}/approve:
post:
@@ -903,6 +907,29 @@ components:
- type: object
properties:
data: {$ref: '#/components/schemas/TenantApprovalResponse'}
PendingEnterpriseReviewListApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data:
type: array
items: {$ref: '#/components/schemas/PendingEnterpriseReviewResponse'}
PendingEnterpriseReviewResponse:
type: object
required: [id, tenantId, reviewType, status, tenantName, tenantCode, contactName, submittedAt]
properties:
id: {type: string, pattern: '^\d+$', description: 审核单 ID}
tenantId: {type: string, pattern: '^\d+$', description: 客户 ID}
reviewType: {type: string, example: REGISTER}
status: {type: string, enum: [PENDING]}
tenantName: {type: string, description: 客户名称}
tenantCode: {type: string, description: 客户编号}
contactName: {type: string, description: 联系人}
contactPhone: {type: string, nullable: true, description: 联系电话}
contactEmail: {type: string, format: email, nullable: true, description: 联系邮箱}
submittedAt: {type: string, format: date-time, description: 提交审核时间}
reviewComment: {type: string, nullable: true, description: 审核意见}
TenantCreationApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
@@ -23,6 +23,10 @@ class IamOpenApiContractTest {
.contains("PermissionCodesApiResponse:")
.contains("TenantPageApiResponse:")
.contains("TenantApiResponse:")
.contains("PendingEnterpriseReviewListApiResponse:")
.contains("PendingEnterpriseReviewResponse:")
.contains("required: [id, tenantId, reviewType, status, tenantName, tenantCode, contactName, submittedAt]")
.contains("submittedAt: {type: string, format: date-time, description: 提交审核时间}")
.contains("TenantSuccess:")
.contains("salesOwnerName:")
.contains("paymentTermDays:")
@@ -46,6 +46,11 @@ class PlatformTenantControllerContractTest {
assertEquals("hasAuthority('platform:customer:review')",
method.getAnnotation(PreAuthorize.class).value());
ParameterizedType apiResponse = (ParameterizedType) method.getGenericReturnType();
ParameterizedType list = (ParameterizedType) apiResponse.getActualTypeArguments()[0];
assertEquals(java.util.List.class, list.getRawType());
assertEquals(com.baiyue.iam.dto.TenantDtos.PendingReviewResponse.class,
list.getActualTypeArguments()[0]);
}
@Test
@@ -30,6 +30,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.verify;
@@ -93,6 +94,37 @@ class TenantServiceTest {
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
}
@Test
void pendingReviewsIncludeCustomerSummaryWithoutPerReviewQueries() {
IamTenantReview review = review(71L, 10001L);
review.setReviewType("REGISTER");
review.setCreatedAt(LocalDateTime.of(2026, 8, 17, 10, 27));
IamTenant tenant = merchant(10001L, "PENDING");
tenant.setTenantCode("TEN10001");
tenant.setTenantName("Example Merchant");
tenant.setContactName("Alice");
tenant.setContactPhone("13800138000");
tenant.setContactEmail("alice@example.com");
when(repository.findPendingReviews()).thenReturn(List.of(review));
when(repository.findMerchantsByIds(Set.of(10001L))).thenReturn(List.of(tenant));
var result = service.pendingReviews();
assertEquals(1, result.size());
assertEquals(71L, result.get(0).id());
assertEquals(10001L, result.get(0).tenantId());
assertEquals("REGISTER", result.get(0).reviewType());
assertEquals("PENDING", result.get(0).status());
assertEquals("Example Merchant", result.get(0).tenantName());
assertEquals("TEN10001", result.get(0).tenantCode());
assertEquals("Alice", result.get(0).contactName());
assertEquals("13800138000", result.get(0).contactPhone());
assertEquals("alice@example.com", result.get(0).contactEmail());
assertEquals(LocalDateTime.of(2026, 8, 17, 10, 27), result.get(0).submittedAt());
verify(repository).findMerchantsByIds(Set.of(10001L));
verify(repository, never()).findTenant(10001L);
}
@Test
void pageFinanceCustomersKeepsFullContactSummaryForInternalFinance() {
IamTenant tenant = merchant(10001L, "ACTIVE");