优化
This commit is contained in:
@@ -19,6 +19,16 @@ public interface IamQualificationClient {
|
|||||||
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||||
@RequestBody PlatformQualificationRequest request);
|
@RequestBody PlatformQualificationRequest request);
|
||||||
|
|
||||||
|
@PostMapping("/required-qualification-packages")
|
||||||
|
ApiResponse<RequiredPackageResponse> submitRequiredPackage(
|
||||||
|
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||||
|
@RequestBody RequiredPackageRequest request);
|
||||||
|
|
||||||
|
@PostMapping("/platform-required-qualification-packages")
|
||||||
|
ApiResponse<RequiredPackageResponse> submitPlatformRequiredPackage(
|
||||||
|
@RequestHeader("X-Internal-Service-Token") String internalToken,
|
||||||
|
@RequestBody RequiredPackageRequest request);
|
||||||
|
|
||||||
record QualificationRequest(Long tenantId, String qualificationType,
|
record QualificationRequest(Long tenantId, String qualificationType,
|
||||||
String certificateNo, String documentUrl,
|
String certificateNo, String documentUrl,
|
||||||
java.time.LocalDate validTo, boolean replace) { }
|
java.time.LocalDate validTo, boolean replace) { }
|
||||||
@@ -29,4 +39,16 @@ public interface IamQualificationClient {
|
|||||||
record PlatformQualificationRequest(Long tenantId, String qualificationType,
|
record PlatformQualificationRequest(Long tenantId, String qualificationType,
|
||||||
String certificateNo, String documentUrl,
|
String certificateNo, String documentUrl,
|
||||||
java.time.LocalDate validTo, boolean replace) { }
|
java.time.LocalDate validTo, boolean replace) { }
|
||||||
|
record RequiredPackageRequest(Long tenantId, String unifiedSocialCreditCode,
|
||||||
|
String businessLicenseDocumentUrl,
|
||||||
|
java.time.LocalDate businessLicenseValidTo,
|
||||||
|
boolean replaceBusinessLicense,
|
||||||
|
String legalPersonIdDocumentUrl,
|
||||||
|
java.time.LocalDate legalPersonIdValidTo,
|
||||||
|
boolean replaceLegalPersonId) { }
|
||||||
|
record RequiredPackageResponse(Long businessLicenseQualificationId,
|
||||||
|
Long legalPersonIdQualificationId,
|
||||||
|
String tenantStatus,
|
||||||
|
String replacedBusinessLicenseDocumentUrl,
|
||||||
|
String replacedLegalPersonIdDocumentUrl) { }
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
|
|||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.file.client.IamQualificationClient;
|
import com.baiyue.file.client.IamQualificationClient;
|
||||||
import com.baiyue.file.service.LocalQualificationStorage;
|
import com.baiyue.file.service.LocalQualificationStorage;
|
||||||
|
import com.baiyue.file.service.QualificationPackageService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -32,6 +33,7 @@ public class PlatformQualificationFileController {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(PlatformQualificationFileController.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(PlatformQualificationFileController.class);
|
||||||
@Resource private LocalQualificationStorage storage;
|
@Resource private LocalQualificationStorage storage;
|
||||||
@Resource private IamQualificationClient iamClient;
|
@Resource private IamQualificationClient iamClient;
|
||||||
|
@Resource private QualificationPackageService packageService;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||||
|
|
||||||
@PostMapping(value = "/{tenantId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(value = "/{tenantId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@@ -45,7 +47,7 @@ public class PlatformQualificationFileController {
|
|||||||
@RequestParam(required = false) LocalDate validTo,
|
@RequestParam(required = false) LocalDate validTo,
|
||||||
@RequestParam(defaultValue = "false") boolean replace) {
|
@RequestParam(defaultValue = "false") boolean replace) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
qualificationType = QualificationTypes.requireSupported(qualificationType);
|
qualificationType = QualificationTypes.requireOptional(qualificationType);
|
||||||
var stored = storage.store(tenantId, file);
|
var stored = storage.store(tenantId, file);
|
||||||
try {
|
try {
|
||||||
var response = iamClient.submitPlatform(internalToken,
|
var response = iamClient.submitPlatform(internalToken,
|
||||||
@@ -79,6 +81,24 @@ public class PlatformQualificationFileController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/{tenantId}/required-package", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
@PreAuthorize("hasAnyAuthority('platform:customer:create','platform:customer:edit')")
|
||||||
|
public ApiResponse<QualificationPackageService.PackageUploadResult> uploadRequiredPackage(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable long tenantId,
|
||||||
|
@RequestPart("businessLicense") MultipartFile businessLicense,
|
||||||
|
@RequestPart("legalPersonId") MultipartFile legalPersonId,
|
||||||
|
@RequestParam String unifiedSocialCreditCode,
|
||||||
|
@RequestParam(required = false) LocalDate businessLicenseValidTo,
|
||||||
|
@RequestParam(required = false) LocalDate legalPersonIdValidTo,
|
||||||
|
@RequestParam(defaultValue = "false") boolean replaceBusinessLicense,
|
||||||
|
@RequestParam(defaultValue = "false") boolean replaceLegalPersonId) {
|
||||||
|
requirePlatform(user);
|
||||||
|
return ApiResponse.success(packageService.upload(tenantId, businessLicense, legalPersonId,
|
||||||
|
unifiedSocialCreditCode, businessLicenseValidTo, legalPersonIdValidTo,
|
||||||
|
replaceBusinessLicense, replaceLegalPersonId, true));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{tenantId}/{fileName}")
|
@GetMapping("/{tenantId}/{fileName}")
|
||||||
@PreAuthorize("hasAuthority('platform:customer:view')")
|
@PreAuthorize("hasAuthority('platform:customer:view')")
|
||||||
public ResponseEntity<org.springframework.core.io.Resource> download(
|
public ResponseEntity<org.springframework.core.io.Resource> download(
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import com.baiyue.common.core.exception.BizException;
|
|||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.file.client.IamQualificationClient;
|
import com.baiyue.file.client.IamQualificationClient;
|
||||||
import com.baiyue.file.service.LocalQualificationStorage;
|
import com.baiyue.file.service.LocalQualificationStorage;
|
||||||
|
import com.baiyue.file.service.QualificationPackageService;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -26,6 +27,7 @@ public class QualificationFileController {
|
|||||||
private static final Logger LOGGER = LoggerFactory.getLogger(QualificationFileController.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(QualificationFileController.class);
|
||||||
@Resource private LocalQualificationStorage storage;
|
@Resource private LocalQualificationStorage storage;
|
||||||
@Resource private IamQualificationClient iamClient;
|
@Resource private IamQualificationClient iamClient;
|
||||||
|
@Resource private QualificationPackageService packageService;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||||
|
|
||||||
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
@PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
@@ -38,7 +40,7 @@ public class QualificationFileController {
|
|||||||
@RequestParam(required = false) LocalDate validTo,
|
@RequestParam(required = false) LocalDate validTo,
|
||||||
@RequestParam(defaultValue = "false") boolean replace) {
|
@RequestParam(defaultValue = "false") boolean replace) {
|
||||||
long tenantId = merchantTenant(user);
|
long tenantId = merchantTenant(user);
|
||||||
qualificationType = QualificationTypes.requireSupported(qualificationType);
|
qualificationType = QualificationTypes.requireOptional(qualificationType);
|
||||||
var stored = storage.store(tenantId, file);
|
var stored = storage.store(tenantId, file);
|
||||||
try {
|
try {
|
||||||
var response = iamClient.submit(internalToken,
|
var response = iamClient.submit(internalToken,
|
||||||
@@ -72,6 +74,22 @@ public class QualificationFileController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping(value = "/required-package", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||||
|
@PreAuthorize("hasAuthority('merchant:qualification:upload')")
|
||||||
|
public ApiResponse<QualificationPackageService.PackageUploadResult> uploadRequiredPackage(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestPart("businessLicense") MultipartFile businessLicense,
|
||||||
|
@RequestPart("legalPersonId") MultipartFile legalPersonId,
|
||||||
|
@RequestParam String unifiedSocialCreditCode,
|
||||||
|
@RequestParam(required = false) LocalDate businessLicenseValidTo,
|
||||||
|
@RequestParam(required = false) LocalDate legalPersonIdValidTo,
|
||||||
|
@RequestParam(defaultValue = "false") boolean replaceBusinessLicense,
|
||||||
|
@RequestParam(defaultValue = "false") boolean replaceLegalPersonId) {
|
||||||
|
return ApiResponse.success(packageService.upload(merchantTenant(user), businessLicense, legalPersonId,
|
||||||
|
unifiedSocialCreditCode, businessLicenseValidTo, legalPersonIdValidTo,
|
||||||
|
replaceBusinessLicense, replaceLegalPersonId, false));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{fileName}")
|
@GetMapping("/{fileName}")
|
||||||
public ResponseEntity<org.springframework.core.io.Resource> download(
|
public ResponseEntity<org.springframework.core.io.Resource> download(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String fileName) {
|
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String fileName) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.file.controller;
|
package com.baiyue.file.controller;
|
||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
@@ -19,4 +20,13 @@ final class QualificationTypes {
|
|||||||
}
|
}
|
||||||
return qualificationType;
|
return qualificationType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String requireOptional(String qualificationType) {
|
||||||
|
String type = requireSupported(qualificationType);
|
||||||
|
if ("BUSINESS_LICENSE".equals(type) || "LEGAL_PERSON_ID".equals(type)) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
|
"Required qualifications must be uploaded as one package / 营业执照、统一社会信用代码和法人身份证必须整包上传");
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.baiyue.file.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.file.client.IamQualificationClient;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class QualificationPackageService {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(QualificationPackageService.class);
|
||||||
|
@Resource private LocalQualificationStorage storage;
|
||||||
|
@Resource private IamQualificationClient iamClient;
|
||||||
|
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||||
|
|
||||||
|
public PackageUploadResult upload(long tenantId, MultipartFile businessLicense,
|
||||||
|
MultipartFile legalPersonId, String creditCode,
|
||||||
|
LocalDate businessLicenseValidTo, LocalDate legalPersonIdValidTo,
|
||||||
|
boolean replaceBusinessLicense, boolean replaceLegalPersonId,
|
||||||
|
boolean platform) {
|
||||||
|
String normalizedCreditCode = creditCode == null ? "" : creditCode.trim().toUpperCase();
|
||||||
|
if (!normalizedCreditCode.matches("[0-9A-Z]{18}")) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST, "请输入有效的18位统一社会信用代码");
|
||||||
|
}
|
||||||
|
LocalQualificationStorage.StoredFile storedBusinessLicense = storage.store(tenantId, businessLicense);
|
||||||
|
LocalQualificationStorage.StoredFile storedLegalPersonId;
|
||||||
|
try {
|
||||||
|
storedLegalPersonId = storage.store(tenantId, legalPersonId);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
storage.delete(storedBusinessLicense);
|
||||||
|
throw exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = new IamQualificationClient.RequiredPackageRequest(
|
||||||
|
tenantId, normalizedCreditCode, storedBusinessLicense.documentUrl(), businessLicenseValidTo,
|
||||||
|
replaceBusinessLicense, storedLegalPersonId.documentUrl(), legalPersonIdValidTo, replaceLegalPersonId);
|
||||||
|
try {
|
||||||
|
var response = platform
|
||||||
|
? iamClient.submitPlatformRequiredPackage(internalToken, request)
|
||||||
|
: iamClient.submitRequiredPackage(internalToken, request);
|
||||||
|
if (response == null) throw new IllegalStateException("IAM required qualification package response is empty");
|
||||||
|
if (!response.successful()) {
|
||||||
|
ResultCode code = ResultCode.of(response.code());
|
||||||
|
String message = response.message() == null || response.message().isBlank()
|
||||||
|
? code.message() : response.message();
|
||||||
|
storage.delete(storedBusinessLicense);
|
||||||
|
storage.delete(storedLegalPersonId);
|
||||||
|
throw new BizException(code, message);
|
||||||
|
}
|
||||||
|
if (response.data() == null) {
|
||||||
|
throw new IllegalStateException("IAM required qualification package response data is empty");
|
||||||
|
}
|
||||||
|
var data = response.data();
|
||||||
|
deleteReplacedFile(tenantId, data.replacedBusinessLicenseDocumentUrl(), storedBusinessLicense.documentUrl());
|
||||||
|
deleteReplacedFile(tenantId, data.replacedLegalPersonIdDocumentUrl(), storedLegalPersonId.documentUrl());
|
||||||
|
return new PackageUploadResult(data.businessLicenseQualificationId(), data.legalPersonIdQualificationId(),
|
||||||
|
data.tenantStatus(), storedBusinessLicense.documentUrl(), storedLegalPersonId.documentUrl());
|
||||||
|
} catch (BizException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
LOGGER.error("IAM 必填资质包登记结果未知,保留已写入文件等待重试或对账,tenantId={}, businessLicenseUrl={}, legalPersonIdUrl={}",
|
||||||
|
tenantId, storedBusinessLicense.documentUrl(), storedLegalPersonId.documentUrl(), exception);
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "IAM qualification service is unavailable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteReplacedFile(long tenantId, String replacedUrl, String currentUrl) {
|
||||||
|
if (!currentUrl.equals(replacedUrl)) storage.deleteByDocumentUrl(tenantId, replacedUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record PackageUploadResult(Long businessLicenseQualificationId,
|
||||||
|
Long legalPersonIdQualificationId,
|
||||||
|
String tenantStatus,
|
||||||
|
String businessLicenseDocumentUrl,
|
||||||
|
String legalPersonIdDocumentUrl) { }
|
||||||
|
}
|
||||||
@@ -61,6 +61,13 @@ class QualificationFileContractTest {
|
|||||||
assertTrue(hasMultipartPost);
|
assertTrue(hasMultipartPost);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantAndPlatformExposeDedicatedRequiredPackageRoutes() {
|
||||||
|
assertTrue(hasMultipartRoute(QualificationFileController.class, "/required-package"));
|
||||||
|
assertTrue(hasMultipartRoute(PlatformQualificationFileController.class,
|
||||||
|
"/{tenantId}/required-package"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformQualificationPreviewUsesTheCustomerViewPermission() throws Exception {
|
void platformQualificationPreviewUsesTheCustomerViewPermission() throws Exception {
|
||||||
var method = PlatformQualificationFileController.class.getDeclaredMethod(
|
var method = PlatformQualificationFileController.class.getDeclaredMethod(
|
||||||
@@ -82,7 +89,7 @@ class QualificationFileContractTest {
|
|||||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
|
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
|
||||||
|
|
||||||
var response = controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null,
|
var response = controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null,
|
||||||
LocalDate.of(2030, 12, 31), true);
|
LocalDate.of(2030, 12, 31), true);
|
||||||
|
|
||||||
var ordered = inOrder(storage, iamClient);
|
var ordered = inOrder(storage, iamClient);
|
||||||
@@ -108,7 +115,7 @@ class QualificationFileContractTest {
|
|||||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
when(iamClient.submitPlatform(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", currentUrl)));
|
new IamQualificationClient.PlatformQualificationResponse(31L, null, "PROFILE_REQUIRED", currentUrl)));
|
||||||
|
|
||||||
controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true);
|
controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, true);
|
||||||
|
|
||||||
verify(storage, never()).deleteByDocumentUrl(10001L, currentUrl);
|
verify(storage, never()).deleteByDocumentUrl(10001L, currentUrl);
|
||||||
}
|
}
|
||||||
@@ -124,7 +131,7 @@ class QualificationFileContractTest {
|
|||||||
when(iamClient.submitPlatform(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
when(iamClient.submitPlatform(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
||||||
|
|
||||||
var exception = assertThrows(BizException.class,
|
var exception = assertThrows(BizException.class,
|
||||||
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, true));
|
() -> controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, true));
|
||||||
|
|
||||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||||
verify(storage, never()).delete(stored);
|
verify(storage, never()).delete(stored);
|
||||||
@@ -142,7 +149,7 @@ class QualificationFileContractTest {
|
|||||||
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
|
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
|
||||||
|
|
||||||
var exception = assertThrows(BizException.class,
|
var exception = assertThrows(BizException.class,
|
||||||
() -> controller.upload(platformUser(), 10001L, file, "LEGAL_PERSON_ID", null, null, false));
|
() -> controller.upload(platformUser(), 10001L, file, "BANK_ACCOUNT_LICENSE", null, null, false));
|
||||||
|
|
||||||
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||||
verify(storage).delete(stored);
|
verify(storage).delete(stored);
|
||||||
@@ -159,7 +166,7 @@ class QualificationFileContractTest {
|
|||||||
when(iamClient.submit(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
when(iamClient.submit(eq("internal-token"), any())).thenThrow(new RuntimeException("timeout"));
|
||||||
|
|
||||||
var exception = assertThrows(BizException.class,
|
var exception = assertThrows(BizException.class,
|
||||||
() -> controller.upload(merchantUser(), file, "BUSINESS_LICENSE", null, null, false));
|
() -> controller.upload(merchantUser(), file, "FREIGHT_FORWARDER_CERTIFICATE", null, null, false));
|
||||||
|
|
||||||
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||||
verify(storage, never()).delete(stored);
|
verify(storage, never()).delete(stored);
|
||||||
@@ -177,12 +184,12 @@ class QualificationFileContractTest {
|
|||||||
when(iamClient.submit(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
when(iamClient.submit(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
new IamQualificationClient.QualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
|
new IamQualificationClient.QualificationResponse(31L, null, "PROFILE_REQUIRED", oldUrl)));
|
||||||
|
|
||||||
controller.upload(merchantUser(), file, "LEGAL_PERSON_ID", null, LocalDate.of(2030, 1, 1), true);
|
controller.upload(merchantUser(), file, "FREIGHT_FORWARDER_CERTIFICATE", null, LocalDate.of(2030, 1, 1), true);
|
||||||
|
|
||||||
var requestCaptor = org.mockito.ArgumentCaptor.forClass(IamQualificationClient.QualificationRequest.class);
|
var requestCaptor = org.mockito.ArgumentCaptor.forClass(IamQualificationClient.QualificationRequest.class);
|
||||||
verify(iamClient).submit(eq("internal-token"), requestCaptor.capture());
|
verify(iamClient).submit(eq("internal-token"), requestCaptor.capture());
|
||||||
verify(storage).deleteByDocumentUrl(10001L, oldUrl);
|
verify(storage).deleteByDocumentUrl(10001L, oldUrl);
|
||||||
assertEquals("LEGAL_PERSON_ID", requestCaptor.getValue().qualificationType());
|
assertEquals("FREIGHT_FORWARDER_CERTIFICATE", requestCaptor.getValue().qualificationType());
|
||||||
assertTrue(requestCaptor.getValue().replace());
|
assertTrue(requestCaptor.getValue().replace());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,9 +210,8 @@ class QualificationFileContractTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void everyRequiredQualificationTypeIsAcceptedAndForwarded() {
|
void everyOptionalQualificationTypeIsAcceptedAndForwarded() {
|
||||||
for (String qualificationType : Set.of(
|
for (String qualificationType : Set.of(
|
||||||
"BUSINESS_LICENSE", "LEGAL_PERSON_ID",
|
|
||||||
"FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE")) {
|
"FREIGHT_FORWARDER_CERTIFICATE", "BANK_ACCOUNT_LICENSE")) {
|
||||||
var storage = mock(LocalQualificationStorage.class);
|
var storage = mock(LocalQualificationStorage.class);
|
||||||
var iamClient = mock(IamQualificationClient.class);
|
var iamClient = mock(IamQualificationClient.class);
|
||||||
@@ -224,6 +230,22 @@ class QualificationFileContractTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void legacySingleUploadRejectsBothRequiredTypesBeforeWritingTheFile() {
|
||||||
|
for (String qualificationType : Set.of("BUSINESS_LICENSE", "LEGAL_PERSON_ID")) {
|
||||||
|
var storage = mock(LocalQualificationStorage.class);
|
||||||
|
var iamClient = mock(IamQualificationClient.class);
|
||||||
|
var controller = merchantController(storage, iamClient);
|
||||||
|
var file = new MockMultipartFile("file", "qualification.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||||
|
|
||||||
|
assertThrows(BizException.class,
|
||||||
|
() -> controller.upload(merchantUser(), file, qualificationType, null, null, false));
|
||||||
|
|
||||||
|
verify(storage, never()).store(any(Long.class), any());
|
||||||
|
verify(iamClient, never()).submit(any(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PlatformQualificationFileController platformController(
|
private static PlatformQualificationFileController platformController(
|
||||||
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
||||||
var controller = new PlatformQualificationFileController();
|
var controller = new PlatformQualificationFileController();
|
||||||
@@ -233,6 +255,14 @@ class QualificationFileContractTest {
|
|||||||
return controller;
|
return controller;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean hasMultipartRoute(Class<?> controllerType, String route) {
|
||||||
|
return Arrays.stream(controllerType.getDeclaredMethods())
|
||||||
|
.map(method -> method.getAnnotation(PostMapping.class))
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.anyMatch(mapping -> Arrays.asList(mapping.value()).contains(route)
|
||||||
|
&& Arrays.asList(mapping.consumes()).contains(MediaType.MULTIPART_FORM_DATA_VALUE));
|
||||||
|
}
|
||||||
|
|
||||||
private static QualificationFileController merchantController(
|
private static QualificationFileController merchantController(
|
||||||
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
LocalQualificationStorage storage, IamQualificationClient iamClient) {
|
||||||
var controller = new QualificationFileController();
|
var controller = new QualificationFileController();
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package com.baiyue.file.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.file.client.IamQualificationClient;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.springframework.mock.web.MockMultipartFile;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.inOrder;
|
||||||
|
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 QualificationPackageServiceTest {
|
||||||
|
private static final long TENANT_ID = 10001L;
|
||||||
|
private static final String BUSINESS_URL =
|
||||||
|
"/api/merchant/v1/files/qualifications/11111111-1111-1111-1111-111111111111.pdf";
|
||||||
|
private static final String LEGAL_PERSON_URL =
|
||||||
|
"/api/merchant/v1/files/qualifications/22222222-2222-2222-2222-222222222222.png";
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void storesBothFilesBeforeRegisteringOneIamPackage() {
|
||||||
|
var fixture = fixture();
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenReturn(fixture.legalPersonStored);
|
||||||
|
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any())).thenReturn(ApiResponse.success(
|
||||||
|
new IamQualificationClient.RequiredPackageResponse(31L, 32L, "PROFILE_REQUIRED", null, null)));
|
||||||
|
|
||||||
|
var result = fixture.service.upload(TENANT_ID, fixture.businessLicense, fixture.legalPersonId,
|
||||||
|
"91310101abcde12345", null, null, false, false, false);
|
||||||
|
|
||||||
|
var ordered = inOrder(fixture.storage, fixture.iamClient);
|
||||||
|
ordered.verify(fixture.storage).store(TENANT_ID, fixture.businessLicense);
|
||||||
|
ordered.verify(fixture.storage).store(TENANT_ID, fixture.legalPersonId);
|
||||||
|
var captor = ArgumentCaptor.forClass(IamQualificationClient.RequiredPackageRequest.class);
|
||||||
|
ordered.verify(fixture.iamClient).submitRequiredPackage(eq("internal-token"), captor.capture());
|
||||||
|
assertEquals("91310101ABCDE12345", captor.getValue().unifiedSocialCreditCode());
|
||||||
|
assertEquals(BUSINESS_URL, result.businessLicenseDocumentUrl());
|
||||||
|
assertEquals(LEGAL_PERSON_URL, result.legalPersonIdDocumentUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void secondStorageFailureDeletesTheFirstFileAndNeverCallsIam() {
|
||||||
|
var fixture = fixture();
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenThrow(new BizException("disk full"));
|
||||||
|
|
||||||
|
assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
|
||||||
|
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
|
||||||
|
null, null, false, false, false));
|
||||||
|
|
||||||
|
verify(fixture.storage).delete(fixture.businessStored);
|
||||||
|
verify(fixture.iamClient, never()).submitRequiredPackage(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void explicitIamRejectionDeletesBothUnreferencedFiles() {
|
||||||
|
var fixture = storedFixture();
|
||||||
|
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any())).thenReturn(
|
||||||
|
ApiResponse.failure(ResultCode.CONFLICT, "duplicate", null, null));
|
||||||
|
|
||||||
|
var exception = assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
|
||||||
|
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
|
||||||
|
null, null, false, false, false));
|
||||||
|
|
||||||
|
assertEquals(ResultCode.CONFLICT, exception.getResultCode());
|
||||||
|
verify(fixture.storage).delete(fixture.businessStored);
|
||||||
|
verify(fixture.storage).delete(fixture.legalPersonStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unknownIamOutcomePreservesBothFilesForReconciliation() {
|
||||||
|
var fixture = storedFixture();
|
||||||
|
when(fixture.iamClient.submitRequiredPackage(eq("internal-token"), any()))
|
||||||
|
.thenThrow(new RuntimeException("timeout"));
|
||||||
|
|
||||||
|
var exception = assertThrows(BizException.class, () -> fixture.service.upload(TENANT_ID,
|
||||||
|
fixture.businessLicense, fixture.legalPersonId, "91310101ABCDE12345",
|
||||||
|
null, null, false, false, false));
|
||||||
|
|
||||||
|
assertEquals(ResultCode.SERVICE_UNAVAILABLE, exception.getResultCode());
|
||||||
|
verify(fixture.storage, never()).delete(fixture.businessStored);
|
||||||
|
verify(fixture.storage, never()).delete(fixture.legalPersonStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformSuccessCleansReplacedFilesOnlyAfterIamCommit() {
|
||||||
|
var fixture = storedFixture();
|
||||||
|
var oldBusinessUrl = "/api/merchant/v1/files/qualifications/33333333-3333-3333-3333-333333333333.pdf";
|
||||||
|
var oldLegalPersonUrl = "/api/merchant/v1/files/qualifications/44444444-4444-4444-4444-444444444444.png";
|
||||||
|
when(fixture.iamClient.submitPlatformRequiredPackage(eq("internal-token"), any())).thenReturn(
|
||||||
|
ApiResponse.success(new IamQualificationClient.RequiredPackageResponse(
|
||||||
|
31L, 32L, "PROFILE_REQUIRED", oldBusinessUrl, oldLegalPersonUrl)));
|
||||||
|
|
||||||
|
fixture.service.upload(TENANT_ID, fixture.businessLicense, fixture.legalPersonId,
|
||||||
|
"91310101ABCDE12345", null, null, true, true, true);
|
||||||
|
|
||||||
|
var ordered = inOrder(fixture.iamClient, fixture.storage);
|
||||||
|
ordered.verify(fixture.iamClient).submitPlatformRequiredPackage(eq("internal-token"), any());
|
||||||
|
ordered.verify(fixture.storage).deleteByDocumentUrl(TENANT_ID, oldBusinessUrl);
|
||||||
|
ordered.verify(fixture.storage).deleteByDocumentUrl(TENANT_ID, oldLegalPersonUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture storedFixture() {
|
||||||
|
var fixture = fixture();
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.businessLicense)).thenReturn(fixture.businessStored);
|
||||||
|
when(fixture.storage.store(TENANT_ID, fixture.legalPersonId)).thenReturn(fixture.legalPersonStored);
|
||||||
|
return fixture;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Fixture fixture() {
|
||||||
|
var storage = mock(LocalQualificationStorage.class);
|
||||||
|
var iamClient = mock(IamQualificationClient.class);
|
||||||
|
var service = new QualificationPackageService();
|
||||||
|
ReflectionTestUtils.setField(service, "storage", storage);
|
||||||
|
ReflectionTestUtils.setField(service, "iamClient", iamClient);
|
||||||
|
ReflectionTestUtils.setField(service, "internalToken", "internal-token");
|
||||||
|
var businessLicense = new MockMultipartFile(
|
||||||
|
"businessLicense", "license.pdf", "application/pdf", "%PDF-1.4".getBytes());
|
||||||
|
var legalPersonId = new MockMultipartFile(
|
||||||
|
"legalPersonId", "legal-person.png", "image/png", new byte[] {1, 2, 3});
|
||||||
|
var businessStored = new LocalQualificationStorage.StoredFile(
|
||||||
|
Path.of("business.pdf"), "business.pdf", BUSINESS_URL);
|
||||||
|
var legalPersonStored = new LocalQualificationStorage.StoredFile(
|
||||||
|
Path.of("legal-person.png"), "legal-person.png", LEGAL_PERSON_URL);
|
||||||
|
return new Fixture(service, storage, iamClient, businessLicense, legalPersonId,
|
||||||
|
businessStored, legalPersonStored);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Fixture(QualificationPackageService service,
|
||||||
|
LocalQualificationStorage storage,
|
||||||
|
IamQualificationClient iamClient,
|
||||||
|
MockMultipartFile businessLicense,
|
||||||
|
MockMultipartFile legalPersonId,
|
||||||
|
LocalQualificationStorage.StoredFile businessStored,
|
||||||
|
LocalQualificationStorage.StoredFile legalPersonStored) { }
|
||||||
|
}
|
||||||
+13
-2
@@ -2,6 +2,8 @@ package com.baiyue.firstleg.controller;
|
|||||||
|
|
||||||
import com.baiyue.common.audit.BusinessAudit;
|
import com.baiyue.common.audit.BusinessAudit;
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.firstleg.dto.FbaPricingDtos.CarrierRateVersionRequest;
|
import com.baiyue.firstleg.dto.FbaPricingDtos.CarrierRateVersionRequest;
|
||||||
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
|
import com.baiyue.firstleg.dto.FbaPricingDtos.CustomerRuleVersionRequest;
|
||||||
@@ -36,10 +38,12 @@ public class PlatformFbaMasterDataController {
|
|||||||
|
|
||||||
@GetMapping("/customer-rules")
|
@GetMapping("/customer-rules")
|
||||||
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:view')")
|
@PreAuthorize("hasAuthority('firstleg:fba-pricing:rule:view')")
|
||||||
|
@Operation(summary = "查询客户计费规则版本", description = "默认查询全部企业;tenantId 可用于筛选目标企业")
|
||||||
public ApiResponse<List<CustomerChargeRuleEntity>> rules(
|
public ApiResponse<List<CustomerChargeRuleEntity>> rules(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) Long tenantId,
|
||||||
@RequestParam(required = false) String ruleCode) {
|
@RequestParam(required = false) String ruleCode) {
|
||||||
return ApiResponse.success(service.listRules(FirstlegScopeGuard.platform(user, tenantId), ruleCode));
|
return ApiResponse.success(service.listRules(optionalPlatformTenant(user, tenantId), ruleCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/customer-rules/{id}")
|
@GetMapping("/customer-rules/{id}")
|
||||||
@@ -105,4 +109,11 @@ public class PlatformFbaMasterDataController {
|
|||||||
service.retireRate(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
|
service.retireRate(FirstlegScopeGuard.platform(user, tenantId), id, user.userId());
|
||||||
return ApiResponse.success(null);
|
return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Long optionalPlatformTenant(AuthenticatedUser user, Long tenantId) {
|
||||||
|
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||||
|
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||||
|
}
|
||||||
|
return tenantId == null ? null : FirstlegScopeGuard.platform(user, tenantId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-6
@@ -10,10 +10,13 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
|
|||||||
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
import com.baiyue.firstleg.dto.InboundOrderSummary;
|
||||||
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
import com.baiyue.firstleg.service.InboundOrderQueryService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
@@ -22,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
/** 平台端入库单列表和详情只读接口。 */
|
/** 平台端入库单列表和详情只读接口。 */
|
||||||
@Tag(name = "平台入库单", description = "二期入库单列表、页签、筛选和只读聚合详情")
|
@Tag(name = "平台入库单", description = "二期入库单列表、页签、筛选和只读聚合详情")
|
||||||
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/inbounds")
|
@RequestMapping("/api/platform/inbounds")
|
||||||
public class PlatformInboundOrderController {
|
public class PlatformInboundOrderController {
|
||||||
@@ -31,14 +35,16 @@ public class PlatformInboundOrderController {
|
|||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "分页查询入库单", description = "按目标租户和二期状态页签分页查询入库单快照。")
|
@Operation(summary = "分页查询入库单",
|
||||||
|
description = "默认分页查询全部企业入库单;可按目标租户和二期状态页签筛选入库单快照。")
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
|
@PreAuthorize("hasAuthority('platform:firstleg:inbound:list')")
|
||||||
public ApiResponse<PageResult<InboundOrderSummary>> page(
|
public ApiResponse<PageResult<InboundOrderSummary>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam long tenantId,
|
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
|
||||||
|
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||||
@Valid InboundOrderQuery query) {
|
@Valid InboundOrderQuery query) {
|
||||||
requirePlatformTenant(user, tenantId);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(tenantId, query));
|
return ApiResponse.success(service.page(tenantId, query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,11 +63,15 @@ public class PlatformInboundOrderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||||
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
requirePlatform(user);
|
||||||
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
|
||||||
}
|
|
||||||
if (tenantId <= 0) {
|
if (tenantId <= 0) {
|
||||||
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
throw new BizException(ResultCode.BAD_REQUEST, "必须指定有效目标租户");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void requirePlatform(AuthenticatedUser user) {
|
||||||
|
if (user == null || !"PLATFORM".equals(user.appCode())) {
|
||||||
|
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.time.LocalDateTime;
|
|||||||
|
|
||||||
/** 入库列表行,所有名称和运输信息均为业务快照。 */
|
/** 入库列表行,所有名称和运输信息均为业务快照。 */
|
||||||
public record InboundOrderSummary(
|
public record InboundOrderSummary(
|
||||||
Long id, Long orderId, String orderNo, String inboundCode,
|
Long id, Long tenantId, Long orderId, String orderNo, String inboundCode,
|
||||||
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
|
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
|
||||||
String feeStatus, String feeReviewStatus, String notificationStatus,
|
String feeStatus, String feeReviewStatus, String notificationStatus,
|
||||||
Integer version,
|
Integer version,
|
||||||
@@ -33,7 +33,7 @@ public record InboundOrderSummary(
|
|||||||
public static InboundOrderSummary from(
|
public static InboundOrderSummary from(
|
||||||
InboundOrderEntity order, InboundTransportSnapshotEntity transport, String locationCodes) {
|
InboundOrderEntity order, InboundTransportSnapshotEntity transport, String locationCodes) {
|
||||||
return new InboundOrderSummary(
|
return new InboundOrderSummary(
|
||||||
order.getId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
|
order.getId(), order.getTenantId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
|
||||||
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
|
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
|
||||||
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
|
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
|
||||||
order.getVersion(),
|
order.getVersion(),
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import org.springframework.stereotype.Repository;
|
|||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** FBA 计费仓储,所有业务查询都强制带租户条件。 */
|
/** FBA 计费仓储;平台客户规则列表可跨企业读取,其他业务操作强制带租户条件。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class FbaPricingRepository {
|
public class FbaPricingRepository {
|
||||||
private final CustomerChargeRuleMapper rules;
|
private final CustomerChargeRuleMapper rules;
|
||||||
@@ -61,9 +61,9 @@ public class FbaPricingRepository {
|
|||||||
.eq(CarrierFbaRateEntity::getId, id));
|
.eq(CarrierFbaRateEntity::getId, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CustomerChargeRuleEntity> listRules(long tenantId, String ruleCode) {
|
public List<CustomerChargeRuleEntity> listRules(Long tenantId, String ruleCode) {
|
||||||
return rules.selectList(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
return rules.selectList(new LambdaQueryWrapper<CustomerChargeRuleEntity>()
|
||||||
.eq(CustomerChargeRuleEntity::getTenantId, tenantId)
|
.eq(tenantId != null, CustomerChargeRuleEntity::getTenantId, tenantId)
|
||||||
.eq(ruleCode != null && !ruleCode.isBlank(), CustomerChargeRuleEntity::getRuleCode, ruleCode)
|
.eq(ruleCode != null && !ruleCode.isBlank(), CustomerChargeRuleEntity::getRuleCode, ruleCode)
|
||||||
.orderByAsc(CustomerChargeRuleEntity::getRuleCode)
|
.orderByAsc(CustomerChargeRuleEntity::getRuleCode)
|
||||||
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
|
.orderByDesc(CustomerChargeRuleEntity::getRuleVersion));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import java.util.Map;
|
|||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/** 入库单只读仓储,所有查询均以租户作为首要隔离条件。 */
|
/** 入库单只读仓储;平台分页可跨企业查询,详情继续强制租户隔离。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class InboundOrderRepository {
|
public class InboundOrderRepository {
|
||||||
private final InboundOrderMapper orders;
|
private final InboundOrderMapper orders;
|
||||||
@@ -51,9 +51,9 @@ public class InboundOrderRepository {
|
|||||||
this.histories = histories;
|
this.histories = histories;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<InboundOrderSummary> page(long tenantId, InboundOrderQuery query) {
|
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
|
||||||
LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>()
|
LambdaQueryWrapper<InboundOrderEntity> wrapper = new LambdaQueryWrapper<InboundOrderEntity>()
|
||||||
.eq(InboundOrderEntity::getTenantId, tenantId)
|
.eq(tenantId != null, InboundOrderEntity::getTenantId, tenantId)
|
||||||
.likeRight(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode,
|
.likeRight(StringUtils.hasText(query.inboundCode()), InboundOrderEntity::getInboundCode,
|
||||||
query.inboundCode())
|
query.inboundCode())
|
||||||
.likeRight(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo())
|
.likeRight(StringUtils.hasText(query.orderNo()), InboundOrderEntity::getOrderNo, query.orderNo())
|
||||||
@@ -85,9 +85,10 @@ public class InboundOrderRepository {
|
|||||||
.orderByDesc(InboundOrderEntity::getCreatedAt)
|
.orderByDesc(InboundOrderEntity::getCreatedAt)
|
||||||
.orderByDesc(InboundOrderEntity::getId);
|
.orderByDesc(InboundOrderEntity::getId);
|
||||||
Page<InboundOrderEntity> result = orders.selectPage(new Page<>(query.page(), query.size()), wrapper);
|
Page<InboundOrderEntity> result = orders.selectPage(new Page<>(query.page(), query.size()), wrapper);
|
||||||
List<Long> ids = result.getRecords().stream().map(InboundOrderEntity::getId).toList();
|
Map<Long, Long> tenantByInbound = result.getRecords().stream()
|
||||||
Map<Long, InboundTransportSnapshotEntity> transportByInbound = transportMap(tenantId, ids);
|
.collect(Collectors.toMap(InboundOrderEntity::getId, InboundOrderEntity::getTenantId));
|
||||||
Map<Long, String> locationCodesByInbound = locationCodesMap(tenantId, ids);
|
Map<Long, InboundTransportSnapshotEntity> transportByInbound = transportMap(tenantId, tenantByInbound);
|
||||||
|
Map<Long, String> locationCodesByInbound = locationCodesMap(tenantId, tenantByInbound);
|
||||||
List<InboundOrderSummary> records = result.getRecords().stream()
|
List<InboundOrderSummary> records = result.getRecords().stream()
|
||||||
.map(order -> InboundOrderSummary.from(order, transportByInbound.get(order.getId()),
|
.map(order -> InboundOrderSummary.from(order, transportByInbound.get(order.getId()),
|
||||||
locationCodesByInbound.get(order.getId())))
|
locationCodesByInbound.get(order.getId())))
|
||||||
@@ -156,27 +157,31 @@ public class InboundOrderRepository {
|
|||||||
palletSummaries, fileSummaries, noteSummaries, historySummaries);
|
palletSummaries, fileSummaries, noteSummaries, historySummaries);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, InboundTransportSnapshotEntity> transportMap(long tenantId, List<Long> inboundIds) {
|
private Map<Long, InboundTransportSnapshotEntity> transportMap(
|
||||||
if (inboundIds.isEmpty()) {
|
Long tenantId, Map<Long, Long> tenantByInbound) {
|
||||||
|
if (tenantByInbound.isEmpty()) {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
return transports.selectList(new LambdaQueryWrapper<InboundTransportSnapshotEntity>()
|
return transports.selectList(new LambdaQueryWrapper<InboundTransportSnapshotEntity>()
|
||||||
.eq(InboundTransportSnapshotEntity::getTenantId, tenantId)
|
.eq(tenantId != null, InboundTransportSnapshotEntity::getTenantId, tenantId)
|
||||||
.in(InboundTransportSnapshotEntity::getInboundId, inboundIds))
|
.in(InboundTransportSnapshotEntity::getInboundId, tenantByInbound.keySet()))
|
||||||
.stream().collect(Collectors.toMap(InboundTransportSnapshotEntity::getInboundId,
|
.stream()
|
||||||
|
.filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||||
|
.collect(Collectors.toMap(InboundTransportSnapshotEntity::getInboundId,
|
||||||
Function.identity(), (left, right) -> left));
|
Function.identity(), (left, right) -> left));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<Long, String> locationCodesMap(long tenantId, List<Long> inboundIds) {
|
private Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
|
||||||
if (inboundIds.isEmpty()) {
|
if (tenantByInbound.isEmpty()) {
|
||||||
return Map.of();
|
return Map.of();
|
||||||
}
|
}
|
||||||
return pallets.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
return pallets.selectList(new LambdaQueryWrapper<InboundPalletAllocationEntity>()
|
||||||
.eq(InboundPalletAllocationEntity::getTenantId, tenantId)
|
.eq(tenantId != null, InboundPalletAllocationEntity::getTenantId, tenantId)
|
||||||
.in(InboundPalletAllocationEntity::getInboundId, inboundIds)
|
.in(InboundPalletAllocationEntity::getInboundId, tenantByInbound.keySet())
|
||||||
.orderByAsc(InboundPalletAllocationEntity::getInboundId)
|
.orderByAsc(InboundPalletAllocationEntity::getInboundId)
|
||||||
.orderByAsc(InboundPalletAllocationEntity::getAllocationNo))
|
.orderByAsc(InboundPalletAllocationEntity::getAllocationNo))
|
||||||
.stream()
|
.stream()
|
||||||
|
.filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
|
||||||
.filter(item -> StringUtils.hasText(item.getLocationCode()))
|
.filter(item -> StringUtils.hasText(item.getLocationCode()))
|
||||||
.collect(Collectors.groupingBy(InboundPalletAllocationEntity::getInboundId,
|
.collect(Collectors.groupingBy(InboundPalletAllocationEntity::getInboundId,
|
||||||
java.util.LinkedHashMap::new,
|
java.util.LinkedHashMap::new,
|
||||||
@@ -184,6 +189,10 @@ public class InboundOrderRepository {
|
|||||||
Collectors.joining("、"))));
|
Collectors.joining("、"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean owns(Map<Long, Long> tenantByInbound, Long inboundId, Long tenantId) {
|
||||||
|
return inboundId != null && tenantId != null && tenantId.equals(tenantByInbound.get(inboundId));
|
||||||
|
}
|
||||||
|
|
||||||
private static <T> LambdaQueryWrapper<T> owned(
|
private static <T> LambdaQueryWrapper<T> owned(
|
||||||
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> tenantColumn,
|
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> tenantColumn,
|
||||||
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> inboundColumn,
|
com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, ?> inboundColumn,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class FbaMasterDataService {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CustomerChargeRuleEntity> listRules(long tenantId, String code) {
|
public List<CustomerChargeRuleEntity> listRules(Long tenantId, String code) {
|
||||||
return repository.listRules(tenantId, code);
|
return repository.listRules(tenantId, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class InboundOrderQueryService {
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<InboundOrderSummary> page(long tenantId, InboundOrderQuery query) {
|
public PageResult<InboundOrderSummary> page(Long tenantId, InboundOrderQuery query) {
|
||||||
validateQuery(query);
|
validateQuery(query);
|
||||||
return repository.page(tenantId, query);
|
return repository.page(tenantId, query);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: 查询客户计费规则版本
|
summary: 查询客户计费规则版本
|
||||||
description: 需要 firstleg:fba-pricing:rule:view 权限
|
description: 需要 firstleg:fba-pricing:rule:view 权限
|
||||||
parameters: [{$ref: '#/components/parameters/TenantId'}]
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/OptionalTenantId'}
|
||||||
|
- {name: ruleCode, in: query, schema: {type: string, maxLength: 50}}
|
||||||
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
responses: {'200': {description: 成功或业务失败}, '403': {description: 无权限}}
|
||||||
/api/platform/v1/fba-pricing/customer-rules/versions:
|
/api/platform/v1/fba-pricing/customer-rules/versions:
|
||||||
post:
|
post:
|
||||||
@@ -87,9 +89,10 @@ components:
|
|||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT}
|
||||||
parameters:
|
parameters:
|
||||||
TenantId: {name: tenantId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
OptionalTenantId: {name: tenantId, in: query, required: false, description: 不传时查询全部企业, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
Id: {name: id, in: path, required: true, schema: {type: integer, format: int64}}
|
TenantId: {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
InboundId: {name: inboundId, in: path, required: true, schema: {type: integer, format: int64}}
|
Id: {name: id, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
InboundId: {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
IdempotencyKey: {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||||
schemas:
|
schemas:
|
||||||
AdjustFeeRequest:
|
AdjustFeeRequest:
|
||||||
@@ -107,7 +110,7 @@ components:
|
|||||||
required: [ruleCode, customerId, customerName, businessType, chargeMode, taxMode, conversionFactor, roundingScale, roundingMode, currency, effectiveFrom]
|
required: [ruleCode, customerId, customerName, businessType, chargeMode, taxMode, conversionFactor, roundingScale, roundingMode, currency, effectiveFrom]
|
||||||
properties:
|
properties:
|
||||||
ruleCode: {type: string, maxLength: 50}
|
ruleCode: {type: string, maxLength: 50}
|
||||||
customerId: {type: integer, format: int64}
|
customerId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
customerName: {type: string, maxLength: 128}
|
customerName: {type: string, maxLength: 128}
|
||||||
businessType: {type: string, maxLength: 32}
|
businessType: {type: string, maxLength: 32}
|
||||||
chargeMode: {type: string, enum: [KG, CBM]}
|
chargeMode: {type: string, enum: [KG, CBM]}
|
||||||
@@ -123,11 +126,11 @@ components:
|
|||||||
required: [rateCode, originWarehouseId, originWarehouseName, businessType, channelCode, carrierId, carrierName, destinationCode, chargeUnit, unitPrice, currency, additionalFee, effectiveFrom]
|
required: [rateCode, originWarehouseId, originWarehouseName, businessType, channelCode, carrierId, carrierName, destinationCode, chargeUnit, unitPrice, currency, additionalFee, effectiveFrom]
|
||||||
properties:
|
properties:
|
||||||
rateCode: {type: string, maxLength: 50}
|
rateCode: {type: string, maxLength: 50}
|
||||||
originWarehouseId: {type: integer, format: int64}
|
originWarehouseId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
originWarehouseName: {type: string}
|
originWarehouseName: {type: string}
|
||||||
businessType: {type: string}
|
businessType: {type: string}
|
||||||
channelCode: {type: string}
|
channelCode: {type: string}
|
||||||
carrierId: {type: integer, format: int64}
|
carrierId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
carrierName: {type: string}
|
carrierName: {type: string}
|
||||||
destinationCode: {type: string}
|
destinationCode: {type: string}
|
||||||
chargeUnit: {type: string, enum: [KG, CBM]}
|
chargeUnit: {type: string, enum: [KG, CBM]}
|
||||||
|
|||||||
@@ -127,12 +127,16 @@ paths:
|
|||||||
/api/platform/inbounds:
|
/api/platform/inbounds:
|
||||||
get:
|
get:
|
||||||
summary: 分页查询入库单
|
summary: 分页查询入库单
|
||||||
description: 需要 platform:firstleg:inbound:list 权限;仅使用 V2 四个页签
|
description: 需要 platform:firstleg:inbound:list 权限;默认查询全部企业,可选 tenantId 筛选;仅使用 V2 四个页签
|
||||||
parameters:
|
parameters:
|
||||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: tenantId, in: query, required: false, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
|
- {name: tab, in: query, schema: {type: string, enum: [WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]}}
|
||||||
responses:
|
responses:
|
||||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
'200':
|
||||||
|
description: 成功或业务失败,按响应体 code 判断;每条记录携带后续租户隔离操作所需的 tenantId
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/InboundOrderPageEnvelope'}
|
||||||
'403': {description: 无查询权限}
|
'403': {description: 无查询权限}
|
||||||
/api/platform/inbounds/{inboundId}:
|
/api/platform/inbounds/{inboundId}:
|
||||||
get:
|
get:
|
||||||
@@ -251,6 +255,34 @@ components:
|
|||||||
in: query
|
in: query
|
||||||
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
||||||
schemas:
|
schemas:
|
||||||
|
InboundOrderPageEnvelope:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
code: {type: string, example: '200'}
|
||||||
|
message: {type: string}
|
||||||
|
data: {$ref: '#/components/schemas/InboundOrderPage'}
|
||||||
|
InboundOrderPage:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
records:
|
||||||
|
type: array
|
||||||
|
items: {$ref: '#/components/schemas/InboundOrderSummary'}
|
||||||
|
page: {type: integer, minimum: 1}
|
||||||
|
size: {type: integer, minimum: 1}
|
||||||
|
total: {type: integer, format: int64, minimum: 0}
|
||||||
|
pages: {type: integer, format: int64, minimum: 0}
|
||||||
|
InboundOrderSummary:
|
||||||
|
type: object
|
||||||
|
required: [id, tenantId]
|
||||||
|
properties:
|
||||||
|
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||||
|
tenantId: {type: string, pattern: '^[1-9][0-9]*$', description: 记录所属企业租户 ID,用于详情和处理接口的租户隔离}
|
||||||
|
orderId: {type: string, nullable: true}
|
||||||
|
orderNo: {type: string, nullable: true}
|
||||||
|
inboundCode: {type: string, nullable: true}
|
||||||
|
processStatus: {type: string}
|
||||||
|
customerId: {type: string, nullable: true}
|
||||||
|
companyId: {type: string, nullable: true}
|
||||||
InboundMeasurementCallbackRequest:
|
InboundMeasurementCallbackRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [eventNo, externalDetailNo, warehouseCode, inboundCode, inboundDetailId, deviceCode, measuredAt, boxSpecification, packageCount, lengthCm, widthCm, heightCm]
|
required: [eventNo, externalDetailNo, warehouseCode, inboundCode, inboundDetailId, deviceCode, measuredAt, boxSpecification, packageCount, lengthCm, widthCm, heightCm]
|
||||||
|
|||||||
@@ -3,13 +3,30 @@ package com.baiyue.firstleg.controller;
|
|||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@DisplayName("FBA计费OpenAPI契约")
|
@DisplayName("FBA计费OpenAPI契约")
|
||||||
class FbaPricingContractTest {
|
class FbaPricingContractTest {
|
||||||
|
@Test
|
||||||
|
@DisplayName("只有客户规则列表允许省略企业参数")
|
||||||
|
void onlyCustomerRuleListAllowsOptionalTenant() {
|
||||||
|
Method rules = method("rules");
|
||||||
|
assertThat(rules.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||||
|
assertThat(rules.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
|
||||||
|
for (String scopedMethod : new String[] {"rule", "createRule", "retireRule", "rates",
|
||||||
|
"rate", "createRate", "retireRate"}) {
|
||||||
|
Method method = method(scopedMethod);
|
||||||
|
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||||
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("契约声明自动发布、已发布费用调整和幂等头")
|
@DisplayName("契约声明自动发布、已发布费用调整和幂等头")
|
||||||
void documentsPricingLifecycle() throws Exception {
|
void documentsPricingLifecycle() throws Exception {
|
||||||
@@ -28,5 +45,26 @@ class FbaPricingContractTest {
|
|||||||
.doesNotContain("应收")
|
.doesNotContain("应收")
|
||||||
.doesNotContain("应付")
|
.doesNotContain("应付")
|
||||||
.doesNotContain("待客户确认");
|
.doesNotContain("待客户确认");
|
||||||
|
String customerRules = openapi.substring(
|
||||||
|
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules:"),
|
||||||
|
openapi.indexOf("/api/platform/v1/fba-pricing/customer-rules/versions:"));
|
||||||
|
assertThat(customerRules)
|
||||||
|
.contains("OptionalTenantId")
|
||||||
|
.contains("name: ruleCode")
|
||||||
|
.doesNotContain("#/components/parameters/Page")
|
||||||
|
.doesNotContain("#/components/parameters/Size")
|
||||||
|
.doesNotContain("name: customerName")
|
||||||
|
.doesNotContain("name: chargeMode")
|
||||||
|
.doesNotContain("name: status");
|
||||||
|
assertThat(openapi)
|
||||||
|
.contains("OptionalTenantId: {name: tenantId, in: query, required: false")
|
||||||
|
.contains("schema: {type: string, pattern: '^[1-9][0-9]*$'}")
|
||||||
|
.doesNotContain("format: int64");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method method(String name) {
|
||||||
|
return java.util.Arrays.stream(PlatformFbaMasterDataController.class.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals(name))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-1
@@ -5,6 +5,7 @@ import com.baiyue.firstleg.dto.InboundOrderQuery;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -15,21 +16,40 @@ class InboundOrderControllerContractTest {
|
|||||||
@Test
|
@Test
|
||||||
void shouldDeclareReadOnlyPermissions() throws Exception {
|
void shouldDeclareReadOnlyPermissions() throws Exception {
|
||||||
assertAuthority("page", "hasAuthority('platform:firstleg:inbound:list')",
|
assertAuthority("page", "hasAuthority('platform:firstleg:inbound:list')",
|
||||||
AuthenticatedUser.class, long.class, InboundOrderQuery.class);
|
AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||||
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
|
assertAuthority("detail", "hasAuthority('platform:firstleg:inbound:view')",
|
||||||
AuthenticatedUser.class, long.class, long.class);
|
AuthenticatedUser.class, long.class, long.class);
|
||||||
|
|
||||||
|
Method page = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||||
|
"page", AuthenticatedUser.class, Long.class, InboundOrderQuery.class);
|
||||||
|
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
Method detail = PlatformInboundOrderController.class.getDeclaredMethod(
|
||||||
|
"detail", AuthenticatedUser.class, long.class, long.class);
|
||||||
|
assertThat(detail.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDocumentOnlyV2TabsAndStringLongIds() throws Exception {
|
void shouldDocumentOnlyV2TabsAndStringLongIds() throws Exception {
|
||||||
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
String openapi = new ClassPathResource("openapi/firstleg-v1.yaml")
|
||||||
.getContentAsString(StandardCharsets.UTF_8);
|
.getContentAsString(StandardCharsets.UTF_8);
|
||||||
|
String inboundList = openapi.substring(
|
||||||
|
openapi.indexOf("/api/platform/inbounds:"),
|
||||||
|
openapi.indexOf("/api/platform/inbounds/{inboundId}:"));
|
||||||
|
String taskList = openapi.substring(
|
||||||
|
openapi.indexOf("/api/platform/v1/first-leg/tasks:"),
|
||||||
|
openapi.indexOf("/api/platform/v1/first-leg/tasks/{taskId}:"));
|
||||||
assertThat(openapi)
|
assertThat(openapi)
|
||||||
.contains("/api/platform/inbounds:")
|
.contains("/api/platform/inbounds:")
|
||||||
.contains("/api/platform/inbounds/{inboundId}:")
|
.contains("/api/platform/inbounds/{inboundId}:")
|
||||||
|
.contains("required: false")
|
||||||
|
.contains("InboundOrderSummary:")
|
||||||
|
.contains("required: [id, tenantId]")
|
||||||
|
.contains("tenantId: {type: string, pattern: '^[1-9][0-9]*$', description:")
|
||||||
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
|
.contains("[WAIT_MEASURE, WAIT_REVIEW, WAIT_FEE_SEND, INBOUNDED]")
|
||||||
.contains("platform:firstleg:inbound:list")
|
.contains("platform:firstleg:inbound:list")
|
||||||
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
.doesNotContain("WAIT_CUSTOMER_CONFIRM");
|
||||||
|
assertThat(inboundList).contains("#/components/schemas/InboundOrderPageEnvelope");
|
||||||
|
assertThat(taskList).doesNotContain("InboundOrderPageEnvelope");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertAuthority(String methodName, String value, Class<?>... types)
|
private static void assertAuthority(String methodName, String value, Class<?>... types)
|
||||||
|
|||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
package com.baiyue.firstleg.repository;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.baiyue.firstleg.entity.CustomerChargeRuleEntity;
|
||||||
|
import com.baiyue.firstleg.mapper.CarrierFbaRateMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.CustomerChargeRuleMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundFeeItemMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundFeeMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundOutboxMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundPricingIdempotencyMapper;
|
||||||
|
import com.baiyue.firstleg.mapper.InboundPricingMapper;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.apache.ibatis.session.Configuration;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class FbaPricingRepositoryRuleListTest {
|
||||||
|
@BeforeAll
|
||||||
|
static void initializeEntityMetadata() {
|
||||||
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new Configuration(), "test"),
|
||||||
|
CustomerChargeRuleEntity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformRuleListOmitsTenantConditionWhenTenantIsAbsent() {
|
||||||
|
assertThat(queryValues(null, null)).isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformRuleListKeepsTenantAndRuleCodeFiltersWhenProvided() {
|
||||||
|
assertThat(queryValues(10001L, "CR-1")).containsExactlyInAnyOrder(10001L, "CR-1");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void internalVersionCalculationKeepsTenantAndRuleCodeFilters() {
|
||||||
|
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
|
||||||
|
CustomerChargeRuleEntity current = new CustomerChargeRuleEntity();
|
||||||
|
current.setRuleVersion(3);
|
||||||
|
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(current));
|
||||||
|
FbaPricingRepository repository = repository(rules);
|
||||||
|
|
||||||
|
int result = repository.nextRuleVersion(10001L, "CR-1");
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
|
||||||
|
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(rules).selectList(wrapper.capture());
|
||||||
|
wrapper.getValue().getSqlSegment();
|
||||||
|
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||||
|
.containsExactlyInAnyOrder(10001L, "CR-1");
|
||||||
|
assertThat(result).isEqualTo(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Collection<Object> queryValues(Long tenantId, String ruleCode) {
|
||||||
|
CustomerChargeRuleMapper rules = mock(CustomerChargeRuleMapper.class);
|
||||||
|
when(rules.selectList(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||||
|
FbaPricingRepository repository = repository(rules);
|
||||||
|
|
||||||
|
repository.listRules(tenantId, ruleCode);
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<CustomerChargeRuleEntity>> wrapper =
|
||||||
|
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(rules).selectList(wrapper.capture());
|
||||||
|
wrapper.getValue().getSqlSegment();
|
||||||
|
return wrapper.getValue().getParamNameValuePairs().values();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FbaPricingRepository repository(CustomerChargeRuleMapper rules) {
|
||||||
|
return new FbaPricingRepository(rules, mock(CarrierFbaRateMapper.class),
|
||||||
|
mock(InboundPricingMapper.class), mock(InboundFeeMapper.class), mock(InboundFeeItemMapper.class),
|
||||||
|
mock(InboundPricingIdempotencyMapper.class), mock(InboundOutboxMapper.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|||||||
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
import com.baiyue.firstleg.dto.InboundOrderQuery;
|
||||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
||||||
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
|
||||||
|
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
|
||||||
import com.baiyue.firstleg.mapper.InboundFileMapper;
|
import com.baiyue.firstleg.mapper.InboundFileMapper;
|
||||||
import com.baiyue.firstleg.mapper.InboundInternalNoteMapper;
|
import com.baiyue.firstleg.mapper.InboundInternalNoteMapper;
|
||||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
||||||
@@ -73,8 +74,10 @@ class InboundOrderRepositoryTest {
|
|||||||
Page<InboundOrderEntity> page = new Page<>(1, 20);
|
Page<InboundOrderEntity> page = new Page<>(1, 20);
|
||||||
InboundOrderEntity first = new InboundOrderEntity();
|
InboundOrderEntity first = new InboundOrderEntity();
|
||||||
first.setId(1L);
|
first.setId(1L);
|
||||||
|
first.setTenantId(10001L);
|
||||||
InboundOrderEntity second = new InboundOrderEntity();
|
InboundOrderEntity second = new InboundOrderEntity();
|
||||||
second.setId(2L);
|
second.setId(2L);
|
||||||
|
second.setTenantId(10001L);
|
||||||
page.setRecords(java.util.List.of(first, second));
|
page.setRecords(java.util.List.of(first, second));
|
||||||
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||||
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||||
@@ -104,12 +107,85 @@ class InboundOrderRepositoryTest {
|
|||||||
.contains("ORDER BY inbound_id ASC,allocation_no ASC");
|
.contains("ORDER BY inbound_id ASC,allocation_no ASC");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
void shouldQueryAndAggregateCrossTenantPageByInboundOwnership() {
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), "test"), InboundOrderEntity.class);
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||||
|
InboundTransportSnapshotEntity.class);
|
||||||
|
TableInfoHelper.initTableInfo(
|
||||||
|
new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
|
||||||
|
InboundPalletAllocationEntity.class);
|
||||||
|
InboundOrderMapper orders = mock(InboundOrderMapper.class);
|
||||||
|
InboundTransportSnapshotMapper transports = mock(InboundTransportSnapshotMapper.class);
|
||||||
|
InboundPalletAllocationMapper pallets = mock(InboundPalletAllocationMapper.class);
|
||||||
|
Page<InboundOrderEntity> page = new Page<>(1, 20);
|
||||||
|
page.setRecords(java.util.List.of(order(1L, 10001L), order(2L, 10002L)));
|
||||||
|
when(orders.selectPage(any(Page.class), any(Wrapper.class))).thenReturn(page);
|
||||||
|
when(transports.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||||
|
transport(1L, 10001L, "T1"),
|
||||||
|
transport(2L, 10002L, "T2"),
|
||||||
|
transport(1L, 10002L, "WRONG")));
|
||||||
|
when(pallets.selectList(any(Wrapper.class))).thenReturn(java.util.List.of(
|
||||||
|
allocation(1L, 10001L, 1, "A1"),
|
||||||
|
allocation(2L, 10002L, 1, "B1"),
|
||||||
|
allocation(1L, 10002L, 2, "WRONG")));
|
||||||
|
InboundOrderRepository repository = new InboundOrderRepository(
|
||||||
|
orders, transports, pallets, mock(InboundFileMapper.class),
|
||||||
|
mock(InboundInternalNoteMapper.class), mock(InboundStatusHistoryMapper.class));
|
||||||
|
|
||||||
|
var result = repository.page(null, new InboundOrderQuery(
|
||||||
|
null, null, null, null, null, null, null, null,
|
||||||
|
null, null, null, null, null, null, null, null, null, 1, 20));
|
||||||
|
|
||||||
|
assertThat(result.records()).extracting(record -> record.tenantId())
|
||||||
|
.containsExactly(10001L, 10002L);
|
||||||
|
assertThat(result.records()).extracting(record -> record.loadingStatus())
|
||||||
|
.containsExactly("T1", "T2");
|
||||||
|
assertThat(result.records()).extracting(record -> record.locationCodes())
|
||||||
|
.containsExactly("A1", "B1");
|
||||||
|
ArgumentCaptor<Wrapper<InboundOrderEntity>> orderCaptor = ArgumentCaptor.forClass(Wrapper.class);
|
||||||
|
verify(orders).selectPage(any(Page.class), orderCaptor.capture());
|
||||||
|
assertThat(orderCaptor.getValue().getCustomSqlSegment()).doesNotContain("tenant_id");
|
||||||
|
|
||||||
|
ArgumentCaptor<Wrapper<InboundTransportSnapshotEntity>> transportCaptor =
|
||||||
|
ArgumentCaptor.forClass(Wrapper.class);
|
||||||
|
verify(transports).selectList(transportCaptor.capture());
|
||||||
|
assertThat(transportCaptor.getValue().getCustomSqlSegment())
|
||||||
|
.doesNotContain("tenant_id")
|
||||||
|
.contains("inbound_id IN");
|
||||||
|
}
|
||||||
|
|
||||||
private static InboundPalletAllocationEntity allocation(
|
private static InboundPalletAllocationEntity allocation(
|
||||||
long inboundId, int allocationNo, String locationCode) {
|
long inboundId, int allocationNo, String locationCode) {
|
||||||
|
return allocation(inboundId, 10001L, allocationNo, locationCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundPalletAllocationEntity allocation(
|
||||||
|
long inboundId, long tenantId, int allocationNo, String locationCode) {
|
||||||
InboundPalletAllocationEntity entity = new InboundPalletAllocationEntity();
|
InboundPalletAllocationEntity entity = new InboundPalletAllocationEntity();
|
||||||
entity.setInboundId(inboundId);
|
entity.setInboundId(inboundId);
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
entity.setAllocationNo(allocationNo);
|
entity.setAllocationNo(allocationNo);
|
||||||
entity.setLocationCode(locationCode);
|
entity.setLocationCode(locationCode);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static InboundOrderEntity order(long id, long tenantId) {
|
||||||
|
InboundOrderEntity entity = new InboundOrderEntity();
|
||||||
|
entity.setId(id);
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static InboundTransportSnapshotEntity transport(
|
||||||
|
long inboundId, long tenantId, String loadingStatus) {
|
||||||
|
InboundTransportSnapshotEntity entity = new InboundTransportSnapshotEntity();
|
||||||
|
entity.setInboundId(inboundId);
|
||||||
|
entity.setTenantId(tenantId);
|
||||||
|
entity.setLoadingStatus(loadingStatus);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,27 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doAnswer;
|
import static org.mockito.Mockito.doAnswer;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@DisplayName("FBA计费基础资料版本")
|
@DisplayName("FBA计费基础资料版本")
|
||||||
class FbaMasterDataServiceTest {
|
class FbaMasterDataServiceTest {
|
||||||
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
|
private static final LocalDateTime AUGUST = LocalDateTime.of(2026, 8, 1, 0, 0);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("平台列表不传企业时委托仓储查询全部企业规则")
|
||||||
|
void listsRulesAcrossTenantsWithoutTenantFilter() {
|
||||||
|
FbaPricingRepository repository = mock(FbaPricingRepository.class);
|
||||||
|
List<CustomerChargeRuleEntity> expected = List.of(activeRule(AUGUST, null));
|
||||||
|
when(repository.listRules(null, "CR-1")).thenReturn(expected);
|
||||||
|
|
||||||
|
List<CustomerChargeRuleEntity> result =
|
||||||
|
new FbaMasterDataService(repository).listRules(null, "CR-1");
|
||||||
|
|
||||||
|
assertThat(result).isSameAs(expected);
|
||||||
|
verify(repository).listRules(null, "CR-1");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("半开有效期首尾相接不视为重叠")
|
@DisplayName("半开有效期首尾相接不视为重叠")
|
||||||
void acceptsAdjacentHalfOpenPeriods() {
|
void acceptsAdjacentHalfOpenPeriods() {
|
||||||
|
|||||||
@@ -21,6 +21,17 @@ import static org.mockito.Mockito.verify;
|
|||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
class InboundOrderQueryServiceTest {
|
class InboundOrderQueryServiceTest {
|
||||||
|
@Test
|
||||||
|
void shouldQueryAllPlatformTenantsWhenTenantFilterIsMissing() {
|
||||||
|
InboundOrderRepository repository = mock(InboundOrderRepository.class);
|
||||||
|
InboundOrderQuery query = query(null, null, null, null);
|
||||||
|
PageResult<InboundOrderSummary> expected = new PageResult<>(List.of(), 1, 20, 0, 0);
|
||||||
|
when(repository.page(null, query)).thenReturn(expected);
|
||||||
|
|
||||||
|
assertThat(new InboundOrderQueryService(repository).page(null, query)).isSameAs(expected);
|
||||||
|
verify(repository).page(null, query);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldKeepV2TabMappingWithoutCustomerConfirmation() {
|
void shouldKeepV2TabMappingWithoutCustomerConfirmation() {
|
||||||
assertThat(InboundListTab.WAIT_MEASURE.processStatuses())
|
assertThat(InboundListTab.WAIT_MEASURE.processStatuses())
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
|||||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
|
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
|
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
|
||||||
|
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
|
||||||
|
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageResponse;
|
||||||
import com.baiyue.iam.service.MerchantOnboardingService;
|
import com.baiyue.iam.service.MerchantOnboardingService;
|
||||||
import com.baiyue.iam.service.ProfileResourceService;
|
import com.baiyue.iam.service.ProfileResourceService;
|
||||||
import com.baiyue.iam.service.TenantService;
|
import com.baiyue.iam.service.TenantService;
|
||||||
@@ -73,6 +75,22 @@ public class InternalTenantController {
|
|||||||
return ApiResponse.success(profileResourceService.registerPlatformQualification(request));
|
return ApiResponse.success(profileResourceService.registerPlatformQualification(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/required-qualification-packages")
|
||||||
|
public ApiResponse<RequiredQualificationPackageResponse> submitRequiredQualificationPackage(
|
||||||
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
|
@Valid @RequestBody RequiredQualificationPackageRequest request) {
|
||||||
|
verify(token);
|
||||||
|
return ApiResponse.success(profileResourceService.submitRequiredQualificationPackage(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/platform-required-qualification-packages")
|
||||||
|
public ApiResponse<RequiredQualificationPackageResponse> registerPlatformRequiredQualificationPackage(
|
||||||
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
|
@Valid @RequestBody RequiredQualificationPackageRequest request) {
|
||||||
|
verify(token);
|
||||||
|
return ApiResponse.success(profileResourceService.registerPlatformRequiredQualificationPackage(request));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/tenants/{tenantId}/order-eligibility")
|
@GetMapping("/tenants/{tenantId}/order-eligibility")
|
||||||
public ApiResponse<OrderEligibilityResponse> orderEligibility(
|
public ApiResponse<OrderEligibilityResponse> orderEligibility(
|
||||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
|
|||||||
@@ -50,5 +50,24 @@ public final class OnboardingDtos {
|
|||||||
String replacedDocumentUrl
|
String replacedDocumentUrl
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
|
public record RequiredQualificationPackageRequest(
|
||||||
|
@NotNull Long tenantId,
|
||||||
|
@NotBlank @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
|
||||||
|
@NotBlank String businessLicenseDocumentUrl,
|
||||||
|
LocalDate businessLicenseValidTo,
|
||||||
|
boolean replaceBusinessLicense,
|
||||||
|
@NotBlank String legalPersonIdDocumentUrl,
|
||||||
|
LocalDate legalPersonIdValidTo,
|
||||||
|
boolean replaceLegalPersonId
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public record RequiredQualificationPackageResponse(
|
||||||
|
Long businessLicenseQualificationId,
|
||||||
|
Long legalPersonIdQualificationId,
|
||||||
|
String tenantStatus,
|
||||||
|
String replacedBusinessLicenseDocumentUrl,
|
||||||
|
String replacedLegalPersonIdDocumentUrl
|
||||||
|
) { }
|
||||||
|
|
||||||
public record OrderEligibilityResponse(boolean eligible, String tenantStatus, String reason) { }
|
public record OrderEligibilityResponse(boolean eligible, String tenantStatus, String reason) { }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ public final class TenantDtos {
|
|||||||
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
@Schema(description = "Enterprise display name / 企业名称", example = "深圳虫虫出海科技有限公司") @NotBlank String tenantName,
|
||||||
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
@Schema(description = "Legal entity name / 企业法定名称", example = "深圳虫虫出海科技有限公司") @NotBlank String legalName,
|
||||||
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
@Schema(description = "Legal representative / 法定代表人", example = "张三") @NotBlank @Size(max = 64) String legalRepresentative,
|
||||||
@Schema(description = "Optional while creating a draft; required before review / 创建草稿时选填,送审前必填", example = "91440300MA5EXAMPLE", nullable = true) @Pattern(regexp = "[0-9A-Za-z]{18}") String unifiedSocialCreditCode,
|
|
||||||
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
@Schema(description = "Primary contact / 联系人", example = "张三") @NotBlank String contactName,
|
||||||
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
@Schema(description = "Contact phone / 联系电话", example = "13800138000") @NotBlank String contactPhone,
|
||||||
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
@Schema(description = "Contact email / 联系邮箱", example = "ops@example.com") @Email String contactEmail,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
|||||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
|
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionResponse;
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationRequest;
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
|
import com.baiyue.iam.dto.OnboardingDtos.PlatformQualificationRegistrationResponse;
|
||||||
|
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.AddressRequest;
|
||||||
import com.baiyue.iam.entity.IamTenant;
|
import com.baiyue.iam.entity.IamTenant;
|
||||||
import com.baiyue.iam.entity.IamTenantAddress;
|
import com.baiyue.iam.entity.IamTenantAddress;
|
||||||
@@ -65,6 +67,7 @@ public class ProfileResourceService {
|
|||||||
throw new BizException(ResultCode.CONFLICT,
|
throw new BizException(ResultCode.CONFLICT,
|
||||||
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
"Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
||||||
}
|
}
|
||||||
|
requireOptionalQualificationType(request.qualificationType());
|
||||||
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
||||||
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
||||||
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
||||||
@@ -86,11 +89,6 @@ public class ProfileResourceService {
|
|||||||
request.qualificationType(), certificateNo, request.documentUrl());
|
request.qualificationType(), certificateNo, request.documentUrl());
|
||||||
qualification.setValidTo(request.validTo());
|
qualification.setValidTo(request.validTo());
|
||||||
repository.saveQualification(qualification);
|
repository.saveQualification(qualification);
|
||||||
if ("BUSINESS_LICENSE".equals(request.qualificationType())
|
|
||||||
&& !certificateNo.equals(tenant.getUnifiedSocialCreditCode())) {
|
|
||||||
tenant.setUnifiedSocialCreditCode(certificateNo);
|
|
||||||
tenantRepository.saveTenant(tenant);
|
|
||||||
}
|
|
||||||
return new QualificationSubmissionResponse(qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
return new QualificationSubmissionResponse(qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +103,7 @@ public class ProfileResourceService {
|
|||||||
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
if (!"PROFILE_REQUIRED".equals(tenant.getStatus()) && !"REJECTED".equals(tenant.getStatus())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
throw new BizException(ResultCode.CONFLICT, "Qualifications can only be uploaded before review / 仅待完善或已驳回客户可上传资质");
|
||||||
}
|
}
|
||||||
|
requireOptionalQualificationType(request.qualificationType());
|
||||||
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
String certificateNo = resolveCertificateNo(tenant, request.qualificationType(), request.certificateNo());
|
||||||
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
List<IamTenantQualification> activeQualifications = repository.findQualificationsForUpdate(tenant.getId()).stream()
|
||||||
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
.filter(item -> request.qualificationType().equals(item.getQualificationType())
|
||||||
@@ -126,15 +125,93 @@ public class ProfileResourceService {
|
|||||||
certificateNo, request.documentUrl());
|
certificateNo, request.documentUrl());
|
||||||
qualification.setValidTo(request.validTo());
|
qualification.setValidTo(request.validTo());
|
||||||
repository.saveQualification(qualification);
|
repository.saveQualification(qualification);
|
||||||
if ("BUSINESS_LICENSE".equals(request.qualificationType())
|
|
||||||
&& !certificateNo.equals(tenant.getUnifiedSocialCreditCode())) {
|
|
||||||
tenant.setUnifiedSocialCreditCode(certificateNo);
|
|
||||||
tenantRepository.saveTenant(tenant);
|
|
||||||
}
|
|
||||||
return new PlatformQualificationRegistrationResponse(
|
return new PlatformQualificationRegistrationResponse(
|
||||||
qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
qualification.getId(), null, tenant.getStatus(), replacedDocumentUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@BusinessAudit(event = "TENANT_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD", resourceType = "TENANT_QUALIFICATION",
|
||||||
|
resourceId = "#request.tenantId", description = "上传企业必填资质包", captureAfter = false)
|
||||||
|
public RequiredQualificationPackageResponse submitRequiredQualificationPackage(
|
||||||
|
RequiredQualificationPackageRequest request) {
|
||||||
|
return registerRequiredQualificationPackage(request, "商户在送审前替换文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@BusinessAudit(event = "PLATFORM_REQUIRED_QUALIFICATION_PACKAGE_UPLOAD", resourceType = "TENANT_QUALIFICATION",
|
||||||
|
resourceId = "#request.tenantId", description = "平台代客户上传必填资质包", captureAfter = false)
|
||||||
|
public RequiredQualificationPackageResponse registerPlatformRequiredQualificationPackage(
|
||||||
|
RequiredQualificationPackageRequest request) {
|
||||||
|
return registerRequiredQualificationPackage(request, "平台在送审前替换文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
private RequiredQualificationPackageResponse registerRequiredQualificationPackage(
|
||||||
|
RequiredQualificationPackageRequest request, String replacementComment) {
|
||||||
|
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 / 仅待完善或已驳回客户可上传资质");
|
||||||
|
}
|
||||||
|
String creditCode = resolveCertificateNo(tenant, "BUSINESS_LICENSE", request.unifiedSocialCreditCode());
|
||||||
|
List<IamTenantQualification> locked = repository.findQualificationsForUpdate(tenant.getId());
|
||||||
|
List<IamTenantQualification> businessLicenses = currentQualifications(locked, "BUSINESS_LICENSE");
|
||||||
|
List<IamTenantQualification> legalPersonIds = currentQualifications(locked, "LEGAL_PERSON_ID");
|
||||||
|
requireReplaceAllowed(businessLicenses, request.replaceBusinessLicense(), "营业执照");
|
||||||
|
requireReplaceAllowed(legalPersonIds, request.replaceLegalPersonId(), "法人身份证");
|
||||||
|
|
||||||
|
String replacedBusinessLicenseUrl = supersede(businessLicenses, replacementComment);
|
||||||
|
String replacedLegalPersonIdUrl = supersede(legalPersonIds, replacementComment);
|
||||||
|
IamTenantQualification businessLicense = createQualification(tenant.getId(), "BUSINESS_LICENSE",
|
||||||
|
creditCode, request.businessLicenseDocumentUrl());
|
||||||
|
businessLicense.setValidTo(request.businessLicenseValidTo());
|
||||||
|
repository.saveQualification(businessLicense);
|
||||||
|
IamTenantQualification legalPersonId = createQualification(tenant.getId(), "LEGAL_PERSON_ID",
|
||||||
|
null, request.legalPersonIdDocumentUrl());
|
||||||
|
legalPersonId.setValidTo(request.legalPersonIdValidTo());
|
||||||
|
repository.saveQualification(legalPersonId);
|
||||||
|
if (!creditCode.equals(tenant.getUnifiedSocialCreditCode())) {
|
||||||
|
tenant.setUnifiedSocialCreditCode(creditCode);
|
||||||
|
tenantRepository.saveTenant(tenant);
|
||||||
|
}
|
||||||
|
return new RequiredQualificationPackageResponse(
|
||||||
|
businessLicense.getId(), legalPersonId.getId(), tenant.getStatus(),
|
||||||
|
replacedBusinessLicenseUrl, replacedLegalPersonIdUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IamTenantQualification> currentQualifications(
|
||||||
|
List<IamTenantQualification> qualifications, String type) {
|
||||||
|
return qualifications.stream()
|
||||||
|
.filter(item -> type.equals(item.getQualificationType()) && isCurrentQualification(item))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireReplaceAllowed(List<IamTenantQualification> current, boolean replace, String label) {
|
||||||
|
if (!current.isEmpty() && !replace) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, label + "已上传,请明确选择替换");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String supersede(List<IamTenantQualification> current, String comment) {
|
||||||
|
String replacedUrl = null;
|
||||||
|
for (IamTenantQualification existing : current) {
|
||||||
|
if (replacedUrl == null) replacedUrl = existing.getDocumentUrl();
|
||||||
|
existing.setStatus("SUPERSEDED");
|
||||||
|
existing.setReviewComment(comment);
|
||||||
|
repository.saveQualification(existing);
|
||||||
|
}
|
||||||
|
return replacedUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireOptionalQualificationType(String type) {
|
||||||
|
if ("BUSINESS_LICENSE".equals(type) || "LEGAL_PERSON_ID".equals(type)) {
|
||||||
|
throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
|
"Required qualifications must be uploaded as one package / 营业执照、统一社会信用代码和法人身份证必须整包上传");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveCertificateNo(IamTenant tenant, String qualificationType, String submittedCertificateNo) {
|
private String resolveCertificateNo(IamTenant tenant, String qualificationType, String submittedCertificateNo) {
|
||||||
if (!"BUSINESS_LICENSE".equals(qualificationType)) {
|
if (!"BUSINESS_LICENSE".equals(qualificationType)) {
|
||||||
return submittedCertificateNo;
|
return submittedCertificateNo;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import com.baiyue.iam.dto.CustomerExportDtos.CustomerExportRow;
|
|||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -54,11 +53,6 @@ public class TenantService {
|
|||||||
)
|
)
|
||||||
@Transactional
|
@Transactional
|
||||||
public TenantResponse register(RegisterRequest request, long operatorId) {
|
public TenantResponse register(RegisterRequest request, long operatorId) {
|
||||||
String creditCode = normalizeCreditCode(request.unifiedSocialCreditCode());
|
|
||||||
if (creditCode != null && repository.existsByCreditCode(creditCode)) {
|
|
||||||
throw new BizException(ResultCode.CONFLICT, "Enterprise already exists / 企业已存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
IamTenant tenant = new IamTenant();
|
IamTenant tenant = new IamTenant();
|
||||||
tenant.setId(IdWorker.getId());
|
tenant.setId(IdWorker.getId());
|
||||||
tenant.setTenantCode("TEN" + tenant.getId());
|
tenant.setTenantCode("TEN" + tenant.getId());
|
||||||
@@ -67,7 +61,7 @@ public class TenantService {
|
|||||||
tenant.setStatus("PROFILE_REQUIRED");
|
tenant.setStatus("PROFILE_REQUIRED");
|
||||||
tenant.setLegalName(request.legalName());
|
tenant.setLegalName(request.legalName());
|
||||||
tenant.setLegalRepresentative(request.legalRepresentative());
|
tenant.setLegalRepresentative(request.legalRepresentative());
|
||||||
tenant.setUnifiedSocialCreditCode(creditCode);
|
tenant.setUnifiedSocialCreditCode(null);
|
||||||
tenant.setContactName(request.contactName());
|
tenant.setContactName(request.contactName());
|
||||||
tenant.setContactPhone(request.contactPhone());
|
tenant.setContactPhone(request.contactPhone());
|
||||||
tenant.setContactEmail(request.contactEmail());
|
tenant.setContactEmail(request.contactEmail());
|
||||||
@@ -334,10 +328,6 @@ public class TenantService {
|
|||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeCreditCode(String creditCode) {
|
|
||||||
return StringUtils.hasText(creditCode) ? creditCode.trim().toUpperCase() : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private TenantResponse response(IamTenant tenant) {
|
private TenantResponse response(IamTenant tenant) {
|
||||||
return new TenantResponse(
|
return new TenantResponse(
|
||||||
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
tenant.getId(), tenant.getTenantCode(), tenant.getTenantName(), tenant.getStatus(),
|
||||||
|
|||||||
@@ -608,6 +608,46 @@ paths:
|
|||||||
'400': {$ref: '#/components/responses/BadRequest'}
|
'400': {$ref: '#/components/responses/BadRequest'}
|
||||||
'403': {$ref: '#/components/responses/Forbidden'}
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
'409': {$ref: '#/components/responses/Conflict'}
|
'409': {$ref: '#/components/responses/Conflict'}
|
||||||
|
/internal/iam/required-qualification-packages:
|
||||||
|
post:
|
||||||
|
tags: [IAM Resources / IAM 资源]
|
||||||
|
summary: 商户必填资质包登记
|
||||||
|
description: 仅供 File Service 调用;在一个 IAM 事务中登记营业执照、统一社会信用代码和法人身份证。
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/RequiredQualificationPackageRequest'}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 统一响应;仅 data 非空且 code 为 200 表示资质包已完整登记
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/RequiredQualificationPackageApiResponse'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
|
/internal/iam/platform-required-qualification-packages:
|
||||||
|
post:
|
||||||
|
tags: [IAM Resources / IAM 资源]
|
||||||
|
summary: 平台代客户必填资质包登记
|
||||||
|
description: 仅供 File Service 调用;与商户入口使用相同的三项事务保存规则。
|
||||||
|
security: []
|
||||||
|
parameters:
|
||||||
|
- {$ref: '#/components/parameters/InternalServiceToken'}
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/RequiredQualificationPackageRequest'}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 统一响应;仅 data 非空且 code 为 200 表示资质包已完整登记
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {$ref: '#/components/schemas/RequiredQualificationPackageApiResponse'}
|
||||||
|
'403': {$ref: '#/components/responses/Forbidden'}
|
||||||
/internal/exports/v1/CUSTOMER_LIST/batches:
|
/internal/exports/v1/CUSTOMER_LIST/batches:
|
||||||
post:
|
post:
|
||||||
tags: [Internal Export Provider]
|
tags: [Internal Export Provider]
|
||||||
@@ -650,6 +690,11 @@ components:
|
|||||||
scheme: bearer
|
scheme: bearer
|
||||||
bearerFormat: JWT
|
bearerFormat: JWT
|
||||||
parameters:
|
parameters:
|
||||||
|
InternalServiceToken:
|
||||||
|
name: X-Internal-Service-Token
|
||||||
|
in: header
|
||||||
|
required: true
|
||||||
|
schema: {type: string}
|
||||||
Id:
|
Id:
|
||||||
name: id
|
name: id
|
||||||
in: path
|
in: path
|
||||||
@@ -838,6 +883,33 @@ components:
|
|||||||
validTo: {type: string, format: date, nullable: true}
|
validTo: {type: string, format: date, nullable: true}
|
||||||
status: {type: string, enum: [PENDING, APPROVED, REJECTED, SUPERSEDED]}
|
status: {type: string, enum: [PENDING, APPROVED, REJECTED, SUPERSEDED]}
|
||||||
reviewComment: {type: string}
|
reviewComment: {type: string}
|
||||||
|
RequiredQualificationPackageRequest:
|
||||||
|
type: object
|
||||||
|
required: [tenantId, unifiedSocialCreditCode, businessLicenseDocumentUrl, replaceBusinessLicense, legalPersonIdDocumentUrl, replaceLegalPersonId]
|
||||||
|
properties:
|
||||||
|
tenantId: {type: string, pattern: '^\d+$'}
|
||||||
|
unifiedSocialCreditCode: {type: string, pattern: '^[0-9A-Za-z]{18}$'}
|
||||||
|
businessLicenseDocumentUrl: {type: string, minLength: 1}
|
||||||
|
businessLicenseValidTo: {type: string, format: date, nullable: true}
|
||||||
|
replaceBusinessLicense: {type: boolean}
|
||||||
|
legalPersonIdDocumentUrl: {type: string, minLength: 1}
|
||||||
|
legalPersonIdValidTo: {type: string, format: date, nullable: true}
|
||||||
|
replaceLegalPersonId: {type: boolean}
|
||||||
|
RequiredQualificationPackageResponse:
|
||||||
|
type: object
|
||||||
|
required: [businessLicenseQualificationId, legalPersonIdQualificationId, tenantStatus]
|
||||||
|
properties:
|
||||||
|
businessLicenseQualificationId: {type: string, pattern: '^\d+$'}
|
||||||
|
legalPersonIdQualificationId: {type: string, pattern: '^\d+$'}
|
||||||
|
tenantStatus: {type: string, enum: [PROFILE_REQUIRED, REJECTED]}
|
||||||
|
replacedBusinessLicenseDocumentUrl: {type: string, nullable: true}
|
||||||
|
replacedLegalPersonIdDocumentUrl: {type: string, nullable: true}
|
||||||
|
RequiredQualificationPackageApiResponse:
|
||||||
|
allOf:
|
||||||
|
- {$ref: '#/components/schemas/ApiResponse'}
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
data: {$ref: '#/components/schemas/RequiredQualificationPackageResponse'}
|
||||||
RegisterRequest:
|
RegisterRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone, servicePackage]
|
required: [tenantName, legalName, legalRepresentative, contactName, contactPhone, servicePackage]
|
||||||
@@ -845,7 +917,6 @@ components:
|
|||||||
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
tenantName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
legalName: {type: string, example: 深圳虫虫出海科技有限公司}
|
||||||
legalRepresentative: {type: string, maxLength: 64, example: 张三}
|
legalRepresentative: {type: string, maxLength: 64, example: 张三}
|
||||||
unifiedSocialCreditCode: {type: string, nullable: true, pattern: '^[0-9A-Za-z]{18}$', example: 91440300MA5ABCDE12, description: 创建草稿时选填,送审前必填}
|
|
||||||
contactName: {type: string, example: 李四}
|
contactName: {type: string, example: 李四}
|
||||||
contactPhone: {type: string, example: '13800138000'}
|
contactPhone: {type: string, example: '13800138000'}
|
||||||
contactEmail: {type: string, format: email, example: contact@example.com}
|
contactEmail: {type: string, format: email, example: contact@example.com}
|
||||||
|
|||||||
@@ -38,4 +38,28 @@ class QualificationUploadContractTest {
|
|||||||
assertTrue(yaml.contains(" /api/merchant/v1/iam/tenant/qualifications/submit-review:"));
|
assertTrue(yaml.contains(" /api/merchant/v1/iam/tenant/qualifications/submit-review:"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void internalControllerExposesBothRequiredPackageRoutes() {
|
||||||
|
var routes = java.util.Arrays.stream(InternalTenantController.class.getDeclaredMethods())
|
||||||
|
.map(method -> method.getAnnotation(PostMapping.class))
|
||||||
|
.filter(java.util.Objects::nonNull)
|
||||||
|
.flatMap(mapping -> java.util.Arrays.stream(mapping.value()))
|
||||||
|
.toList();
|
||||||
|
assertTrue(routes.contains("/required-qualification-packages"));
|
||||||
|
assertTrue(routes.contains("/platform-required-qualification-packages"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void staticOpenApiDocumentsTheRequiredPackageContract() throws IOException {
|
||||||
|
try (var input = getClass().getClassLoader().getResourceAsStream("openapi/iam-v1.yaml")) {
|
||||||
|
assertNotNull(input);
|
||||||
|
String yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
assertTrue(yaml.contains("/internal/iam/required-qualification-packages:"));
|
||||||
|
assertTrue(yaml.contains("/internal/iam/platform-required-qualification-packages:"));
|
||||||
|
assertTrue(yaml.contains("RequiredQualificationPackageRequest:"));
|
||||||
|
assertTrue(yaml.contains("replaceBusinessLicense"));
|
||||||
|
assertTrue(yaml.contains("replaceLegalPersonId"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.iam.service;
|
package com.baiyue.iam.service;
|
||||||
|
|
||||||
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
import com.baiyue.iam.dto.OnboardingDtos.QualificationSubmissionRequest;
|
||||||
|
import com.baiyue.iam.dto.OnboardingDtos.RequiredQualificationPackageRequest;
|
||||||
import com.baiyue.iam.entity.IamTenant;
|
import com.baiyue.iam.entity.IamTenant;
|
||||||
import com.baiyue.iam.repository.ProfileResourceRepository;
|
import com.baiyue.iam.repository.ProfileResourceRepository;
|
||||||
import com.baiyue.iam.repository.TenantRepository;
|
import com.baiyue.iam.repository.TenantRepository;
|
||||||
@@ -18,12 +19,13 @@ import static org.mockito.Mockito.mock;
|
|||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
|
||||||
class MerchantQualificationUploadTest {
|
class MerchantQualificationUploadTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void uploadsOneDraftQualificationWithoutCreatingAReview() {
|
void uploadsOneOptionalDraftQualificationWithoutCreatingAReview() {
|
||||||
TenantRepository tenantRepository = mock(TenantRepository.class);
|
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
ProfileResourceService service = new ProfileResourceService();
|
ProfileResourceService service = new ProfileResourceService();
|
||||||
@@ -37,8 +39,8 @@ class MerchantQualificationUploadTest {
|
|||||||
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||||
|
|
||||||
var response = service.submitQualification(new QualificationSubmissionRequest(
|
var response = service.submitQualification(new QualificationSubmissionRequest(
|
||||||
10001L, "LEGAL_PERSON_ID", null,
|
10001L, "FREIGHT_FORWARDER_CERTIFICATE", null,
|
||||||
"/api/merchant/v1/files/qualifications/legal-person.png", LocalDate.of(2030, 1, 1), false));
|
"/api/merchant/v1/files/qualifications/freight-forwarder.png", LocalDate.of(2030, 1, 1), false));
|
||||||
|
|
||||||
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
||||||
assertNull(response.reviewId());
|
assertNull(response.reviewId());
|
||||||
@@ -47,7 +49,26 @@ class MerchantQualificationUploadTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void businessLicenseRequiresAndPersistsAUniqueUnifiedSocialCreditCode() {
|
void requiredQualificationsRejectTheLegacySingleFilePath() {
|
||||||
|
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);
|
||||||
|
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
|
||||||
|
10001L, "BUSINESS_LICENSE", "91310101abcde12345",
|
||||||
|
"/api/merchant/v1/files/qualifications/license.png", null, false)));
|
||||||
|
|
||||||
|
verify(profileRepository, never()).insertQualification(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiredPackagePersistsBothFilesAndCreditCodeTogether() {
|
||||||
TenantRepository tenantRepository = mock(TenantRepository.class);
|
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
ProfileResourceService service = new ProfileResourceService();
|
ProfileResourceService service = new ProfileResourceService();
|
||||||
@@ -60,21 +81,19 @@ class MerchantQualificationUploadTest {
|
|||||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
when(profileRepository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||||
|
|
||||||
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
|
var response = service.submitRequiredQualificationPackage(new RequiredQualificationPackageRequest(
|
||||||
10001L, "BUSINESS_LICENSE", null,
|
10001L, "91310101abcde12345",
|
||||||
"/api/merchant/v1/files/qualifications/license.png", null, false)));
|
"/api/merchant/v1/files/qualifications/license.png", null, false,
|
||||||
|
"/api/merchant/v1/files/qualifications/legal-person.png", null, false));
|
||||||
var response = service.submitQualification(new QualificationSubmissionRequest(
|
|
||||||
10001L, "BUSINESS_LICENSE", "91310101abcde12345",
|
|
||||||
"/api/merchant/v1/files/qualifications/license.png", null, false));
|
|
||||||
|
|
||||||
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
||||||
assertEquals("91310101ABCDE12345", tenant.getUnifiedSocialCreditCode());
|
assertEquals("91310101ABCDE12345", tenant.getUnifiedSocialCreditCode());
|
||||||
|
verify(profileRepository, times(2)).insertQualification(any());
|
||||||
verify(tenantRepository).saveTenant(tenant);
|
verify(tenantRepository).saveTenant(tenant);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void businessLicenseCannotOverwriteAnEstablishedCreditCode() {
|
void requiredPackageCannotOverwriteAnEstablishedCreditCode() {
|
||||||
TenantRepository tenantRepository = mock(TenantRepository.class);
|
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||||
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository profileRepository = mock(ProfileResourceRepository.class);
|
||||||
ProfileResourceService service = new ProfileResourceService();
|
ProfileResourceService service = new ProfileResourceService();
|
||||||
@@ -87,11 +106,11 @@ class MerchantQualificationUploadTest {
|
|||||||
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
tenant.setUnifiedSocialCreditCode("91330100EXAMPLE001");
|
||||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
|
|
||||||
assertThrows(BizException.class, () -> service.submitQualification(new QualificationSubmissionRequest(
|
assertThrows(BizException.class, () -> service.submitRequiredQualificationPackage(
|
||||||
10001L, "BUSINESS_LICENSE", "91310101ABCDE12345",
|
new RequiredQualificationPackageRequest(10001L, "91310101ABCDE12345",
|
||||||
"/api/merchant/v1/files/qualifications/license.png", null, true)));
|
"/api/merchant/v1/files/qualifications/license.png", null, true,
|
||||||
|
"/api/merchant/v1/files/qualifications/legal-person.png", null, true)));
|
||||||
|
|
||||||
verify(profileRepository, never()).insertQualification(any());
|
verify(profileRepository, never()).insertQualification(any());
|
||||||
verify(tenantRepository, never()).saveTenant(tenant);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import static org.mockito.Mockito.when;
|
|||||||
|
|
||||||
class PlatformQualificationRegistrationTest {
|
class PlatformQualificationRegistrationTest {
|
||||||
@Test
|
@Test
|
||||||
void platformUploadRegistersMetadataWithoutCreatingReviewTicket() {
|
void platformUploadRegistersOptionalMetadataWithoutCreatingReviewTicket() {
|
||||||
ProfileResourceRepository repository = mock(ProfileResourceRepository.class);
|
ProfileResourceRepository repository = mock(ProfileResourceRepository.class);
|
||||||
TenantRepository tenantRepository = mock(TenantRepository.class);
|
TenantRepository tenantRepository = mock(TenantRepository.class);
|
||||||
ProfileResourceService service = new ProfileResourceService();
|
ProfileResourceService service = new ProfileResourceService();
|
||||||
@@ -33,13 +33,13 @@ class PlatformQualificationRegistrationTest {
|
|||||||
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of());
|
||||||
|
|
||||||
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
||||||
10001L, "LEGAL_PERSON_ID", null, "/api/merchant/v1/files/qualifications/file.pdf",
|
10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/file.pdf",
|
||||||
LocalDate.of(2030, 12, 31), false));
|
LocalDate.of(2030, 12, 31), false));
|
||||||
|
|
||||||
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
||||||
verify(repository).insertQualification(captor.capture());
|
verify(repository).insertQualification(captor.capture());
|
||||||
verify(repository).saveQualification(captor.getValue());
|
verify(repository).saveQualification(captor.getValue());
|
||||||
assertEquals("LEGAL_PERSON_ID", captor.getValue().getQualificationType());
|
assertEquals("BANK_ACCOUNT_LICENSE", captor.getValue().getQualificationType());
|
||||||
assertEquals(LocalDate.of(2030, 12, 31), captor.getValue().getValidTo());
|
assertEquals(LocalDate.of(2030, 12, 31), captor.getValue().getValidTo());
|
||||||
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
assertEquals("PROFILE_REQUIRED", response.tenantStatus());
|
||||||
assertNull(response.reviewId());
|
assertNull(response.reviewId());
|
||||||
@@ -56,14 +56,14 @@ class PlatformQualificationRegistrationTest {
|
|||||||
IamTenant tenant = new IamTenant();
|
IamTenant tenant = new IamTenant();
|
||||||
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("PROFILE_REQUIRED");
|
tenant.setId(10001L); tenant.setTenantType("MERCHANT"); tenant.setStatus("PROFILE_REQUIRED");
|
||||||
IamTenantQualification existing = new IamTenantQualification();
|
IamTenantQualification existing = new IamTenantQualification();
|
||||||
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("LEGAL_PERSON_ID");
|
existing.setId(88L); existing.setTenantId(10001L); existing.setQualificationType("BANK_ACCOUNT_LICENSE");
|
||||||
existing.setStatus("PENDING");
|
existing.setStatus("PENDING");
|
||||||
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf");
|
existing.setDocumentUrl("/api/merchant/v1/files/qualifications/old-file.pdf");
|
||||||
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
when(tenantRepository.findTenantForUpdate(10001L)).thenReturn(tenant);
|
||||||
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
|
when(repository.findQualificationsForUpdate(10001L)).thenReturn(List.of(existing));
|
||||||
|
|
||||||
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
var response = service.registerPlatformQualification(new PlatformQualificationRegistrationRequest(
|
||||||
10001L, "LEGAL_PERSON_ID", null, "/api/merchant/v1/files/qualifications/new-file.pdf",
|
10001L, "BANK_ACCOUNT_LICENSE", null, "/api/merchant/v1/files/qualifications/new-file.pdf",
|
||||||
LocalDate.of(2031, 12, 31), true));
|
LocalDate.of(2031, 12, 31), true));
|
||||||
|
|
||||||
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
ArgumentCaptor<IamTenantQualification> captor = ArgumentCaptor.forClass(IamTenantQualification.class);
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ class TenantServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "repository", repository);
|
ReflectionTestUtils.setField(service, "repository", repository);
|
||||||
|
|
||||||
var created = service.register(new RegisterRequest(
|
var created = service.register(new RegisterRequest(
|
||||||
"Example Merchant", "Example Merchant Ltd", "Bob", "91330100EXAMPLE001", "Alice", "13800138000",
|
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||||
"alice@example.com", "基础版", "alice_wechat", "Sales Zhang", 30, "LIMITED_COMPANY",
|
"alice@example.com", "基础版", "alice_wechat", "Sales Zhang", 30, "LIMITED_COMPANY",
|
||||||
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
|
LocalDate.of(2020, 1, 2), new BigDecimal("5000000.00"), "CNY", "深圳市南山区", "草稿客户"),
|
||||||
9001L);
|
9001L);
|
||||||
@@ -134,13 +134,13 @@ class TenantServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformRegistrationAllowsCreditCodeToBeCompletedWithQualificationsLater() {
|
void platformRegistrationAlwaysCreatesTheDraftWithoutACreditCode() {
|
||||||
TenantRepository repository = mock(TenantRepository.class);
|
TenantRepository repository = mock(TenantRepository.class);
|
||||||
TenantService service = new TenantService();
|
TenantService service = new TenantService();
|
||||||
ReflectionTestUtils.setField(service, "repository", repository);
|
ReflectionTestUtils.setField(service, "repository", repository);
|
||||||
|
|
||||||
var created = service.register(new RegisterRequest(
|
var created = service.register(new RegisterRequest(
|
||||||
"Example Merchant", "Example Merchant Ltd", "Bob", null, "Alice", "13800138000",
|
"Example Merchant", "Example Merchant Ltd", "Bob", "Alice", "13800138000",
|
||||||
"alice@example.com", "基础版", null, null, 30, null,
|
"alice@example.com", "基础版", null, null, 30, null,
|
||||||
null, null, "CNY", null, null), 9001L);
|
null, null, "CNY", null, null), 9001L);
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -13,6 +13,7 @@ import com.baiyue.warehouse.entity.ExpressCompanyEntity;
|
|||||||
import com.baiyue.warehouse.service.ExpressCompanyQueryService;
|
import com.baiyue.warehouse.service.ExpressCompanyQueryService;
|
||||||
import com.baiyue.warehouse.service.ExpressCompanyImportService;
|
import com.baiyue.warehouse.service.ExpressCompanyImportService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -42,7 +43,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
/** 平台快递公司管理接口。 */
|
/** 平台快递公司管理接口。 */
|
||||||
@Validated
|
@Validated
|
||||||
@Tag(name = "平台快递公司", description = "按目标租户分页查询快递公司主数据")
|
@Tag(name = "平台快递公司", description = "跨企业分页查询和按目标租户维护快递公司主数据")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/v1/express-companies")
|
@RequestMapping("/api/platform/v1/express-companies")
|
||||||
public class PlatformExpressCompanyController {
|
public class PlatformExpressCompanyController {
|
||||||
@@ -56,7 +57,7 @@ public class PlatformExpressCompanyController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:express:list')")
|
@PreAuthorize("hasAuthority('platform:express:list')")
|
||||||
@Operation(summary = "分页查询快递公司", description = "按目标租户、关键字和状态分页查询快递公司")
|
@Operation(summary = "分页查询快递公司", description = "默认查询全部企业;可按目标租户、关键字和状态筛选")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
|
||||||
@@ -65,7 +66,8 @@ public class PlatformExpressCompanyController {
|
|||||||
})
|
})
|
||||||
public ApiResponse<PageResult<ExpressCompanyEntity>> page(
|
public ApiResponse<PageResult<ExpressCompanyEntity>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam @Min(1) long tenantId,
|
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
|
||||||
|
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||||
@Valid @ParameterObject ExpressCompanyPageQuery query) {
|
@Valid @ParameterObject ExpressCompanyPageQuery query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(tenantId, query));
|
return ApiResponse.success(service.page(tenantId, query));
|
||||||
|
|||||||
+6
-3
@@ -13,6 +13,7 @@ import com.baiyue.warehouse.entity.PartnerEntity;
|
|||||||
import com.baiyue.warehouse.service.PlatformPartnerQueryService;
|
import com.baiyue.warehouse.service.PlatformPartnerQueryService;
|
||||||
import com.baiyue.warehouse.service.PartnerImportService;
|
import com.baiyue.warehouse.service.PartnerImportService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
@@ -42,7 +43,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
|
|
||||||
/** 平台往来单位管理接口。 */
|
/** 平台往来单位管理接口。 */
|
||||||
@Validated
|
@Validated
|
||||||
@Tag(name = "平台往来单位", description = "按目标租户分页查询往来单位主数据")
|
@Tag(name = "平台往来单位", description = "跨企业分页查询和按目标租户维护往来单位主数据")
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/v1/partners")
|
@RequestMapping("/api/platform/v1/partners")
|
||||||
public class PlatformPartnerController {
|
public class PlatformPartnerController {
|
||||||
@@ -83,7 +84,8 @@ public class PlatformPartnerController {
|
|||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('platform:counterparty:list')")
|
@PreAuthorize("hasAuthority('platform:counterparty:list')")
|
||||||
@Operation(summary = "分页查询往来单位", description = "按目标租户、关键字、类型和状态分页查询往来单位")
|
@Operation(summary = "分页查询往来单位",
|
||||||
|
description = "默认分页查询全部企业往来单位;可按目标租户、关键字、类型和状态筛选")
|
||||||
@ApiResponses({
|
@ApiResponses({
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "查询成功"),
|
||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "400", description = "查询条件无效"),
|
||||||
@@ -92,7 +94,8 @@ public class PlatformPartnerController {
|
|||||||
})
|
})
|
||||||
public ApiResponse<PageResult<PartnerEntity>> page(
|
public ApiResponse<PageResult<PartnerEntity>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam @Min(1) long tenantId,
|
@Parameter(description = "可选目标企业租户ID;不传时查询全部企业")
|
||||||
|
@RequestParam(required = false) @Min(1) Long tenantId,
|
||||||
@Valid @ParameterObject PartnerPageQuery query) {
|
@Valid @ParameterObject PartnerPageQuery query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(tenantId, query));
|
return ApiResponse.success(service.page(tenantId, query));
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.baiyue.warehouse.mapper;
|
package com.baiyue.warehouse.mapper;
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baiyue.warehouse.entity.PartnerEntity;
|
import com.baiyue.warehouse.entity.PartnerEntity;
|
||||||
import com.baiyue.warehouse.entity.PartnerTypeEntity;
|
import com.baiyue.warehouse.entity.PartnerTypeEntity;
|
||||||
import org.apache.ibatis.annotations.Delete;
|
import org.apache.ibatis.annotations.Delete;
|
||||||
@@ -25,6 +26,56 @@ public interface PartnerMapper extends BaseMapper<PartnerEntity> {
|
|||||||
@Select("SELECT partner_id FROM wh_partner_type WHERE tenant_id = #{tenantId} AND partner_type = #{type}")
|
@Select("SELECT partner_id FROM wh_partner_type WHERE tenant_id = #{tenantId} AND partner_type = #{type}")
|
||||||
List<Long> selectPartnerIdsByType(@Param("tenantId") long tenantId, @Param("type") String type);
|
List<Long> selectPartnerIdsByType(@Param("tenantId") long tenantId, @Param("type") String type);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
<script>
|
||||||
|
SELECT p.*
|
||||||
|
FROM wh_partner p
|
||||||
|
WHERE p.deleted = 0
|
||||||
|
<if test="tenantId != null">
|
||||||
|
AND p.tenant_id = #{tenantId}
|
||||||
|
</if>
|
||||||
|
<choose>
|
||||||
|
<when test="type == 'BOTH'">
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM wh_partner_type customer_type
|
||||||
|
WHERE customer_type.partner_id = p.id
|
||||||
|
AND customer_type.partner_type = 'CUSTOMER'
|
||||||
|
AND customer_type.tenant_id = p.tenant_id
|
||||||
|
)
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM wh_partner_type supplier_type
|
||||||
|
WHERE supplier_type.partner_id = p.id
|
||||||
|
AND supplier_type.partner_type = 'SUPPLIER'
|
||||||
|
AND supplier_type.tenant_id = p.tenant_id
|
||||||
|
)
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
AND EXISTS (
|
||||||
|
SELECT 1 FROM wh_partner_type pt
|
||||||
|
WHERE pt.partner_id = p.id
|
||||||
|
AND pt.partner_type = #{type}
|
||||||
|
AND pt.tenant_id = p.tenant_id
|
||||||
|
)
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
<if test="status != null and status != ''">
|
||||||
|
AND p.status = #{status}
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (p.partner_code LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR p.partner_name LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR p.contact_name LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR p.contact_phone LIKE CONCAT('%', #{keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
ORDER BY p.partner_name ASC, p.id ASC
|
||||||
|
</script>
|
||||||
|
""")
|
||||||
|
Page<PartnerEntity> selectPartnerPageByType(Page<PartnerEntity> page,
|
||||||
|
@Param("tenantId") Long tenantId,
|
||||||
|
@Param("type") String type,
|
||||||
|
@Param("status") String status,
|
||||||
|
@Param("keyword") String keyword);
|
||||||
|
|
||||||
@Delete("DELETE FROM wh_partner_type WHERE partner_id = #{partnerId}")
|
@Delete("DELETE FROM wh_partner_type WHERE partner_id = #{partnerId}")
|
||||||
void deleteTypes(@Param("partnerId") long partnerId);
|
void deleteTypes(@Param("partnerId") long partnerId);
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -9,7 +9,7 @@ import com.baiyue.warehouse.mapper.ExpressCompanyMapper;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/** 快递公司主数据仓储。 */
|
/** 快递公司主数据仓储;平台分页租户条件可选,资源读写仍强制租户边界。 */
|
||||||
@Repository
|
@Repository
|
||||||
public class ExpressCompanyRepository {
|
public class ExpressCompanyRepository {
|
||||||
private final ExpressCompanyMapper mapper;
|
private final ExpressCompanyMapper mapper;
|
||||||
@@ -18,9 +18,9 @@ public class ExpressCompanyRepository {
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<ExpressCompanyEntity> page(long tenantId, ExpressCompanyPageQuery query) {
|
public PageResult<ExpressCompanyEntity> page(Long tenantId, ExpressCompanyPageQuery query) {
|
||||||
LambdaQueryWrapper<ExpressCompanyEntity> wrapper = new LambdaQueryWrapper<ExpressCompanyEntity>()
|
LambdaQueryWrapper<ExpressCompanyEntity> wrapper = new LambdaQueryWrapper<ExpressCompanyEntity>()
|
||||||
.eq(ExpressCompanyEntity::getTenantId, tenantId)
|
.eq(tenantId != null, ExpressCompanyEntity::getTenantId, tenantId)
|
||||||
.eq(StringUtils.hasText(query.status()), ExpressCompanyEntity::getStatus, query.status())
|
.eq(StringUtils.hasText(query.status()), ExpressCompanyEntity::getStatus, query.status())
|
||||||
.and(StringUtils.hasText(query.keyword()), condition -> condition
|
.and(StringUtils.hasText(query.keyword()), condition -> condition
|
||||||
.like(ExpressCompanyEntity::getCompanyCode, query.keyword())
|
.like(ExpressCompanyEntity::getCompanyCode, query.keyword())
|
||||||
|
|||||||
@@ -41,14 +41,17 @@ public class PartnerRepository {
|
|||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<PartnerEntity> page(long tenantId, PartnerPageQuery request) {
|
public PageResult<PartnerEntity> page(Long tenantId, PartnerPageQuery request) {
|
||||||
List<Long> typedIds = typedIds(tenantId, request.type());
|
if (StringUtils.hasText(request.type())) {
|
||||||
if (StringUtils.hasText(request.type()) && typedIds.isEmpty()) {
|
Page<PartnerEntity> page = mapper.selectPartnerPageByType(
|
||||||
return new PageResult<>(List.of(), request.page(), request.size(), 0, 0);
|
new Page<>(request.page(), request.size()), tenantId, request.type(),
|
||||||
|
request.status(), request.keyword());
|
||||||
|
PageResult<PartnerEntity> result = PageResult.from(page);
|
||||||
|
hydrateTypes(result.records());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
LambdaQueryWrapper<PartnerEntity> wrapper = new LambdaQueryWrapper<PartnerEntity>()
|
LambdaQueryWrapper<PartnerEntity> wrapper = new LambdaQueryWrapper<PartnerEntity>()
|
||||||
.eq(PartnerEntity::getTenantId, tenantId)
|
.eq(tenantId != null, PartnerEntity::getTenantId, tenantId)
|
||||||
.in(StringUtils.hasText(request.type()), PartnerEntity::getId, typedIds)
|
|
||||||
.eq(StringUtils.hasText(request.status()), PartnerEntity::getStatus, request.status())
|
.eq(StringUtils.hasText(request.status()), PartnerEntity::getStatus, request.status())
|
||||||
.and(StringUtils.hasText(request.keyword()), query -> query
|
.and(StringUtils.hasText(request.keyword()), query -> query
|
||||||
.like(PartnerEntity::getPartnerCode, request.keyword())
|
.like(PartnerEntity::getPartnerCode, request.keyword())
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ public class ExpressCompanyQueryService {
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<ExpressCompanyEntity> page(long tenantId, ExpressCompanyPageQuery query) {
|
public PageResult<ExpressCompanyEntity> page(Long tenantId, ExpressCompanyPageQuery query) {
|
||||||
return repository.page(tenantId, query);
|
return repository.page(tenantId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@ public class PlatformPartnerQueryService {
|
|||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<PartnerEntity> page(long tenantId, PartnerPageQuery query) {
|
public PageResult<PartnerEntity> page(Long tenantId, PartnerPageQuery query) {
|
||||||
return repository.page(tenantId, query);
|
return repository.page(tenantId, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.baiyue.warehouse.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PlatformExpressCompanyAllTenantQueryContractTest {
|
||||||
|
@Test
|
||||||
|
void 分页租户参数可选但详情写入启停和导入仍然必填() {
|
||||||
|
Method page = method("page");
|
||||||
|
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||||
|
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
|
||||||
|
for (String scopedMethod : new String[] {"detail", "create", "update", "changeStatus", "importCompanies"}) {
|
||||||
|
Method method = method(scopedMethod);
|
||||||
|
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||||
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method method(String name) {
|
||||||
|
return Arrays.stream(PlatformExpressCompanyController.class.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals(name))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -75,7 +75,7 @@ class PlatformMasterDataControllerContractTest {
|
|||||||
|
|
||||||
Method page = controller.getDeclaredMethod("page",
|
Method page = controller.getDeclaredMethod("page",
|
||||||
com.baiyue.common.security.AuthenticatedUser.class,
|
com.baiyue.common.security.AuthenticatedUser.class,
|
||||||
long.class,
|
Long.class,
|
||||||
controller == PlatformPartnerController.class
|
controller == PlatformPartnerController.class
|
||||||
? com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery.class
|
? com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery.class
|
||||||
: com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyPageQuery.class);
|
: com.baiyue.warehouse.dto.MasterDataQueries.ExpressCompanyPageQuery.class);
|
||||||
|
|||||||
+64
@@ -0,0 +1,64 @@
|
|||||||
|
package com.baiyue.warehouse.controller;
|
||||||
|
|
||||||
|
import com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery;
|
||||||
|
import com.baiyue.warehouse.mapper.PartnerMapper;
|
||||||
|
import com.baiyue.warehouse.repository.PartnerRepository;
|
||||||
|
import com.baiyue.warehouse.service.PlatformPartnerQueryService;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PlatformPartnerAllTenantQueryContractTest {
|
||||||
|
@Test
|
||||||
|
void 平台分页租户可选但详情写入和导入仍要求租户() {
|
||||||
|
Method page = method(PlatformPartnerController.class, "page");
|
||||||
|
assertThat(page.getParameterTypes()[1]).isEqualTo(Long.class);
|
||||||
|
assertThat(page.getParameters()[1].getAnnotation(RequestParam.class).required()).isFalse();
|
||||||
|
|
||||||
|
for (String scopedMethod : new String[] {"importPartners", "detail", "create", "update", "changeStatus"}) {
|
||||||
|
Method method = method(PlatformPartnerController.class, scopedMethod);
|
||||||
|
assertThat(method.getParameterTypes()[1]).isEqualTo(long.class);
|
||||||
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).required()).isTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 分页链路使用可选租户而其他仓储方法保持原有强租户边界() throws Exception {
|
||||||
|
assertThat(PlatformPartnerQueryService.class.getMethod("page", Long.class, PartnerPageQuery.class))
|
||||||
|
.isNotNull();
|
||||||
|
assertThat(PartnerRepository.class.getMethod("page", Long.class, PartnerPageQuery.class))
|
||||||
|
.isNotNull();
|
||||||
|
assertThat(PartnerRepository.class.getMethod("findOwned", long.class, long.class))
|
||||||
|
.isNotNull();
|
||||||
|
assertThat(PartnerRepository.class.getMethod("existsCode", long.class, String.class, Long.class))
|
||||||
|
.isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 类型筛选在数据库内关联分页且租户条件可选() throws Exception {
|
||||||
|
Method method = PartnerMapper.class.getMethod("selectPartnerPageByType",
|
||||||
|
Page.class, Long.class, String.class, String.class, String.class);
|
||||||
|
String sql = String.join("\n", method.getAnnotation(Select.class).value());
|
||||||
|
|
||||||
|
assertThat(sql)
|
||||||
|
.contains("AND EXISTS (")
|
||||||
|
.contains("pt.partner_id = p.id")
|
||||||
|
.contains("pt.partner_type = #{type}")
|
||||||
|
.contains("pt.tenant_id = p.tenant_id")
|
||||||
|
.contains("<if test=\"tenantId != null\">")
|
||||||
|
.contains("AND p.tenant_id = #{tenantId}")
|
||||||
|
.doesNotContain("partner_id IN");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Method method(Class<?> type, String name) {
|
||||||
|
return java.util.Arrays.stream(type.getDeclaredMethods())
|
||||||
|
.filter(candidate -> candidate.getName().equals(name))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.baiyue.warehouse.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class PartnerMapperContractTest {
|
||||||
|
@Test
|
||||||
|
void bothTypeRequiresCustomerAndSupplierAssociations() throws Exception {
|
||||||
|
Select select = PartnerMapper.class.getDeclaredMethod("selectPartnerPageByType",
|
||||||
|
Page.class, Long.class, String.class, String.class, String.class)
|
||||||
|
.getAnnotation(Select.class);
|
||||||
|
String sql = String.join("\n", select.value());
|
||||||
|
|
||||||
|
assertThat(sql)
|
||||||
|
.contains("<when test=\"type == 'BOTH'\">")
|
||||||
|
.contains("customer_type.partner_type = 'CUSTOMER'")
|
||||||
|
.contains("supplier_type.partner_type = 'SUPPLIER'")
|
||||||
|
.contains("<otherwise>")
|
||||||
|
.contains("pt.partner_type = #{type}");
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-1
@@ -25,7 +25,7 @@ class ExpressCompanyRepositoryTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void 平台分页强制租户并支持筛选() {
|
void 平台分页传入租户时按租户并支持筛选() {
|
||||||
ExpressCompanyMapper mapper = mock(ExpressCompanyMapper.class);
|
ExpressCompanyMapper mapper = mock(ExpressCompanyMapper.class);
|
||||||
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
@@ -40,4 +40,22 @@ class ExpressCompanyRepositoryTest {
|
|||||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||||
.contains(10001L, "ACTIVE", "%顺丰%");
|
.contains(10001L, "ACTIVE", "%顺丰%");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void 平台分页不传租户时查询全部企业并保留其他筛选() {
|
||||||
|
ExpressCompanyMapper mapper = mock(ExpressCompanyMapper.class);
|
||||||
|
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
new ExpressCompanyRepository(mapper).page(null,
|
||||||
|
new ExpressCompanyPageQuery(1, 20, "顺丰", "ACTIVE"));
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
ArgumentCaptor<LambdaQueryWrapper<ExpressCompanyEntity>> wrapper =
|
||||||
|
ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
||||||
|
verify(mapper).selectPage(any(Page.class), wrapper.capture());
|
||||||
|
wrapper.getValue().getSqlSegment();
|
||||||
|
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
||||||
|
.contains("ACTIVE", "%顺丰%")
|
||||||
|
.doesNotContain(10001L);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,17 +56,27 @@ class PartnerRepositoryTest {
|
|||||||
|
|
||||||
@Test void 平台分页强制租户并支持筛选() {
|
@Test void 平台分页强制租户并支持筛选() {
|
||||||
PartnerMapper mapper=mock(PartnerMapper.class);
|
PartnerMapper mapper=mock(PartnerMapper.class);
|
||||||
when(mapper.selectPartnerIdsByType(10001L, "CUSTOMER")).thenReturn(List.of(9L));
|
when(mapper.selectPartnerPageByType(any(Page.class), eq(10001L), eq("CUSTOMER"),
|
||||||
when(mapper.selectPage(any(Page.class), any())).thenAnswer(invocation -> invocation.getArgument(0));
|
eq("ACTIVE"), eq("示例"))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
new PartnerRepository(mapper).page(10001L,
|
new PartnerRepository(mapper).page(10001L,
|
||||||
new PartnerPageQuery(1, 20, "示例", "CUSTOMER", "ACTIVE"));
|
new PartnerPageQuery(1, 20, "示例", "CUSTOMER", "ACTIVE"));
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
verify(mapper).selectPartnerPageByType(any(Page.class), eq(10001L), eq("CUSTOMER"),
|
||||||
ArgumentCaptor<LambdaQueryWrapper<PartnerEntity>> wrapper=ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
eq("ACTIVE"), eq("示例"));
|
||||||
verify(mapper).selectPage(any(Page.class), wrapper.capture());
|
verify(mapper, never()).selectPartnerIdsByType(anyLong(), anyString());
|
||||||
wrapper.getValue().getSqlSegment();
|
}
|
||||||
assertThat(wrapper.getValue().getParamNameValuePairs().values())
|
|
||||||
.contains(10001L, 9L, "ACTIVE", "%示例%");
|
@Test void 平台分页不传租户时查询全部企业并支持承运商类型筛选() {
|
||||||
|
PartnerMapper mapper=mock(PartnerMapper.class);
|
||||||
|
when(mapper.selectPartnerPageByType(any(Page.class), isNull(), eq("CARRIER"),
|
||||||
|
eq("ACTIVE"), isNull())).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
new PartnerRepository(mapper).page(null,
|
||||||
|
new PartnerPageQuery(1, 20, null, "CARRIER", "ACTIVE"));
|
||||||
|
|
||||||
|
verify(mapper).selectPartnerPageByType(any(Page.class), isNull(), eq("CARRIER"),
|
||||||
|
eq("ACTIVE"), isNull());
|
||||||
|
verify(mapper, never()).selectPartnerIdsByType(anyLong(), anyString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user