feat(warehouse): support supplier carrier associations

This commit is contained in:
2026-08-27 10:29:28 +08:00
parent 554b6c93b7
commit 079ff43f73
17 changed files with 562 additions and 19 deletions
@@ -17,5 +17,9 @@ class InternalSupplierControllerContractTest {
.findFirst().orElseThrow();
assertThat(Arrays.stream(get.getParameters()).map(parameter -> parameter.getName()))
.containsExactly("token", "supplierId", "requireActive", "category");
Method candidates = Arrays.stream(InternalSupplierController.class.getDeclaredMethods())
.filter(method -> method.getName().equals("candidates")).findFirst().orElseThrow();
assertThat(Arrays.stream(candidates.getParameters()).map(parameter -> parameter.getName()))
.containsExactly("token", "category", "carrierCode");
}
}
@@ -20,6 +20,9 @@ class PlatformSupplierControllerContractTest {
assertThat(method("create").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:create");
assertThat(method("update").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:edit");
assertThat(method("status").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:status");
assertThat(method("importTemplate").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:create");
assertThat(method("importSuppliers").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:create");
assertThat(method("export").getAnnotation(PreAuthorize.class).value()).contains("platform:supplier:list");
}
private static Method method(String n) { return java.util.Arrays.stream(PlatformSupplierController.class.getDeclaredMethods()).filter(m -> m.getName().equals(n)).findFirst().orElseThrow(); }
}
@@ -0,0 +1,33 @@
package com.baiyue.warehouse.controller;
import com.baiyue.warehouse.dto.SupplierDtos.SaveRequest;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class SupplierCarrierContractTest {
@Test void carrierCodesMayBeOmittedAndSingleCarrierCodeIsRemoved() {
var component = Arrays.stream(SaveRequest.class.getRecordComponents())
.filter(value -> value.getName().equals("carrierCodes")).findFirst().orElseThrow();
assertThat(component.getAnnotation(NotNull.class)).isNull();
assertThat(Arrays.stream(SaveRequest.class.getRecordComponents()).map(java.lang.reflect.RecordComponent::getName))
.contains("carrierCodes").doesNotContain("carrierCode");
}
@Test void openApiFreezesPageCandidatesResourceImportAndExport() throws Exception {
String yaml = Files.readString(Path.of("src/main/resources/openapi/supplier-v1.yaml"), StandardCharsets.UTF_8);
assertThat(yaml).contains("name: carrierCode", "按船司编码筛选时强制只返回ACTIVE且BOOKING",
"carrierCodes: { $ref: '#/components/schemas/CarrierCodes' }",
"/internal/warehouse/v1/suppliers:", "/api/platform/v1/suppliers/import-template:",
"/api/platform/v1/suppliers/import:", "/api/platform/v1/suppliers/export:",
"权限 platform:supplier:list");
String required = yaml.substring(yaml.indexOf("SupplierSave:"), yaml.indexOf("Supplier:", yaml.indexOf("SupplierSave:")));
assertThat(required).doesNotContain("required: [name, category, carrierCodes");
}
}
@@ -18,14 +18,19 @@ class SupplierSqlContractTest {
"MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE",
"created_by BIGINT UNSIGNED", "updated_by BIGINT UNSIGNED",
"supplier_add_column_if_missing", "uk_wh_supplier_name", "idx_wh_supplier_filter",
"assert_supplier_schema");
"CREATE TABLE IF NOT EXISTS wh_supplier_carrier", "PRIMARY KEY (supplier_id,carrier_code)",
"idx_wh_supplier_carrier_code", "chk_wh_supplier_carrier_code", "assert_supplier_schema");
String supplierTable = sql.substring(sql.indexOf("CREATE TABLE IF NOT EXISTS wh_supplier ("),
sql.indexOf("CREATE TABLE IF NOT EXISTS wh_supplier_carrier"));
assertThat(supplierTable).doesNotContain("carrier_code", "carrier_codes");
assertThat(sql).doesNotContain("scope VARCHAR", "chk_wh_supplier_scope", "DOMESTIC/INTERNATIONAL");
}
}
@Test void unifiedMigrationContainsSupplierSchemaAndPermissionContract() throws IOException {
String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8);
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "uk_wh_supplier_name",
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "CREATE TABLE IF NOT EXISTS wh_supplier_carrier",
"PRIMARY KEY(supplier_id,carrier_code)", "idx_wh_supplier_carrier_code", "uk_wh_supplier_name",
"idx_wh_supplier_filter", "platform:supplier:list", "platform:supplier:create",
"platform:supplier:edit", "platform:supplier:status", "assert_platform_supplier_permissions");
String supplierSection = sql.substring(sql.indexOf("-- 平台公共供应商业务档案及权限"));
@@ -0,0 +1,45 @@
package com.baiyue.warehouse.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.warehouse.dto.SupplierDtos.PageQuery;
import com.baiyue.warehouse.dto.SupplierDtos.SupplierResource;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class SupplierImportExportServiceTest {
@Mock private SupplierService supplierService;
@InjectMocks private SupplierImportExportService service;
private PageQuery query;
@BeforeEach void setUp() { query = new PageQuery(1, 20, "BOOKING", null, "ACTIVE", null, "MATSON"); }
@Test void templateUsesMultiCarrierCodesColumn() throws Exception {
try (XSSFWorkbook workbook = new XSSFWorkbook(new ByteArrayInputStream(service.template()))) {
assertThat(workbook.getSheetAt(0).getRow(0).getCell(1).getStringCellValue()).isEqualTo("2");
assertThat(workbook.getSheetAt(0).getRow(2).getCell(2).getStringCellValue()).isEqualTo("船司标准编码(多值)");
}
}
@Test void csvExportsCarrierCodesWithoutChangingPersistenceShape() {
SupplierResource resource = new SupplierResource(7L, 0L, "订舱代理", "BOOKING", List.of("MATSON", "ZIM"),
"宁波", "法人", "联系人", "13800000000", null, null, null, null, "ACTIVE", "MONTHLY_30", null,
null, null, null, null, false, 0);
when(supplierService.page(new PageQuery(1, 200, "BOOKING", null, "ACTIVE", null, "MATSON")))
.thenReturn(new PageResult<>(List.of(resource), 1, 200, 1, 1));
String csv = new String(service.exportCsv(query), StandardCharsets.UTF_8);
assertThat(csv).contains("船司标准编码", "\"MATSON|ZIM\"");
}
}
@@ -13,6 +13,8 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
@@ -25,7 +27,7 @@ class SupplierServiceTest {
private SaveRequest booking;
@BeforeEach void setUp() {
booking = new SaveRequest("测试供应商", "BOOKING", "宁波", "法人", "联系人",
booking = new SaveRequest("测试供应商", "BOOKING", java.util.List.of("MATSON"), "宁波", "法人", "联系人",
"13800000000", "supplier@example.com", "地址", "91330000TEST", "192000000000000001",
"ACTIVE", "MONTHLY_30", null);
}
@@ -34,6 +36,34 @@ class SupplierServiceTest {
when(repository.existsName(0L, "测试供应商", null)).thenReturn(false);
service.create(101L, booking);
verify(repository).insert(any(SupplierEntity.class));
verify(repository).replaceCarrierCodes(eq(0L), eq(101L), eq(java.util.List.of("MATSON")), any());
}
@Test void deduplicatesBookingCarrierCodesAndRejectsNonBookingAssociations() {
SaveRequest duplicated = new SaveRequest("测试供应商", "BOOKING", java.util.List.of("MATSON", "MATSON", "ZIM"),
"宁波", "法人", "联系人", "13800000000", null, null, null, null,
"ACTIVE", "MONTHLY_30", null);
service.create(102L, duplicated);
verify(repository).replaceCarrierCodes(eq(0L), eq(102L), eq(java.util.List.of("MATSON", "ZIM")), any());
SaveRequest customs = new SaveRequest("报关行", "CUSTOMS", java.util.List.of("MATSON"), "宁波", "法人",
"联系人", "13800000000", null, null, null, null, "ACTIVE", "MONTHLY_30", null);
assertThatThrownBy(() -> service.create(103L, customs)).isInstanceOf(BizException.class)
.hasMessageContaining("非BOOKING");
}
@Test void bookingRequiresAtLeastOneCarrierCode() {
SaveRequest empty = new SaveRequest("测试供应商", "BOOKING", java.util.List.of(), "宁波", "法人", "联系人",
"13800000000", null, null, null, null, "ACTIVE", "MONTHLY_30", null);
assertThatThrownBy(() -> service.create(104L, empty)).isInstanceOf(BizException.class)
.hasMessageContaining("至少关联一个");
}
@Test void nonBookingMayOmitCarrierCodes() {
SaveRequest customs = new SaveRequest("报关行", "CUSTOMS", null, "宁波", "法人", "联系人",
"13800000000", null, null, null, null, "ACTIVE", "MONTHLY_30", null);
service.create(105L, customs);
verify(repository).replaceCarrierCodes(eq(0L), eq(105L), eq(java.util.List.of()), any());
}
@Test void repeatedStatusChangeIsIdempotent() {
@@ -46,11 +76,22 @@ class SupplierServiceTest {
@Test void validatesInternalSupplierSnapshot() {
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L); entity.setName("测试供应商");
entity.setCategory("BOOKING"); entity.setStatus("ACTIVE");
when(repository.carrierCodes(0L, java.util.List.of(101L))).thenReturn(java.util.Map.of(101L, java.util.List.of("MATSON")));
when(repository.find(0L, 101L)).thenReturn(entity);
var snapshot = service.internalSnapshot(101L, true, "BOOKING");
org.assertj.core.api.Assertions.assertThat(snapshot.supplierId()).isEqualTo(101L);
org.assertj.core.api.Assertions.assertThat(snapshot.category()).isEqualTo("BOOKING");
org.assertj.core.api.Assertions.assertThat(snapshot.status()).isEqualTo("ACTIVE");
org.assertj.core.api.Assertions.assertThat(snapshot.carrierCodes()).containsExactly("MATSON");
}
@Test void detailReturnsCarrierCodesAsResourceArray() {
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L);
entity.setName("测试供应商"); entity.setCategory("BOOKING"); entity.setStatus("ACTIVE");
when(repository.find(0L, 101L)).thenReturn(entity);
when(repository.carrierCodes(0L, java.util.List.of(101L)))
.thenReturn(java.util.Map.of(101L, java.util.List.of("MATSON", "ZIM")));
assertThat(service.detail(101L).carrierCodes()).containsExactly("MATSON", "ZIM");
}
@Test void internalSnapshotRequiresActiveSupplierAndMatchingCategory() {