This commit is contained in:
2026-08-14 12:50:37 +08:00
parent 91e781b3ff
commit ce6963a25e
46 changed files with 664 additions and 76 deletions
@@ -16,6 +16,8 @@ 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 com.baiyue.common.core.api.PageResult;
import com.baiyue.iam.dto.TenantDtos.FinanceCustomerSummary;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
@@ -106,6 +108,18 @@ public class InternalTenantController {
return ApiResponse.success(tenantService.orderEligibility(tenantId));
}
@GetMapping("/finance-customers")
public ApiResponse<PageResult<FinanceCustomerSummary>> financeCustomers(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@RequestParam(defaultValue = "1") @Min(1) long page,
@RequestParam(defaultValue = "20") @Min(1) @Max(200) long size,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) @Pattern(regexp = "ACTIVE|DISABLED") String status,
@RequestParam(required = false) Long customerId) {
verify(token);
return ApiResponse.success(tenantService.pageFinanceCustomers(page, size, keyword, status, customerId));
}
@GetMapping("/exports/customers")
public ApiResponse<CustomerExportBatch> exportCustomers(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@@ -49,7 +49,7 @@ public class PlatformTenantController {
@GetMapping("/tenants")
@PreAuthorize("hasAuthority('platform:customer:list')")
@Operation(summary = "查询企业列表", description = "平台端分页查询商户企业,支持企业名称、统一社会信用代码、联系人和状态筛选。")
@Operation(summary = "查询企业列表", description = "平台端分页查询商户企业,支持客户名称和状态筛选。客户编号、联系人等仅作展示信息,不作为搜索条件。")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "企业列表查询成功"),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "分页参数类型无效"),
@@ -124,4 +124,8 @@ public final class TenantDtos {
LocalDate establishedDate, BigDecimal registeredCapital,
String registeredCapitalCurrency, String registeredAddress,
String internalRemark, LocalDateTime createdAt, LocalDateTime approvedAt) { }
/** Full customer contact summary for trusted internal finance read models. */
public record FinanceCustomerSummary(Long id, String tenantCode, String tenantName, String status,
String contactName, String contactPhone) { }
}
@@ -44,14 +44,7 @@ public class CustomerListExportProviderRepository {
.eq(IamTenant::getTenantType, "MERCHANT")
.eq(StringUtils.hasText(status), IamTenant::getStatus, status);
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));
wrapper.like(IamTenant::getTenantName, keyword);
}
return wrapper;
}
@@ -91,15 +91,28 @@ public class TenantRepository {
.eq(IamTenant::getTenantType, "MERCHANT")
.eq(StringUtils.hasText(status), IamTenant::getStatus, status)
.orderByDesc(IamTenant::getCreatedAt);
if (StringUtils.hasText(keyword)) {
wrapper.like(IamTenant::getTenantName, keyword);
}
return PageResult.from(tenantMapper.selectPage(new Page<>(page, size), wrapper));
}
public PageResult<IamTenant> pageFinanceCustomers(long page, long size, String keyword, String status, Long customerId) {
LambdaQueryWrapper<IamTenant> wrapper = new LambdaQueryWrapper<IamTenant>()
.eq(IamTenant::getTenantType, "MERCHANT")
.eq(customerId != null, IamTenant::getId, customerId)
.in(!StringUtils.hasText(status), IamTenant::getStatus, List.of("ACTIVE", "DISABLED"))
.eq(StringUtils.hasText(status), IamTenant::getStatus, status)
.orderByDesc(IamTenant::getCreatedAt);
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));
.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 PageResult.from(tenantMapper.selectPage(new Page<>(page, size), wrapper));
}
@@ -21,6 +21,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.UpdateProfileRequest;
import com.baiyue.iam.dto.TenantDtos.UpdateTenantRequest;
import com.baiyue.iam.dto.TenantDtos.ChangeTenantStatusRequest;
@@ -198,6 +199,14 @@ public class TenantService {
source.page(), source.size(), source.total(), source.pages());
}
public PageResult<FinanceCustomerSummary> pageFinanceCustomers(long page, long size, String keyword, String status, Long customerId) {
PageResult<IamTenant> source = repository.pageFinanceCustomers(page, size, keyword, status, customerId);
return new PageResult<>(source.records().stream().map(tenant -> new FinanceCustomerSummary(
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
tenant.getContactName(), tenant.getContactPhone())).toList(),
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);
+44 -1
View File
@@ -44,7 +44,7 @@ paths:
parameters:
- {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: 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]}}
responses:
'200':
@@ -740,6 +740,39 @@ paths:
application/json:
schema: {$ref: '#/components/schemas/RequiredQualificationPackageApiResponse'}
'403': {$ref: '#/components/responses/Forbidden'}
/internal/iam/finance-customers:
get:
tags: [IAM Resources / IAM 资源]
summary: 提供财务客户主数据摘要
description: 仅供 Finance Service 调用;只返回正常/停用客户及财务账户所需的客户编号、名称和联系方式,不依赖财务表是否已有记录。
security: []
parameters:
- {$ref: '#/components/parameters/InternalServiceToken'}
- {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}}
- {name: status, in: query, required: false, schema: {type: string, enum: [ACTIVE, DISABLED]}}
- {name: customerId, in: query, required: false, schema: {type: integer, format: int64, minimum: 1}}
responses:
'200':
description: 财务客户主数据分页摘要
content:
application/json:
schema:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
- type: object
properties:
data:
type: object
required: [records, page, size, total, pages]
properties:
records: {type: array, items: {$ref: '#/components/schemas/FinanceCustomerSummary'}}
page: {type: integer}
size: {type: integer}
total: {type: integer, format: int64}
pages: {type: integer}
'403': {$ref: '#/components/responses/Forbidden'}
/internal/exports/v1/CUSTOMER_LIST/batches:
post:
tags: [Internal Export Provider]
@@ -971,6 +1004,16 @@ components:
internalRemark: {type: string, maxLength: 1000, description: 仅平台端可见}
createdAt: {type: string, format: date-time, description: 入驻时间}
approvedAt: {type: string, format: date-time, description: 审批通过时间}
FinanceCustomerSummary:
type: object
required: [id, tenantCode, tenantName, status]
properties:
id: {type: string, pattern: '^\d+$'}
tenantCode: {type: string}
tenantName: {type: string}
status: {type: string, enum: [ACTIVE, DISABLED]}
contactName: {type: string, nullable: true}
contactPhone: {type: string, nullable: true}
TenantAddressListApiResponse:
allOf:
- {$ref: '#/components/schemas/ApiResponse'}
@@ -40,4 +40,17 @@ class InternalCustomerExportContractTest {
assertEquals(ResultCode.SUCCESS.code(), response.code());
verify(service).exportMerchants(null, 100, "柏悦", "ACTIVE");
}
@Test
void exposesFinanceCustomerMasterDataOnlyWithInternalToken() {
BizException denied = assertThrows(BizException.class,
() -> controller.financeCustomers(null, 1, 20, "Alice", "ACTIVE"));
assertEquals(ResultCode.FORBIDDEN, denied.getResultCode());
when(service.pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null))
.thenReturn(new com.baiyue.common.core.api.PageResult<>(List.of(), 1, 20, 0, 0));
var response = controller.financeCustomers("test-internal-service-token-at-least-32-bytes", 1, 20, "Alice", "ACTIVE");
assertEquals(ResultCode.SUCCESS.code(), response.code());
verify(service).pageFinanceCustomers(1, 20, "Alice", "ACTIVE", null);
}
}
@@ -40,8 +40,8 @@ class CustomerListExportProviderRepositoryTest {
assertThat(query.getSqlSegment())
.contains("tenant_type")
.contains("status")
.contains("tenant_code")
.contains("tenant_name")
.doesNotContain("tenant_code")
.contains("id >")
.contains("id <=")
.contains("ORDER BY id ASC")
@@ -93,6 +93,24 @@ class TenantServiceTest {
verify(repository).pageMerchants(1, 10, "Example", "ACTIVE");
}
@Test
void pageFinanceCustomersKeepsFullContactSummaryForInternalFinance() {
IamTenant tenant = merchant(10001L, "ACTIVE");
tenant.setTenantCode("TEN10001");
tenant.setTenantName("Example Merchant");
tenant.setContactName("Alice");
tenant.setContactPhone("13800138000");
when(repository.pageFinanceCustomers(1, 10, "Alice", "ACTIVE", null))
.thenReturn(new PageResult<>(List.of(tenant), 1, 10, 1, 1));
var result = service.pageFinanceCustomers(1, 10, "Alice", "ACTIVE", null);
assertEquals("TEN10001", result.records().get(0).tenantCode());
assertEquals("Alice", result.records().get(0).contactName());
assertEquals("13800138000", result.records().get(0).contactPhone());
verify(repository).pageFinanceCustomers(1, 10, "Alice", "ACTIVE", null);
}
@Test
void platformCanMaintainAndDisableOnlyMerchantTenant() {
IamTenant tenant = merchant(10001L, "ACTIVE");