Merge remote-tracking branch 'origin/dev' into test

# Conflicts:
#	warehouse/src/main/java/com/baiyue/warehouse/dto/WarehouseRequests.java
#	warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseImportService.java
#	warehouse/src/main/java/com/baiyue/warehouse/service/WarehouseSaveNormalizer.java
#	warehouse/src/main/resources/db/warehouse.sql
This commit is contained in:
2026-08-26 20:51:27 +08:00
1157 changed files with 78457 additions and 2736 deletions
+5 -1
View File
@@ -2,8 +2,9 @@
## 职责与边界
- 负责国内收货仓、地址/能力、往来单位、快递公司、平台公共供应商、国内整柜库存/出入库,以及托盘主数据、库位预占和占用。
- 负责国内收货仓及现有平台仓库主数据、地址/能力、往来单位、快递公司、平台公共供应商、国内整柜库存/出入库,以及托盘主数据、库位预占和占用。四期只在本模块扩展“海外仓合作仓”仓库类型及其公共主数据契约。
- 入库单、测量和入库状态归 `firstleg/`;订单只通过内部接口校验并保存快照。
- 四期海外接柜、卸货、海外入库、换标、产品、箱号、库存和库内作业属于海外区域服务,不得落入本国内 Warehouse Service;范围先读 `../../docs/四期/README.md`。
- 公共资源统一 `tenant_id=0`,不得绑定客户或要求客户筛选。
## 模块入口
@@ -11,6 +12,8 @@
| 目标 | 入口 |
|---|---|
| 仓库/地址/能力 | `controller/PlatformWarehouseController.java`、`MerchantWarehouseController.java`、`service/WarehouseApplicationService.java` |
| 公共海外收货仓内部目录 | `controller/InternalWarehouseController.java` 的 `/internal/warehouses/shared-overseas`、`/shared-overseas/{warehouseId}`,响应 `dto/SharedOverseasWarehouseResponse.java`;只返回平台公共 `OVERSEAS + ACTIVE + RECEIVING` 仓,供海外资源服务按 warehouseId 引用 |
| 四期海外仓合作仓类型 | 沿用仓库/地址/能力入口;同步核对仓库 DTO、OpenAPI、`src/main/resources/db/`、统一 SQL 及 Admin `modules/warehouses/`,并与普通海外仓语义明确区分 |
| 往来单位/快递 | `controller/PlatformPartnerController.java`、`PlatformExpressCompanyController.java`、`service/PlatformPartnerQueryService.java`、`PartnerImportService.java` |
| 托盘 | `controller/PlatformPalletController.java`、`service/PalletApplicationService.java` |
| 库位与分配 | `controller/PlatformLocationController.java`、`InternalPalletLocationController.java`、`service/PalletLocationAllocationService.java` |
@@ -24,6 +27,7 @@
## 修改约束
- 内部仓储接口使用 `X-Internal-Service-Token`,不加入 Gateway 公网路由;资源停用/不存在遵循统一 body code。
- 仓库 ID、类型、状态、地址和服务能力在本模块唯一维护;海外服务不得复制仓库主档。共享海外收货仓目录只接受内部服务令牌,列表和单项必须使用相同的 `tenant_id=0 + OVERSEAS + ACTIVE + RECEIVING` 判定。
- 导入先全量校验再原子写入;调整 DTO、类型或容量规则时同步 Order/Firstleg 客户端和契约测试。
## 三期托盘、库位与 A0 规则
@@ -4,6 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
import com.baiyue.warehouse.dto.SharedOverseasWarehouseResponse;
import com.baiyue.warehouse.service.WarehouseApplicationService;
import jakarta.annotation.Resource;
import jakarta.annotation.PostConstruct;
@@ -11,6 +13,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
@RestController
@RequestMapping("/internal/warehouses")
@@ -39,6 +42,28 @@ public class InternalWarehouseController {
return ApiResponse.success(service.activeDetail(PLATFORM_TENANT_ID, id));
}
@GetMapping("/overseas-partners")
public ApiResponse<List<OverseasPartnerWarehouseResponse>> overseasPartners(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token) {
verifyToken(token);
return ApiResponse.success(service.activeOverseasPartners());
}
@GetMapping("/shared-overseas")
public ApiResponse<List<SharedOverseasWarehouseResponse>> sharedOverseasWarehouses(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token) {
verifyToken(token);
return ApiResponse.success(service.activeSharedOverseasWarehouses());
}
@GetMapping("/shared-overseas/{warehouseId}")
public ApiResponse<SharedOverseasWarehouseResponse> sharedOverseasWarehouse(
@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
@PathVariable long warehouseId) {
verifyToken(token);
return ApiResponse.success(service.activeSharedOverseasWarehouse(warehouseId));
}
private void verifyToken(String token) {
byte[] supplied = token == null ? new byte[0] : token.getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expected, supplied)) {
@@ -71,6 +71,9 @@ public final class MasterDataQueries {
@NotBlank @Size(max = 128) String partnerName,
@Schema(description = "单值单位类型", allowableValues = {"CARRIER", "SHIPPER", "CONSIGNEE"})
@Pattern(regexp = "^(CARRIER|SHIPPER|CONSIGNEE)$") String partnerType,
@Schema(description = "船司标准编码;包含 CARRIER 类型时必填,其他类型必须为空",
allowableValues = {"MATSON", "SML", "ZIM", "COSCO", "EMC", "HEDE", "OTHER"})
@Pattern(regexp = "^(MATSON|SML|ZIM|COSCO|EMC|HEDE|OTHER)$") String carrierCode,
@Schema(description = "联系人", example = "张三")
@NotBlank @Size(max = 64) String contactName,
@Schema(description = "联系电话", example = "13800138000")
@@ -0,0 +1,17 @@
package com.baiyue.warehouse.dto;
import com.baiyue.warehouse.entity.WarehouseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "OverseasPartnerWarehouseResponse", description = "Active overseas partner warehouse option / 启用的海外合作仓候选")
public record OverseasPartnerWarehouseResponse(
@Schema(description = "Platform public warehouse ID / 平台公共仓库 ID") Long id,
@Schema(description = "Warehouse code / 仓库编码") String warehouseCode,
@Schema(description = "Warehouse name / 仓库名称") String warehouseName,
@Schema(description = "ISO two-letter country code / 国家二字码") String countryCode
) {
public static OverseasPartnerWarehouseResponse of(WarehouseEntity warehouse) {
return new OverseasPartnerWarehouseResponse(warehouse.getId(), warehouse.getWarehouseCode(),
warehouse.getWarehouseName(), warehouse.getCountryCode());
}
}
@@ -0,0 +1,19 @@
package com.baiyue.warehouse.dto;
import com.baiyue.warehouse.entity.WarehouseEntity;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(name = "SharedOverseasWarehouseResponse",
description = "Domestic-authoritative active shared overseas receiving warehouse")
public record SharedOverseasWarehouseResponse(
Long warehouseId,
String warehouseCode,
String warehouseName,
String countryCode,
String status
) {
public static SharedOverseasWarehouseResponse of(WarehouseEntity warehouse) {
return new SharedOverseasWarehouseResponse(warehouse.getId(), warehouse.getWarehouseCode(),
warehouse.getWarehouseName(), warehouse.getCountryCode(), warehouse.getStatus());
}
}
@@ -29,7 +29,7 @@ public final class WarehouseRequests {
@NotBlank @Size(max = 50) String warehouseCode,
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓", maxLength = 120, requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank @Size(max = 120) String warehouseName,
@Schema(description = "Warehouse type; defaults to DOMESTIC / 仓库类型,默认 DOMESTIC", example = "FBA", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"}, defaultValue = "DOMESTIC")
@Schema(description = "Warehouse type; defaults to DOMESTIC / 仓库类型,默认 DOMESTIC", example = "OVERSEAS_PARTNER", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"}, defaultValue = "DOMESTIC")
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER") String warehouseType,
@Schema(description = "ISO 3166-1 alpha-2 country code; DOMESTIC defaults to CN / ISO 两位国家代码,DOMESTIC 默认 CN", example = "US", pattern = "[A-Z]{2}")
@Pattern(regexp = "[A-Z]{2}") String countryCode,
@@ -104,7 +104,7 @@ public final class WarehouseRequests {
String countryCode,
@Schema(description = "Enabled capability code filter / 已启用能力编码筛选", example = "STORAGE")
String capabilityCode,
@Schema(description = "Warehouse type filter / 仓库类型筛选", example = "DOMESTIC", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"})
@Schema(description = "Warehouse type filter / 仓库类型筛选", example = "OVERSEAS_PARTNER", allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"})
@Pattern(regexp = "DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER") String warehouseType,
@Schema(description = "Status filter / 状态筛选", example = "ACTIVE", allowableValues = {"ACTIVE", "INACTIVE"})
@Pattern(regexp = "ACTIVE|INACTIVE") String status
@@ -28,6 +28,9 @@ public class PartnerEntity extends BaseEntity {
private String partnerName;
@Schema(description = "类型", example = "CARRIER", allowableValues = {"CARRIER", "SHIPPER", "CONSIGNEE"})
private String partnerType;
@Schema(description = "船司标准编码;船公司必填,其他类型必须为空", example = "MATSON",
allowableValues = {"MATSON", "SML", "ZIM", "COSCO", "EMC", "HEDE", "OTHER"})
private String carrierCode;
@TableField(exist = false)
@Schema(description = "单位类型集合", allowableValues = {"CARRIER", "SHIPPER", "CONSIGNEE"})
private List<String> types;
@@ -27,7 +27,8 @@ public class WarehouseEntity extends BaseEntity {
private String warehouseCode;
@Schema(description = "Warehouse name / 仓库名称", example = "Los Angeles Main Warehouse / 洛杉矶主仓")
private String warehouseName;
@Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS")
@Schema(description = "Warehouse type / 仓库类型", example = "OVERSEAS_PARTNER",
allowableValues = {"DOMESTIC", "FBA", "OVERSEAS", "OVERSEAS_PARTNER"})
private String warehouseType;
@Schema(description = "ISO two-letter country code / ISO 两位国家代码", example = "US")
private String countryCode;
@@ -59,6 +59,15 @@ public class WarehouseRepository {
.eq(WarehouseEntity::getStatus, "ACTIVE"));
}
public List<WarehouseEntity> findActiveByType(long tenantId, String warehouseType) {
return warehouses.selectList(new LambdaQueryWrapper<WarehouseEntity>()
.eq(WarehouseEntity::getTenantId, tenantId)
.eq(WarehouseEntity::getWarehouseType, warehouseType)
.eq(WarehouseEntity::getStatus, "ACTIVE")
.orderByAsc(WarehouseEntity::getWarehouseName)
.orderByAsc(WarehouseEntity::getId));
}
public boolean existsCode(long tenantId, String code, Long excludedId) {
return warehouses.selectCount(new LambdaQueryWrapper<WarehouseEntity>()
.eq(WarehouseEntity::getTenantId, tenantId)
@@ -35,10 +35,12 @@ public class PartnerImportService {
private static final String TEMPLATE_VERSION = "1";
private static final Set<String> ALLOWED_TYPES = Set.of(
"CARRIER", "SHIPPER", "CONSIGNEE");
private static final Set<String> ALLOWED_CARRIER_CODES = Set.of(
"MATSON", "SML", "ZIM", "COSCO", "EMC", "HEDE", "OTHER");
private static final Pattern EMAIL = Pattern.compile("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$");
private static final String[] HEADERS = {
"单位编码*", "单位名称*", "单位类型*", "联系人*", "联系电话*", "联系邮箱",
"国家二字码*", "省/州", "城市*", "区/县", "详细地址*", "邮政编码", "状态*", "备注"
"船司标准编码", "国家二字码*", "省/州", "城市*", "区/县", "详细地址*", "邮政编码", "状态*", "备注"
};
@Resource
@@ -107,9 +109,9 @@ public class PartnerImportService {
ImportRow item = new ImportRow(index + 1,
value(row, 0, formatter), value(row, 1, formatter), parseTypes(value(row, 2, formatter)),
value(row, 3, formatter), value(row, 4, formatter), value(row, 5, formatter),
value(row, 6, formatter).toUpperCase(Locale.ROOT), value(row, 7, formatter),
value(row, 8, formatter), value(row, 9, formatter), value(row, 10, formatter),
value(row, 11, formatter), value(row, 12, formatter).toUpperCase(Locale.ROOT), value(row, 13, formatter));
value(row, 6, formatter).toUpperCase(Locale.ROOT), value(row, 7, formatter).toUpperCase(Locale.ROOT),
value(row, 8, formatter), value(row, 9, formatter), value(row, 10, formatter), value(row, 11, formatter),
value(row, 12, formatter), value(row, 13, formatter).toUpperCase(Locale.ROOT), value(row, 14, formatter));
validate(tenantId, item, rows, errors);
rows.add(item);
}
@@ -134,6 +136,11 @@ public class PartnerImportService {
if (row.types.isEmpty()) errors.add(error(row, "单位类型", "至少填写一个类型"));
row.types.stream().filter(type -> !ALLOWED_TYPES.contains(type))
.forEach(type -> errors.add(error(row, "单位类型", "不支持类型 " + type)));
if (row.types.contains("CARRIER") && !ALLOWED_CARRIER_CODES.contains(row.carrierCode)) {
errors.add(error(row, "船司标准编码", "船公司必须选择 MATSON、SML、ZIM、COSCO、EMC、HEDE 或 OTHER"));
} else if (!row.types.contains("CARRIER") && !row.carrierCode.isBlank()) {
errors.add(error(row, "船司标准编码", "非船公司必须留空"));
}
required(row, errors, "联系人", row.contactName, 64);
required(row, errors, "联系电话", row.contactPhone, 32);
if (row.email.length() > 128 || (!row.email.isBlank() && !EMAIL.matcher(row.email).matches())) {
@@ -162,7 +169,8 @@ public class PartnerImportService {
entity.setPartnerCode(row.code);
entity.setPartnerName(row.name);
entity.setTypes(row.types);
entity.setPartnerType(row.types.get(0));
entity.setPartnerType(row.types.contains("CARRIER") ? "CARRIER" : row.types.get(0));
entity.setCarrierCode(nullable(row.carrierCode));
entity.setContactName(row.contactName);
entity.setContactPhone(row.contactPhone);
entity.setContactEmail(nullable(row.email));
@@ -216,6 +224,6 @@ public class PartnerImportService {
}
private record ImportRow(int row, String code, String name, List<String> types, String contactName,
String contactPhone, String email, String countryCode, String province, String city,
String contactPhone, String email, String carrierCode, String countryCode, String province, String city,
String district, String address, String postalCode, String status, String remark) { }
}
@@ -24,6 +24,8 @@ public class PlatformPartnerQueryService {
private static final long PUBLIC_TENANT_ID = 0L;
private static final Set<String> ALLOWED_QUERY_TYPES = Set.of(
"CARRIER", "SHIPPER", "CONSIGNEE");
private static final Set<String> ALLOWED_CARRIER_CODES = Set.of(
"MATSON", "SML", "ZIM", "COSCO", "EMC", "HEDE", "OTHER");
@Resource
private PartnerRepository repository;
@@ -121,10 +123,12 @@ public class PlatformPartnerQueryService {
private static void copy(PartnerEntity entity, PartnerSaveRequest request) {
List<String> types = normalizeTypes(request.types(), request.partnerType());
String carrierCode = normalizeCarrierCode(types, request.carrierCode());
entity.setPartnerCode(request.partnerCode());
entity.setPartnerName(request.partnerName());
entity.setTypes(types);
entity.setPartnerType(legacyType(types));
entity.setCarrierCode(carrierCode);
entity.setContactName(request.contactName());
entity.setContactPhone(request.contactPhone());
entity.setContactEmail(request.contactEmail());
@@ -154,6 +158,25 @@ public class PlatformPartnerQueryService {
}
private static String legacyType(List<String> types) {
return types.get(0);
return types.contains("CARRIER") ? "CARRIER" : types.get(0);
}
static String normalizeCarrierCode(List<String> types, String carrierCode) {
boolean carrier = types.contains("CARRIER");
if (!carrier) {
if (carrierCode != null && !carrierCode.isBlank()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "非船公司不得设置船司标准编码");
}
return null;
}
if (carrierCode == null || carrierCode.isBlank()) {
throw new BizException(ResultCode.BAD_REQUEST, "船公司必须选择船司标准编码");
}
String normalized = carrierCode.trim().toUpperCase(Locale.ROOT);
if (!ALLOWED_CARRIER_CODES.contains(normalized)) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"船司标准编码仅支持 MATSON、SML、ZIM、COSCO、EMC、HEDE 或 OTHER");
}
return normalized;
}
}
@@ -7,7 +7,9 @@ import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
import com.baiyue.warehouse.dto.WarehouseRequests;
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
import com.baiyue.warehouse.dto.WarehouseSummaryResponse;
import com.baiyue.warehouse.dto.SharedOverseasWarehouseResponse;
import com.baiyue.warehouse.entity.ServiceCapabilityEntity;
import com.baiyue.warehouse.entity.WarehouseAddressEntity;
import com.baiyue.warehouse.entity.WarehouseEntity;
@@ -86,6 +88,40 @@ public class WarehouseApplicationService {
return repository.activePage(tenantId, query);
}
public List<OverseasPartnerWarehouseResponse> activeOverseasPartners() {
return repository.findActiveByType(WarehouseSaveNormalizer.PUBLIC_TENANT_ID,
WarehouseSaveNormalizer.TYPE_OVERSEAS_PARTNER)
.stream()
.map(OverseasPartnerWarehouseResponse::of)
.toList();
}
public List<SharedOverseasWarehouseResponse> activeSharedOverseasWarehouses() {
return repository.findActiveByType(WarehouseSaveNormalizer.PUBLIC_TENANT_ID,
WarehouseSaveNormalizer.TYPE_OVERSEAS)
.stream()
.filter(warehouse -> hasReceivingCapability(warehouse.getId()))
.map(SharedOverseasWarehouseResponse::of)
.toList();
}
public SharedOverseasWarehouseResponse activeSharedOverseasWarehouse(long warehouseId) {
WarehouseEntity warehouse = repository.findOwned(WarehouseSaveNormalizer.PUBLIC_TENANT_ID, warehouseId);
if (warehouse == null || !"ACTIVE".equals(warehouse.getStatus())
|| !WarehouseSaveNormalizer.TYPE_OVERSEAS.equals(warehouse.getWarehouseType())
|| !hasReceivingCapability(warehouseId)) {
throw new BizException(ResultCode.NOT_FOUND,
"启用的公共海外收货仓不存在");
}
return SharedOverseasWarehouseResponse.of(warehouse);
}
private boolean hasReceivingCapability(long warehouseId) {
return repository.findCapabilities(WarehouseSaveNormalizer.PUBLIC_TENANT_ID, warehouseId).stream()
.anyMatch(capability -> "RECEIVING".equals(capability.getCapabilityCode())
&& Boolean.TRUE.equals(capability.getEnabled()));
}
public WarehouseDetailResponse activeDetail(long tenantId, long id) {
WarehouseEntity warehouse = repository.findOwned(tenantId, id);
if (warehouse == null) {
@@ -56,7 +56,7 @@ public class WarehouseImportService {
Row instruction = sheet.createRow(1);
instruction.createCell(0).setCellValue("说明");
instruction.createCell(1).setCellValue(
"默认仅新增;仓库类型默认 DOMESTIC,状态默认 ACTIVE;DOMESTIC 默认 CN/UTC+8;FBA 名称可不填并默认使用编码,多时区州必须显式填写时区;更新不覆盖联系人和备注");
"默认仅新增;仓库类型默认 DOMESTIC,可填 OVERSEAS_PARTNER 表示海外合作仓;状态默认 ACTIVE;DOMESTIC 默认 CN/UTC+8;FBA 名称可不填并默认使用编码,多时区州必须显式填写时区;更新不覆盖联系人和备注");
Row header = sheet.createRow(2);
for (int i = 0; i < HEADERS.length; i++) {
header.createCell(i).setCellValue(HEADERS[i]);
@@ -203,7 +203,7 @@ public class WarehouseImportService {
optionalLength(row, errors, "仓库名称", row.name(), 120);
}
if (!row.type().isBlank() && !row.type().matches("(?i)DOMESTIC|FBA|OVERSEAS|OVERSEAS_PARTNER")) {
errors.add(new WarehouseImportError(row.row(), "仓库类型", "只能填写 DOMESTIC、FBA 或 OVERSEAS"));
errors.add(new WarehouseImportError(row.row(), "仓库类型", "只能填写 DOMESTIC、FBA、OVERSEAS 或 OVERSEAS_PARTNER"));
}
if (!row.countryCode().isBlank() && !row.countryCode().matches("(?i)[A-Z]{2}")) {
errors.add(new WarehouseImportError(row.row(), "国家二字码", "必须是两个英文字母"));
@@ -71,7 +71,8 @@ final class WarehouseSaveNormalizer {
static NormalizedSaveRequest normalize(WarehouseRequests.SaveRequest request, long tenantId) {
String warehouseType = upperOrDefault(request.warehouseType(), TYPE_DOMESTIC);
if (!Set.of(TYPE_DOMESTIC, TYPE_FBA, TYPE_OVERSEAS, TYPE_OVERSEAS_PARTNER).contains(warehouseType)) {
throw new BizException(ResultCode.VALIDATION_FAILED, "Warehouse type must be DOMESTIC, FBA, OVERSEAS or OVERSEAS_PARTNER / 仓库类型只能为 DOMESTIC、FBA、OVERSEAS 或 OVERSEAS_PARTNER");
throw new BizException(ResultCode.VALIDATION_FAILED,
"Warehouse type must be DOMESTIC, FBA, OVERSEAS or OVERSEAS_PARTNER / 仓库类型只能为 DOMESTIC、FBA、OVERSEAS 或 OVERSEAS_PARTNER");
}
String status = upperOrDefault(request.status(), STATUS_ACTIVE);
if (!STATUS_ACTIVE.equals(status) && !"INACTIVE".equals(status)) {
@@ -102,7 +102,8 @@ CALL add_column_if_missing('wh_partner', 'tenant_id', 'BIGINT UNSIGNED NOT NULL
CALL add_column_if_missing('wh_partner', 'partner_code', 'VARCHAR(50) NOT NULL DEFAULT '''' AFTER tenant_id');
CALL add_column_if_missing('wh_partner', 'partner_name', 'VARCHAR(128) NOT NULL DEFAULT '''' AFTER partner_code');
CALL add_column_if_missing('wh_partner', 'partner_type', 'VARCHAR(20) NOT NULL DEFAULT ''CUSTOMER'' AFTER partner_name');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER partner_type');
CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL COMMENT ''船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填'' AFTER partner_type');
CALL add_column_if_missing('wh_partner', 'contact_name', 'VARCHAR(64) NOT NULL DEFAULT '''' AFTER carrier_code');
CALL add_column_if_missing('wh_partner', 'contact_phone', 'VARCHAR(32) NOT NULL DEFAULT '''' AFTER contact_name');
CALL add_column_if_missing('wh_partner', 'contact_email', 'VARCHAR(128) NULL');
CALL add_column_if_missing('wh_partner', 'country_code', 'CHAR(2) NOT NULL DEFAULT ''CN'' AFTER contact_email');
@@ -118,6 +119,8 @@ CALL add_column_if_missing('wh_partner', 'updated_by', 'BIGINT UNSIGNED NULL');
CALL add_column_if_missing('wh_partner', 'updated_at', 'DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)');
CALL add_column_if_missing('wh_partner', 'deleted', 'TINYINT(1) NOT NULL DEFAULT 0');
CALL add_column_if_missing('wh_partner', 'version', 'INT NOT NULL DEFAULT 0');
UPDATE wh_partner SET carrier_code = 'OTHER'
WHERE id = 191000000000000001 AND partner_type = 'CARRIER' AND carrier_code IS NULL;
-- 该表在旧库中不存在时先由 warehouse-express-company.sql 建表;此段补齐早期部署缺列。
CALL add_column_if_missing('wh_express_company', 'tenant_id', 'BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER id');
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS wh_partner (
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 '往来单位类型:CARRIER、SHIPPER或CONSIGNEE',
carrier_code VARCHAR(16) NULL COMMENT '船司标准编码:MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER;仅CARRIER必填',
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 '区县',
@@ -16,7 +17,8 @@ CREATE TABLE IF NOT EXISTS wh_partner (
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),
CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))
CONSTRAINT chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE')),
CONSTRAINT chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))
) 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',
@@ -162,7 +162,7 @@ JOIN wh_warehouse warehouse
ON DUPLICATE KEY UPDATE
tenant_id = VALUES(tenant_id);
-- 国内固定作业仓必须启用收货能力,FBA 目的地址仍不得创建作业能力。
-- 四个固定平台作业仓必须启用收货能力,FBA 目的地址仍不得创建作业能力。
INSERT INTO wh_service_capability (
id, tenant_id, warehouse_id, capability_code, capability_name, enabled, cutoff_time,
description, created_by, created_at, updated_by, updated_at, deleted, version
@@ -171,7 +171,11 @@ VALUES
(193000000000400001,0,193000000000200001,'RECEIVING','头程收货',1,'16:00:00',
'义乌仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0),
(193000000000400002,0,193000000000200002,'RECEIVING','头程收货',1,'16:00:00',
'东莞仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0)
'东莞仓头程订单收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0),
(193000000000400003,0,193000000000200003,'RECEIVING','海外收货',1,'16:00:00',
'洛杉矶仓海外接柜入仓收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0),
(193000000000400004,0,193000000000200004,'RECEIVING','海外收货',1,'16:00:00',
'纽约仓海外接柜入仓收货能力',NULL,CURRENT_TIMESTAMP(3),NULL,CURRENT_TIMESTAMP(3),0,0)
ON DUPLICATE KEY UPDATE
capability_name = VALUES(capability_name),
enabled = VALUES(enabled),
@@ -677,11 +681,11 @@ BEGIN
IF (SELECT COUNT(*)
FROM wh_service_capability capability
WHERE capability.tenant_id = 0
AND capability.warehouse_id IN (193000000000200001,193000000000200002)
AND capability.warehouse_id IN (193000000000200001,193000000000200002,193000000000200003,193000000000200004)
AND capability.capability_code = 'RECEIVING'
AND capability.enabled = 1
AND capability.deleted = 0) <> 2 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '国内固定作业仓收货能力写入校验失败';
AND capability.deleted = 0) <> 4 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '固定平台作业仓收货能力写入校验失败';
END IF;
END//
DELIMITER ;
@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS wh_warehouse (
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '平台公共仓库固定使用 tenant_id=0',
warehouse_code VARCHAR(50) NOT NULL COMMENT '平台内唯一仓库编码',
warehouse_name VARCHAR(120) NOT NULL COMMENT '仓库名称',
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC、FBA、OVERSEAS、OVERSEAS_PARTNER',
warehouse_type VARCHAR(32) NOT NULL DEFAULT 'DOMESTIC' COMMENT '公共仓库类型:DOMESTIC国内仓、FBA亚马逊仓、OVERSEAS普通海外仓、OVERSEAS_PARTNER海外合作仓',
country_code CHAR(2) NOT NULL COMMENT '所在国家二字码',
timezone VARCHAR(64) NOT NULL COMMENT '仓库所在地UTC偏移量',
contact_name VARCHAR(64) DEFAULT NULL COMMENT '仓库联系人',
@@ -3,6 +3,8 @@ package com.baiyue.warehouse.controller;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.WarehouseDetailResponse;
import com.baiyue.warehouse.dto.OverseasPartnerWarehouseResponse;
import com.baiyue.warehouse.dto.SharedOverseasWarehouseResponse;
import com.baiyue.warehouse.entity.WarehouseEntity;
import com.baiyue.warehouse.service.WarehouseApplicationService;
import org.junit.jupiter.api.BeforeEach;
@@ -71,4 +73,28 @@ class InternalWarehouseControllerTest {
verifyNoInteractions(service);
}
@Test
void listsActiveOverseasPartnerOptionsForInternalCallers() {
var option = new OverseasPartnerWarehouseResponse(200002L, "US-PARTNER-LAX", "洛杉矶合作仓", "US");
when(service.activeOverseasPartners()).thenReturn(List.of(option));
var response = controller.overseasPartners(INTERNAL_TOKEN);
assertThat(response.data()).containsExactly(option);
verify(service).activeOverseasPartners();
}
@Test
void exposesSharedOverseasReceivingDirectoryOnlyWithInternalToken() {
var warehouse = new SharedOverseasWarehouseResponse(200003L, "US-LAX-01",
"洛杉矶海外仓", "US", "ACTIVE");
when(service.activeSharedOverseasWarehouse(200003L)).thenReturn(warehouse);
assertThat(controller.sharedOverseasWarehouse(INTERNAL_TOKEN, 200003L).data())
.isEqualTo(warehouse);
assertThatThrownBy(() -> controller.sharedOverseasWarehouse("bad", 200003L))
.isInstanceOf(BizException.class);
verify(service).activeSharedOverseasWarehouse(200003L);
}
}
@@ -25,11 +25,12 @@ class WarehouseSchemaContractTest {
void shouldHandleCase02() throws IOException {
String sql = read("warehouse-partner.sql");
assertColumns(sql, "wh_partner", "data_scope_id", "partner_code", "partner_name", "partner_type", "contact_name",
"contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line",
"carrier_code", "contact_phone", "contact_email", "country_code", "province", "city", "district", "address_line",
"postal_code", "status", "remark");
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_partner_type")
.contains("idx_wh_partner_type_scope")
.contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.contains("chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))")
.contains("chk_wh_partner_type_value CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
assertBaseColumns(sql);
@@ -100,6 +101,8 @@ class WarehouseSchemaContractTest {
.contains("CALL add_column_if_missing('wh_warehouse_address'")
.contains("CALL add_column_if_missing('wh_service_capability'")
.contains("CALL add_column_if_missing('wh_partner'")
.contains("CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL")
.contains("UPDATE wh_partner SET carrier_code = 'OTHER'")
.contains("CALL add_column_if_missing('wh_express_company'")
.contains("DROP PROCEDURE IF EXISTS add_column_if_missing")
.doesNotContain("ADD COLUMN IF NOT EXISTS")
@@ -109,7 +112,7 @@ class WarehouseSchemaContractTest {
}
@Test
void shouldSeedReceivingCapabilityForDomesticOperatingWarehouses() throws IOException {
void shouldSeedReceivingCapabilityForFixedOperatingWarehouses() throws IOException {
String masterData = read("warehouse-public-master-data.sql");
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
@@ -117,7 +120,11 @@ class WarehouseSchemaContractTest {
assertThat(sql)
.contains("193000000000200001,'RECEIVING','头程收货',1")
.contains("193000000000200002,'RECEIVING','头程收货',1")
.contains("国内固定作业仓收货能力写入校验失败")
.contains("193000000000400003,0,193000000000200003,'RECEIVING','海外收货',1")
.contains("'洛杉矶仓海外接柜入仓收货能力'")
.contains("193000000000400004,0,193000000000200004,'RECEIVING','海外收货',1")
.contains("'纽约仓海外接柜入仓收货能力'")
.contains("固定平台作业仓收货能力写入校验失败")
.contains("warehouse.warehouse_type = 'FBA'")
.contains("FBA公共地址不得创建平台作业能力");
}
@@ -146,7 +153,13 @@ class WarehouseSchemaContractTest {
String unified = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
assertThat(tableDefinition(unified, "wh_partner"))
.contains("chk_wh_partner_type CHECK (partner_type IN ('CARRIER','SHIPPER','CONSIGNEE'))")
.contains("carrier_code VARCHAR(16) NULL")
.contains("chk_wh_partner_carrier_code CHECK (carrier_code IS NULL OR carrier_code IN ('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))")
.doesNotContain("CUSTOMS_BROKER", "SUPPLIER或BOTH");
assertThat(unified)
.contains("CALL add_column_if_missing('wh_partner', 'carrier_code', 'VARCHAR(16) NULL")
.contains("UPDATE wh_partner SET carrier_code = 'OTHER'")
.contains("queue_column_comment('baiyue_warehouse', 'wh_partner', 'carrier_code'");
}
private static String read(String file) throws IOException {
@@ -40,7 +40,7 @@ class PartnerImportServiceTest {
void shouldImportValidMultiTypeRow() throws Exception {
byte[] file = withRows(service.template(), new String[][] {
{"CAR-100", "测试船公司", "CARRIER,SHIPPER", "张三", "13800138000", "test@example.com",
"CN", "广东省", "深圳市", "南山区", "测试路 1 号", "518000", "ACTIVE", "测试备注"}
"MATSON", "CN", "广东省", "深圳市", "南山区", "测试路 1 号", "518000", "ACTIVE", "测试备注"}
});
PartnerImportResult result = service.importFile(0L, "partners.xlsx", file);
@@ -52,6 +52,7 @@ class PartnerImportServiceTest {
assertThat(captor.getValue().getTenantId()).isZero();
assertThat(captor.getValue().getDataScopeId()).isZero();
assertThat(captor.getValue().getTypes()).containsExactly("CARRIER", "SHIPPER");
assertThat(captor.getValue().getCarrierCode()).isEqualTo("MATSON");
assertThat(captor.getValue().getRemark()).isEqualTo("测试备注");
}
@@ -59,8 +60,8 @@ class PartnerImportServiceTest {
@DisplayName("文件内任一错误时整批零写入并返回行号")
void shouldRejectWholeBatchWhenAnyRowIsInvalid() throws Exception {
byte[] file = withRows(service.template(), new String[][] {
{"CAR-100", "测试船公司一", "CARRIER", "张三", "13800138000", "", "CN", "", "深圳市", "", "测试路 1 号", "", "ACTIVE", ""},
{"CAR-100", "测试单位二", "UNKNOWN", "李四", "13900139000", "bad-email", "C", "", "", "", "", "", "ENABLED", ""}
{"CAR-100", "测试船公司一", "CARRIER", "张三", "13800138000", "", "MATSON", "CN", "", "深圳市", "", "测试路 1 号", "", "ACTIVE", ""},
{"CAR-100", "测试单位二", "UNKNOWN", "李四", "13900139000", "bad-email", "", "C", "", "", "", "", "", "ENABLED", ""}
});
PartnerImportResult result = service.importFile(0L, "partners.xlsx", file);
@@ -79,6 +79,7 @@ class PlatformMasterDataManagementServiceTest {
assertThat(entity.getValue().getPartnerCode()).isEqualTo("CUS-001");
assertThat(entity.getValue().getPartnerName()).isEqualTo("示例客户");
assertThat(entity.getValue().getTypes()).containsExactly("CARRIER", "SHIPPER");
assertThat(entity.getValue().getCarrierCode()).isEqualTo("OTHER");
assertThat(entity.getValue().getRemark()).isEqualTo("测试备注");
}
@@ -210,7 +211,7 @@ class PlatformMasterDataManagementServiceTest {
}
private static PartnerSaveRequest partnerRequest() {
return new PartnerSaveRequest("CUS-001", "示例客户", "CARRIER", "张三", "13800138000",
return new PartnerSaveRequest("CUS-001", "示例客户", "CARRIER", "OTHER", "张三", "13800138000",
"test@example.com", "CN", "广东省", "深圳市", "南山区", "示例地址", "518000", "ACTIVE",
java.util.List.of("CARRIER", "SHIPPER"), "测试备注");
}
@@ -116,4 +116,56 @@ class WarehouseApplicationServiceTest {
.isEqualTo(ResultCode.VALIDATION_FAILED);
verify(repository, never()).insertWarehouse(any());
}
@Test
void createsOverseasPartnerWarehouseAsPlatformPublicMasterData() {
WarehouseRequests.SaveRequest request = new WarehouseRequests.SaveRequest(null, "US-PARTNER-LAX", "洛杉矶合作仓",
"OVERSEAS_PARTNER", "US", "UTC-8", "Alex", "+1-213-555-0100", null, null, null,
new WarehouseRequests.AddressRequest(null, "US", "CA", "Los Angeles", null,
"90001", "123 Partner Ave", null, null, null),
java.util.List.of(new WarehouseRequests.CapabilityRequest("STORAGE", "仓储", true, null, null)));
service.create(request);
var warehouse = org.mockito.ArgumentCaptor.forClass(WarehouseEntity.class);
verify(repository).insertWarehouse(warehouse.capture());
assertThat(warehouse.getValue().getTenantId()).isZero();
assertThat(warehouse.getValue().getWarehouseType()).isEqualTo("OVERSEAS_PARTNER");
assertThat(warehouse.getValue().getCountryCode()).isEqualTo("US");
assertThat(warehouse.getValue().getTimezone()).isEqualTo("UTC-8");
}
@Test
void exposesOnlyActiveOverseasPartnerWarehousesToInternalConsumers() {
WarehouseEntity partner = new WarehouseEntity();
partner.setId(2L);
partner.setWarehouseCode("US-PARTNER-LAX");
partner.setWarehouseName("洛杉矶合作仓");
partner.setCountryCode("US");
when(repository.findActiveByType(0L, "OVERSEAS_PARTNER")).thenReturn(java.util.List.of(partner));
assertThat(service.activeOverseasPartners())
.extracting("id", "warehouseCode", "warehouseName", "countryCode")
.containsExactly(tuple(2L, "US-PARTNER-LAX", "洛杉矶合作仓", "US"));
verify(repository).findActiveByType(0L, "OVERSEAS_PARTNER");
}
@Test
void exposesOnlyActiveSharedOverseasWarehousesWithReceivingCapability() {
WarehouseEntity receiving = new WarehouseEntity();
receiving.setId(3L); receiving.setWarehouseCode("US-LAX-01");
receiving.setWarehouseName("洛杉矶海外仓"); receiving.setCountryCode("US");
receiving.setWarehouseType("OVERSEAS"); receiving.setStatus("ACTIVE");
ServiceCapabilityEntity capability = new ServiceCapabilityEntity();
capability.setCapabilityCode("RECEIVING"); capability.setEnabled(true);
when(repository.findActiveByType(0L, "OVERSEAS")).thenReturn(java.util.List.of(receiving));
when(repository.findOwned(0L, 3L)).thenReturn(receiving);
when(repository.findCapabilities(0L, 3L)).thenReturn(java.util.List.of(capability));
assertThat(service.activeSharedOverseasWarehouses())
.extracting("warehouseId", "warehouseCode", "status")
.containsExactly(tuple(3L, "US-LAX-01", "ACTIVE"));
assertThat(service.activeSharedOverseasWarehouse(3L).warehouseName())
.isEqualTo("洛杉矶海外仓");
}
}
@@ -64,6 +64,22 @@ class WarehouseImportServiceTest {
assertThat(captor.getAllValues().get(1).warehouseName()).isEqualTo("FBA-LAX9");
}
@Test
@DisplayName("海外合作仓类型可通过批量导入进入公共仓库主数据")
void shouldImportOverseasPartnerWarehouse() throws Exception {
byte[] file = withRows(service.template(), new String[][] {
{"US-PARTNER-LAX", "洛杉矶合作仓", "OVERSEAS_PARTNER", "US", "CA", "Los Angeles", "", "123 Partner Ave", "90001", "UTC-8", "", "", "", "", ""}
});
WarehouseImportResult result = service.importFile(0L, "warehouses.xlsx", file, null);
assertThat(result.success()).isTrue();
var captor = org.mockito.ArgumentCaptor.forClass(WarehouseRequests.SaveRequest.class);
verify(warehouseService).create(captor.capture());
assertThat(captor.getValue().tenantId()).isZero();
assertThat(captor.getValue().warehouseType()).isEqualTo("OVERSEAS_PARTNER");
}
@Test
@DisplayName("任一行错误时整批零写入")
void shouldRejectWholeBatchWhenAnyRowInvalid() throws Exception {
@@ -19,6 +19,38 @@ SELECT COUNT(*) AS fixed_platform_address_count
AND address.deleted = 0
AND warehouse.deleted = 0;
SELECT COUNT(*) AS fixed_platform_receiving_capability_count
FROM wh_service_capability capability
WHERE capability.tenant_id = 0
AND capability.warehouse_id IN (
193000000000200001, 193000000000200002,
193000000000200003, 193000000000200004
)
AND capability.capability_code = 'RECEIVING'
AND capability.enabled = 1
AND capability.deleted = 0;
SELECT COUNT(*) AS overseas_receiving_capability_violations
FROM wh_service_capability capability
WHERE capability.tenant_id = 0
AND (
(capability.id = 193000000000400003
AND (capability.warehouse_id <> 193000000000200003
OR capability.capability_code <> 'RECEIVING'
OR capability.capability_name <> '海外收货'
OR capability.description <> '洛杉矶仓海外接柜入仓收货能力'
OR capability.enabled <> 1
OR capability.deleted <> 0))
OR
(capability.id = 193000000000400004
AND (capability.warehouse_id <> 193000000000200004
OR capability.capability_code <> 'RECEIVING'
OR capability.capability_name <> '海外收货'
OR capability.description <> '纽约仓海外接柜入仓收货能力'
OR capability.enabled <> 1
OR capability.deleted <> 0))
);
SELECT COUNT(*) AS fba_public_warehouse_count
FROM wh_warehouse
WHERE tenant_id = 0
@@ -111,6 +111,12 @@ assert.doesNotMatch(sql, /contact_name\s*=\s*VALUES\(contact_name\)/)
assert.doesNotMatch(sql, /contact_phone\s*=\s*VALUES\(contact_phone\)/)
assert.doesNotMatch(sql, /contact_email\s*=\s*VALUES\(contact_email\)/)
assert.doesNotMatch(sql, /remark\s*=\s*VALUES\(remark\)/)
assert.doesNotMatch(sql, /INSERT INTO wh_service_capability/i)
assert.match(sql, /四个固定平台作业仓必须启用收货能力/)
assert.match(sql, /INSERT INTO wh_service_capability[\s\S]+?'RECEIVING'/i)
assert.match(sql, /193000000000400003,0,193000000000200003,'RECEIVING','海外收货',1,'16:00:00',[\s\S]*?'洛杉矶仓海外接柜入仓收货能力'/)
assert.match(sql, /193000000000400004,0,193000000000200004,'RECEIVING','海外收货',1,'16:00:00',[\s\S]*?'纽约仓海外接柜入仓收货能力'/)
assert.match(sql, /warehouse_id IN \(193000000000200001,193000000000200002,193000000000200003,193000000000200004\)[\s\S]*?<> 4 THEN[\s\S]*?固定平台作业仓收货能力写入校验失败/)
const fbaBlock = sql.slice(sql.indexOf('CREATE TEMPORARY TABLE tmp_wh_fba_public_master_data'))
assert.doesNotMatch(fbaBlock, /INSERT INTO wh_service_capability/i)
assert.match(sql, /FBA公共主数据地址种子ID与既有数据冲突/)
assert.match(sql, /FBA公共地址不得创建平台作业能力/)