This commit is contained in:
2026-08-15 11:05:58 +08:00
parent c4282020b5
commit 557ab75319
50 changed files with 591 additions and 394 deletions
@@ -24,13 +24,13 @@ public final class MasterDataQueries {
@Schema(description = "编码、名称、联系人或联系电话关键字", example = "示例客户")
@Size(max = 128) String keyword,
@Schema(description = "往来单位类型", allowableValues = {
"CUSTOMER", "SUPPLIER", "BOTH", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
@Pattern(regexp = "^(CUSTOMER|SUPPLIER|BOTH|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String type,
"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
@Pattern(regexp = "^(CUSTOMER|CARRIER|SHIPPER|CONSIGNEE)$") String type,
@Schema(description = "状态", allowableValues = {"ACTIVE", "INACTIVE"})
@Pattern(regexp = "^(ACTIVE|INACTIVE)$") String status,
@Schema(description = "往来单位类型集合(可重复传参)", allowableValues = {
"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
@Size(max = 6) List<@Pattern(regexp = "^(CUSTOMER|SUPPLIER|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String> types
"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
@Size(max = 4) List<@Pattern(regexp = "^(CUSTOMER|CARRIER|SHIPPER|CONSIGNEE)$") String> types
) {
public PartnerPageQuery {
if (page == 0) {
@@ -69,8 +69,8 @@ public final class MasterDataQueries {
@NotBlank @Size(max = 50) String partnerCode,
@Schema(description = "往来单位名称", example = "深圳示例贸易有限公司")
@NotBlank @Size(max = 128) String partnerName,
@Schema(description = "兼容旧客户端的单值类型", allowableValues = {"CUSTOMER", "SUPPLIER", "BOTH", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
@Pattern(regexp = "^(CUSTOMER|SUPPLIER|BOTH|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String partnerType,
@Schema(description = "单值单位类型", allowableValues = {"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
@Pattern(regexp = "^(CUSTOMER|CARRIER|SHIPPER|CONSIGNEE)$") String partnerType,
@Schema(description = "联系人", example = "张三")
@NotBlank @Size(max = 64) String contactName,
@Schema(description = "联系电话", example = "13800138000")
@@ -91,8 +91,8 @@ public final class MasterDataQueries {
@Size(max = 20) String postalCode,
@Schema(description = "状态", allowableValues = {"ACTIVE", "INACTIVE"})
@NotBlank @Pattern(regexp = "^(ACTIVE|INACTIVE)$") String status,
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
@Size(max = 6) List<@Pattern(regexp = "^(CUSTOMER|SUPPLIER|CARRIER|CUSTOMS_BROKER|SHIPPER|CONSIGNEE)$") String> types,
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
@Size(max = 4) List<@Pattern(regexp = "^(CUSTOMER|CARRIER|SHIPPER|CONSIGNEE)$") String> types,
@Schema(description = "备注") @Size(max = 500) String remark
) {
}
@@ -26,10 +26,10 @@ public class PartnerEntity extends BaseEntity {
private String partnerCode;
@Schema(description = "往来单位名称", example = "深圳示例贸易有限公司")
private String partnerName;
@Schema(description = "类型", example = "CUSTOMER", allowableValues = {"CUSTOMER", "SUPPLIER", "BOTH"})
@Schema(description = "类型", example = "CUSTOMER", allowableValues = {"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
private String partnerType;
@TableField(exist = false)
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE"})
@Schema(description = "单位类型集合", allowableValues = {"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE"})
private List<String> types;
@Schema(description = "联系人")
private String contactName;
@@ -37,30 +37,12 @@ public interface PartnerMapper extends BaseMapper<PartnerEntity> {
<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>
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
)
<if test="status != null and status != ''">
AND p.status = #{status}
</if>
@@ -141,7 +141,6 @@ public class PartnerRepository {
private record PartnerKey(Long tenantId, Long partnerId) { }
private static List<String> legacyTypes(String type) {
if ("BOTH".equals(type)) return List.of("CUSTOMER", "SUPPLIER");
return StringUtils.hasText(type) ? List.of(type) : List.of();
}
}
@@ -34,7 +34,7 @@ public class PartnerImportService {
private static final int MAX_ROWS = 1000;
private static final String TEMPLATE_VERSION = "1";
private static final Set<String> ALLOWED_TYPES = Set.of(
"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE");
"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE");
private static final Pattern EMAIL = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
private static final String[] HEADERS = {
"单位编码*", "单位名称*", "单位类型*", "联系人*", "联系电话*", "联系邮箱",
@@ -53,7 +53,7 @@ public class PartnerImportService {
Row instruction = sheet.createRow(1);
instruction.createCell(0).setCellValue("说明");
instruction.createCell(1).setCellValue(
"仅新增;类型可用中文逗号或英文逗号分隔:CUSTOMER、SUPPLIER、CARRIER、CUSTOMS_BROKER、SHIPPER、CONSIGNEE;状态为 ACTIVE 或 INACTIVE");
"仅新增;类型可用中文逗号或英文逗号分隔:CUSTOMER、CARRIER、SHIPPER、CONSIGNEE;供应商与报关代理请在供应商中心维护;状态为 ACTIVE 或 INACTIVE");
Row header = sheet.createRow(2);
for (int i = 0; i < HEADERS.length; i++) {
header.createCell(i).setCellValue(HEADERS[i]);
@@ -162,7 +162,7 @@ public class PartnerImportService {
entity.setPartnerCode(row.code);
entity.setPartnerName(row.name);
entity.setTypes(row.types);
entity.setPartnerType(row.types.contains("CUSTOMER") && row.types.contains("SUPPLIER") ? "BOTH" : row.types.get(0));
entity.setPartnerType(row.types.get(0));
entity.setContactName(row.contactName);
entity.setContactPhone(row.contactPhone);
entity.setContactEmail(nullable(row.email));
@@ -23,7 +23,7 @@ import java.util.Locale;
public class PlatformPartnerQueryService {
private static final long PUBLIC_TENANT_ID = 0L;
private static final Set<String> ALLOWED_QUERY_TYPES = Set.of(
"CUSTOMER", "SUPPLIER", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE", "BOTH");
"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE");
@Resource
private PartnerRepository repository;
@@ -143,18 +143,17 @@ public class PlatformPartnerQueryService {
if (requested != null) requested.forEach(type -> {
if (type != null && !type.isBlank()) normalized.add(type);
});
if (normalized.isEmpty() && "BOTH".equals(legacyType)) {
normalized.add("CUSTOMER");
normalized.add("SUPPLIER");
} else if (normalized.isEmpty() && legacyType != null && !legacyType.isBlank()) {
if (normalized.isEmpty() && legacyType != null && !legacyType.isBlank()) {
normalized.add(legacyType);
}
if (normalized.isEmpty()) throw new BizException(ResultCode.BAD_REQUEST, "请至少选择一个单位类型");
if (normalized.stream().anyMatch(type -> !ALLOWED_QUERY_TYPES.contains(type))) {
throw new BizException(ResultCode.VALIDATION_FAILED, "往来单位仅支持 CUSTOMER、CARRIER、SHIPPER、CONSIGNEE");
}
return List.copyOf(normalized);
}
private static String legacyType(List<String> types) {
if (types.contains("CUSTOMER") && types.contains("SUPPLIER")) return "BOTH";
return types.get(0);
}
}
@@ -4,7 +4,7 @@ CREATE TABLE IF NOT EXISTS wh_partner (
id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID(雪花ID)', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0',
data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围',
partner_code VARCHAR(50) NOT NULL COMMENT '平台内唯一往来单位编码', partner_name VARCHAR(128) NOT NULL COMMENT '往来单位名称',
partner_type VARCHAR(20) NOT NULL COMMENT 'CUSTOMER、SUPPLIER或BOTH',
partner_type VARCHAR(20) NOT NULL COMMENT '往来单位类型:CUSTOMER、CARRIER、SHIPPER或CONSIGNEE',
contact_name VARCHAR(64) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(32) NOT NULL COMMENT '联系电话',
contact_email VARCHAR(128) NULL COMMENT '联系邮箱', country_code CHAR(2) NOT NULL COMMENT '国家二字码',
province VARCHAR(64) NULL COMMENT '省或州', city VARCHAR(64) NOT NULL COMMENT '城市', district VARCHAR(64) NULL COMMENT '区县',
@@ -15,13 +15,15 @@ CREATE TABLE IF NOT EXISTS wh_partner (
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记:0未删除、1已删除', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY(id), UNIQUE KEY uk_wh_partner_tenant_code(tenant_id,partner_code,deleted),
KEY idx_wh_partner_tenant_status(tenant_id,status,deleted)
KEY idx_wh_partner_tenant_status(tenant_id,status,deleted),
CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CUSTOMER','CARRIER','SHIPPER','CONSIGNEE'))
) ENGINE=InnoDB COMMENT='平台统一维护、供全部客户共享的往来单位主数据';
CREATE TABLE IF NOT EXISTS wh_partner_type (
partner_id BIGINT UNSIGNED NOT NULL COMMENT '往来单位ID', tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共往来单位固定使用 tenant_id=0',
data_scope_id BIGINT UNSIGNED NOT NULL COMMENT '所属业务数据范围,平台公共数据为0', partner_type VARCHAR(32) NOT NULL COMMENT '稳定业务类型:CUSTOMER客户、SHIPPER发货方等',
PRIMARY KEY(partner_id,partner_type),
KEY idx_wh_partner_type_scope(tenant_id,data_scope_id,partner_type,partner_id)
KEY idx_wh_partner_type_scope(tenant_id,data_scope_id,partner_type,partner_id),
CONSTRAINT chk_wh_partner_type_value CHECK (partner_type IN ('CUSTOMER','CARRIER','SHIPPER','CONSIGNEE'))
) ENGINE=InnoDB COMMENT='平台共享往来单位稳定业务类型';
INSERT INTO wh_partner(id,tenant_id,data_scope_id,partner_code,partner_name,partner_type,contact_name,contact_phone,contact_email,country_code,province,city,district,address_line,postal_code,status)
VALUES(191000000000000001,0,0,'CUS-001','深圳示例贸易有限公司','CUSTOMER','张三','13800138000','contact@example.com','CN','广东省','深圳市','宝安区','航城街道示例路1号','518000','ACTIVE')
@@ -25,12 +25,18 @@ class PlatformMasterDataControllerContractTest {
try (var factory = Validation.buildDefaultValidatorFactory()) {
var validator = factory.getValidator();
for (String type : java.util.List.of(
"CUSTOMER", "SUPPLIER", "BOTH", "CARRIER", "CUSTOMS_BROKER", "SHIPPER", "CONSIGNEE")) {
"CUSTOMER", "CARRIER", "SHIPPER", "CONSIGNEE")) {
var query = new com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery(
1, 20, null, type, null, null);
assertThat(validator.validate(query)).isEmpty();
}
for (String removed : java.util.List.of("SUPPLIER", "CUSTOMS_BROKER", "BOTH")) {
var query = new com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery(
1, 20, null, removed, null, null);
assertThat(validator.validate(query)).isNotEmpty();
}
var invalid = new com.baiyue.warehouse.dto.MasterDataQueries.PartnerPageQuery(
1, 20, null, "UNKNOWN", null, null);
assertThat(validator.validate(invalid))
@@ -8,18 +8,14 @@ import static org.assertj.core.api.Assertions.assertThat;
class PartnerMapperContractTest {
@Test
void bothTypeRequiresCustomerAndSupplierAssociations() throws Exception {
void typeFilterUsesOnlyStablePartnerTypeAssociation() 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}");
assertThat(sql).contains("pt.partner_type = #{type}")
.doesNotContain("type == 'BOTH'", "supplier_type.partner_type = 'SUPPLIER'");
}
@Test
@@ -89,7 +89,7 @@ class PartnerRepositoryTest {
PartnerEntity customerA = new PartnerEntity();
customerA.setId(7L); customerA.setTenantId(10001L); customerA.setPartnerType("CUSTOMER");
PartnerEntity customerB = new PartnerEntity();
customerB.setId(7L); customerB.setTenantId(10002L); customerB.setPartnerType("SUPPLIER");
customerB.setId(7L); customerB.setTenantId(10002L); customerB.setPartnerType("SHIPPER");
Page<PartnerEntity> source = new Page<>(1, 20);
source.setRecords(List.of(customerA, customerB));
source.setTotal(2);
@@ -98,14 +98,14 @@ class PartnerRepositoryTest {
PartnerTypeEntity typeA = new PartnerTypeEntity();
typeA.setTenantId(10001L); typeA.setPartnerId(7L); typeA.setPartnerType("CUSTOMER");
PartnerTypeEntity typeB = new PartnerTypeEntity();
typeB.setTenantId(10002L); typeB.setPartnerId(7L); typeB.setPartnerType("SUPPLIER");
typeB.setTenantId(10002L); typeB.setPartnerId(7L); typeB.setPartnerType("SHIPPER");
when(mapper.selectTypes(List.of(10001L, 10002L), List.of(7L, 7L)))
.thenReturn(List.of(typeA, typeB));
repository.page(null, new PartnerPageQuery(1, 20, null, null, null, null));
assertThat(customerA.getTypes()).containsExactly("CUSTOMER");
assertThat(customerB.getTypes()).containsExactly("SUPPLIER");
assertThat(customerB.getTypes()).containsExactly("SHIPPER");
}
@Test