优化
This commit is contained in:
@@ -15144,7 +15144,7 @@ USE baiyue_warehouse;
|
|||||||
CREATE TABLE IF NOT EXISTS wh_supplier (
|
CREATE TABLE IF NOT EXISTS wh_supplier (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '供应商ID', tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
|
id BIGINT UNSIGNED NOT NULL COMMENT '供应商ID', tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
|
||||||
name VARCHAR(160) NOT NULL COMMENT '供应商名称', category VARCHAR(20) NOT NULL COMMENT '供应商类别:BOOKING订舱、CUSTOMS报关、TRUCKING拖车、CERTIFICATE认证',
|
name VARCHAR(160) NOT NULL COMMENT '供应商名称', category VARCHAR(20) NOT NULL COMMENT '供应商类别:BOOKING订舱、CUSTOMS报关、TRUCKING拖车、CERTIFICATE认证',
|
||||||
scope VARCHAR(20) NULL COMMENT '订舱分类范围:DOMESTIC/INTERNATIONAL', region VARCHAR(80) NOT NULL COMMENT '地区',
|
region VARCHAR(80) NOT NULL COMMENT '地区',
|
||||||
legal_representative VARCHAR(80) NOT NULL COMMENT '法人代表', contact_name VARCHAR(80) NOT NULL COMMENT '联系人',
|
legal_representative VARCHAR(80) NOT NULL COMMENT '法人代表', contact_name VARCHAR(80) NOT NULL COMMENT '联系人',
|
||||||
phone VARCHAR(40) NOT NULL COMMENT '联系电话', email VARCHAR(160) NULL COMMENT '电子邮箱', address VARCHAR(255) NULL COMMENT '公司地址',
|
phone VARCHAR(40) NOT NULL COMMENT '联系电话', email VARCHAR(160) NULL COMMENT '电子邮箱', address VARCHAR(255) NULL COMMENT '公司地址',
|
||||||
business_license_no VARCHAR(64) NULL COMMENT '营业执照号', license_file_id VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID',
|
business_license_no VARCHAR(64) NULL COMMENT '营业执照号', license_file_id VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID',
|
||||||
@@ -15154,7 +15154,6 @@ CREATE TABLE IF NOT EXISTS wh_supplier (
|
|||||||
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', PRIMARY KEY(id),
|
deleted TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标记', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', PRIMARY KEY(id),
|
||||||
UNIQUE KEY uk_wh_supplier_name(tenant_id,name), KEY idx_wh_supplier_filter(tenant_id,category,status,region),
|
UNIQUE KEY uk_wh_supplier_name(tenant_id,name), KEY idx_wh_supplier_filter(tenant_id,category,status,region),
|
||||||
CONSTRAINT chk_wh_supplier_category CHECK(category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')),
|
CONSTRAINT chk_wh_supplier_category CHECK(category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')),
|
||||||
CONSTRAINT chk_wh_supplier_scope CHECK((category='BOOKING' AND scope IN ('DOMESTIC','INTERNATIONAL')) OR (category<>'BOOKING' AND scope IS NULL)),
|
|
||||||
CONSTRAINT chk_wh_supplier_status CHECK(status IN ('ACTIVE','INACTIVE')),
|
CONSTRAINT chk_wh_supplier_status CHECK(status IN ('ACTIVE','INACTIVE')),
|
||||||
CONSTRAINT chk_wh_supplier_settlement CHECK(settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
CONSTRAINT chk_wh_supplier_settlement CHECK(settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
||||||
@@ -15167,7 +15166,6 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL supplier_add_column_if_missing('scope',"VARCHAR(20) NULL COMMENT '订舱分类范围:DOMESTIC/INTERNATIONAL' AFTER category");
|
|
||||||
CALL supplier_add_column_if_missing('license_file_id',"VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID' AFTER business_license_no");
|
CALL supplier_add_column_if_missing('license_file_id',"VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID' AFTER business_license_no");
|
||||||
CALL supplier_add_column_if_missing('created_by',"BIGINT UNSIGNED NULL COMMENT '创建人ID'");
|
CALL supplier_add_column_if_missing('created_by',"BIGINT UNSIGNED NULL COMMENT '创建人ID'");
|
||||||
CALL supplier_add_column_if_missing('created_at',"DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'");
|
CALL supplier_add_column_if_missing('created_at',"DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'");
|
||||||
@@ -15188,7 +15186,6 @@ BEGIN
|
|||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_category',"category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_category',"category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')");
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_scope',"(category='BOOKING' AND scope IN ('DOMESTIC','INTERNATIONAL')) OR (category<>'BOOKING' AND scope IS NULL)");
|
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_status',"status IN ('ACTIVE','INACTIVE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_status',"status IN ('ACTIVE','INACTIVE')");
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_settlement',"settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_settlement',"settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE')");
|
||||||
DROP PROCEDURE supplier_add_check_if_missing;
|
DROP PROCEDURE supplier_add_check_if_missing;
|
||||||
|
|||||||
@@ -16,5 +16,5 @@ public interface WarehouseSupplierClient {
|
|||||||
@RequestParam(defaultValue = "true") boolean requireActive,
|
@RequestParam(defaultValue = "true") boolean requireActive,
|
||||||
@RequestParam String category);
|
@RequestParam String category);
|
||||||
|
|
||||||
record SupplierSnapshot(long supplierId, String name, String category, String scope, String status) {}
|
record SupplierSnapshot(long supplierId, String name, String category, String status) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class FinanceFeeSourceEventServiceTest {
|
|||||||
lenient().when(supplierValidator.categoryFor(any(), any())).thenReturn("TRUCKING");
|
lenient().when(supplierValidator.categoryFor(any(), any())).thenReturn("TRUCKING");
|
||||||
lenient().when(supplierValidator.requireActive(anyLong(), any())).thenReturn(
|
lenient().when(supplierValidator.requireActive(anyLong(), any())).thenReturn(
|
||||||
new com.baiyue.finance.client.WarehouseSupplierClient.SupplierSnapshot(
|
new com.baiyue.finance.client.WarehouseSupplierClient.SupplierSnapshot(
|
||||||
71L, "现有供应商", "TRUCKING", "DOMESTIC", "ACTIVE"));
|
71L, "现有供应商", "TRUCKING", "ACTIVE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -545,7 +545,7 @@ class FinanceSettlementBehaviorTest {
|
|||||||
org.mockito.Mockito.lenient().when(supplierValidator.categoryFor(any(), any())).thenReturn("BOOKING");
|
org.mockito.Mockito.lenient().when(supplierValidator.categoryFor(any(), any())).thenReturn("BOOKING");
|
||||||
org.mockito.Mockito.lenient().when(supplierValidator.requireActive(anyLong(), any()))
|
org.mockito.Mockito.lenient().when(supplierValidator.requireActive(anyLong(), any()))
|
||||||
.thenReturn(new com.baiyue.finance.client.WarehouseSupplierClient.SupplierSnapshot(
|
.thenReturn(new com.baiyue.finance.client.WarehouseSupplierClient.SupplierSnapshot(
|
||||||
20L, "供应商20", "BOOKING", "INTERNATIONAL", "ACTIVE"));
|
20L, "供应商20", "BOOKING", "ACTIVE"));
|
||||||
return supplierValidator;
|
return supplierValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class FinanceSupplierValidatorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void requiresActiveWarehouseSupplierWithMatchingCategory() {
|
void requiresActiveWarehouseSupplierWithMatchingCategory() {
|
||||||
var supplier = new WarehouseSupplierClient.SupplierSnapshot(20L, "正式供应商", "TRUCKING", "DOMESTIC", "ACTIVE");
|
var supplier = new WarehouseSupplierClient.SupplierSnapshot(20L, "正式供应商", "TRUCKING", "ACTIVE");
|
||||||
when(client.get("internal-token", 20L, true, "TRUCKING")).thenReturn(ApiResponse.success(supplier));
|
when(client.get("internal-token", 20L, true, "TRUCKING")).thenReturn(ApiResponse.success(supplier));
|
||||||
|
|
||||||
assertThat(validator.requireActive(20L, "TRUCKING").name()).isEqualTo("正式供应商");
|
assertThat(validator.requireActive(20L, "TRUCKING").name()).isEqualTo("正式供应商");
|
||||||
@@ -37,7 +37,7 @@ class FinanceSupplierValidatorTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsMismatchedOrUnavailableSupplier() {
|
void rejectsMismatchedOrUnavailableSupplier() {
|
||||||
var mismatched = new WarehouseSupplierClient.SupplierSnapshot(20L, "订舱供应商", "BOOKING", "INTERNATIONAL", "ACTIVE");
|
var mismatched = new WarehouseSupplierClient.SupplierSnapshot(20L, "订舱供应商", "BOOKING", "ACTIVE");
|
||||||
when(client.get("internal-token", 20L, true, "TRUCKING")).thenReturn(ApiResponse.success(mismatched));
|
when(client.get("internal-token", 20L, true, "TRUCKING")).thenReturn(ApiResponse.success(mismatched));
|
||||||
assertCode(() -> validator.requireActive(20L, "TRUCKING"), "FIN_409");
|
assertCode(() -> validator.requireActive(20L, "TRUCKING"), "FIN_409");
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@
|
|||||||
- 平台接口必须校验目标客户、权限、乐观锁、幂等键、历史和审计;商户查询只能使用登录态客户。
|
- 平台接口必须校验目标客户、权限、乐观锁、幂等键、历史和审计;商户查询只能使用登录态客户。
|
||||||
- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。
|
- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。
|
||||||
- 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。
|
- 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。
|
||||||
- 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING + INTERNATIONAL`,拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别、范围或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。
|
- 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。
|
||||||
- Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。
|
- Firstleg 只拥有业务费用及履约事实;Finance 拥有费用入池、账期、账单、收款、成本分摊结果和财务状态。修改费用来源版本、发布条件、柜次归属或体积快照时同步检查 `../finance/src/main/java/com/baiyue/finance/client/FirstlegFeeSourceClient.java`、同目录 `FirstlegCostSnapshotClient.java` 及双方契约测试,不在 Firstleg 复制财务状态机。
|
||||||
|
|
||||||
## 三期装柜与 PDA 资源结算
|
## 三期装柜与 PDA 资源结算
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ public interface WarehouseSupplierClient {
|
|||||||
ApiResponse<Snapshot> get(@RequestHeader("X-Internal-Service-Token") String token,
|
ApiResponse<Snapshot> get(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
@PathVariable long supplierId,
|
@PathVariable long supplierId,
|
||||||
@RequestParam(defaultValue = "true") boolean requireActive,
|
@RequestParam(defaultValue = "true") boolean requireActive,
|
||||||
@RequestParam(required = false) String category,
|
@RequestParam(required = false) String category);
|
||||||
@RequestParam(required = false) String scope);
|
|
||||||
|
|
||||||
record Snapshot(long supplierId, String name, String category, String scope, String status) {}
|
record Snapshot(long supplierId, String name, String category, String status) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,8 @@ public final class LoadingPickDtos {
|
|||||||
|
|
||||||
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
|
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
|
||||||
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
|
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
|
||||||
String currentContainerNo, String taskStatus,
|
String currentContainerNo, String sourceType, String recordStatus,
|
||||||
|
Long mergedIntoContainerBookingId, String taskStatus,
|
||||||
String workflowStatus, Integer version, Assignment assignment,
|
String workflowStatus, Integer version, Assignment assignment,
|
||||||
Review review, List<PickItem> items) {
|
Review review, List<PickItem> items) {
|
||||||
}
|
}
|
||||||
@@ -135,6 +136,7 @@ public final class LoadingPickDtos {
|
|||||||
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
|
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
|
||||||
public record PendingTask(Long taskId, Long loadingPlanId, Long containerBookingId,
|
public record PendingTask(Long taskId, Long loadingPlanId, Long containerBookingId,
|
||||||
String cabinetNo, String currentContainerNo,
|
String cabinetNo, String currentContainerNo,
|
||||||
|
String sourceType, String recordStatus, Long mergedIntoContainerBookingId,
|
||||||
Long warehouseId, String warehouseName, String taskStatus,
|
Long warehouseId, String warehouseName, String taskStatus,
|
||||||
String workflowStatus, Integer version, Integer assignmentVersion,
|
String workflowStatus, Integer version, Integer assignmentVersion,
|
||||||
Integer itemCount, Integer pendingItemCount) {
|
Integer itemCount, Integer pendingItemCount) {
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
SELECT task.id AS taskId, task.loading_plan_id AS loadingPlanId,
|
SELECT task.id AS taskId, task.loading_plan_id AS loadingPlanId,
|
||||||
task.container_booking_id AS containerBookingId, task.cabinet_no AS cabinetNo,
|
task.container_booking_id AS containerBookingId, task.cabinet_no AS cabinetNo,
|
||||||
booking.container_no AS containerNo, booking.warehouse_id AS warehouseId,
|
booking.container_no AS containerNo, booking.warehouse_id AS warehouseId,
|
||||||
|
booking.source_type AS sourceType, booking.record_status AS recordStatus,
|
||||||
|
booking.merged_into_container_booking_id AS mergedIntoContainerBookingId,
|
||||||
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
|
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
|
||||||
CASE WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED' ELSE 'IN_PROGRESS' END AS workflowStatus,
|
CASE WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED' ELSE 'IN_PROGRESS' END AS workflowStatus,
|
||||||
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
|
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
|
||||||
@@ -67,6 +69,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
</if>
|
</if>
|
||||||
GROUP BY task.id, task.loading_plan_id, task.container_booking_id, task.cabinet_no,
|
GROUP BY task.id, task.loading_plan_id, task.container_booking_id, task.cabinet_no,
|
||||||
booking.container_no, booking.warehouse_id,
|
booking.container_no, booking.warehouse_id,
|
||||||
|
booking.source_type, booking.record_status, booking.merged_into_container_booking_id,
|
||||||
booking.warehouse_name, task.task_status, task.version,
|
booking.warehouse_name, task.task_status, task.version,
|
||||||
assignment.pick_task_id, assignment.version
|
assignment.pick_task_id, assignment.version
|
||||||
ORDER BY task.id DESC
|
ORDER BY task.id DESC
|
||||||
@@ -82,6 +85,9 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
private String containerNo;
|
private String containerNo;
|
||||||
private Long containerBookingId;
|
private Long containerBookingId;
|
||||||
private String cabinetNo;
|
private String cabinetNo;
|
||||||
|
private String sourceType;
|
||||||
|
private String recordStatus;
|
||||||
|
private Long mergedIntoContainerBookingId;
|
||||||
private Long warehouseId;
|
private Long warehouseId;
|
||||||
private String warehouseName;
|
private String warehouseName;
|
||||||
private String taskStatus;
|
private String taskStatus;
|
||||||
@@ -101,6 +107,12 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
public void setContainerBookingId(Long value) { this.containerBookingId = value; }
|
public void setContainerBookingId(Long value) { this.containerBookingId = value; }
|
||||||
public String getCabinetNo() { return cabinetNo; }
|
public String getCabinetNo() { return cabinetNo; }
|
||||||
public void setCabinetNo(String value) { this.cabinetNo = value; }
|
public void setCabinetNo(String value) { this.cabinetNo = value; }
|
||||||
|
public String getSourceType() { return sourceType; }
|
||||||
|
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
|
||||||
|
public String getRecordStatus() { return recordStatus; }
|
||||||
|
public void setRecordStatus(String recordStatus) { this.recordStatus = recordStatus; }
|
||||||
|
public Long getMergedIntoContainerBookingId() { return mergedIntoContainerBookingId; }
|
||||||
|
public void setMergedIntoContainerBookingId(Long value) { this.mergedIntoContainerBookingId = value; }
|
||||||
public Long getWarehouseId() { return warehouseId; }
|
public Long getWarehouseId() { return warehouseId; }
|
||||||
public void setWarehouseId(Long warehouseId) { this.warehouseId = warehouseId; }
|
public void setWarehouseId(Long warehouseId) { this.warehouseId = warehouseId; }
|
||||||
public String getWarehouseName() { return warehouseName; }
|
public String getWarehouseName() { return warehouseName; }
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ public class LoadingPickService {
|
|||||||
return repository.pendingTasks(operatorId, normalizedKeyword).stream()
|
return repository.pendingTasks(operatorId, normalizedKeyword).stream()
|
||||||
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerBookingId(),
|
.map(row -> new PendingTask(row.getTaskId(), row.getLoadingPlanId(), row.getContainerBookingId(),
|
||||||
row.getCabinetNo(), row.getContainerNo(),
|
row.getCabinetNo(), row.getContainerNo(),
|
||||||
|
row.getSourceType(), row.getRecordStatus(), row.getMergedIntoContainerBookingId(),
|
||||||
row.getWarehouseId(), row.getWarehouseName(), row.getTaskStatus(), row.getWorkflowStatus(),
|
row.getWarehouseId(), row.getWarehouseName(), row.getTaskStatus(), row.getWorkflowStatus(),
|
||||||
row.getVersion(), row.getAssignmentVersion(), row.getItemCount(), row.getPendingItemCount()))
|
row.getVersion(), row.getAssignmentVersion(), row.getItemCount(), row.getPendingItemCount()))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -607,6 +608,9 @@ public class LoadingPickService {
|
|||||||
: assigned == null ? "UNASSIGNED"
|
: assigned == null ? "UNASSIGNED"
|
||||||
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
||||||
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
|
var booking = smartLoadingRepository.booking(task.getContainerBookingId());
|
||||||
|
if (booking == null || !"ACTIVE".equals(booking.getRecordStatus())) {
|
||||||
|
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录");
|
||||||
|
}
|
||||||
var owners = repository.inboundOwners(task.getId());
|
var owners = repository.inboundOwners(task.getId());
|
||||||
Long detailInboundId = source.isEmpty() ? null : source.get(0).getInboundId();
|
Long detailInboundId = source.isEmpty() ? null : source.get(0).getInboundId();
|
||||||
var owner = detailInboundId == null ? null : owners.stream()
|
var owner = detailInboundId == null ? null : owners.stream()
|
||||||
@@ -616,7 +620,8 @@ public class LoadingPickService {
|
|||||||
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(),
|
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(),
|
||||||
owner == null ? null : owner.inboundId(), inbound == null ? null : inbound.version(),
|
owner == null ? null : owner.inboundId(), inbound == null ? null : inbound.version(),
|
||||||
inbound == null ? null : inbound.version(), task.getCabinetNo(),
|
inbound == null ? null : inbound.version(), task.getCabinetNo(),
|
||||||
booking == null ? null : booking.getContainerNo(), task.getTaskStatus(),
|
booking.getContainerNo(), booking.getSourceType(), booking.getRecordStatus(),
|
||||||
|
booking.getMergedIntoContainerBookingId(), task.getTaskStatus(),
|
||||||
workflowStatus, task.getVersion(), assignment, review, items);
|
workflowStatus, task.getVersion(), assignment, review, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-6
@@ -15,29 +15,32 @@ public class WarehouseSupplierSnapshotService {
|
|||||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||||
|
|
||||||
public WarehouseSupplierClient.Snapshot booking(Long supplierId, String suppliedName) {
|
public WarehouseSupplierClient.Snapshot booking(Long supplierId, String suppliedName) {
|
||||||
return resolve(supplierId, suppliedName, "BOOKING", "INTERNATIONAL");
|
return resolve(supplierId, suppliedName, "BOOKING");
|
||||||
}
|
}
|
||||||
|
|
||||||
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
|
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
|
||||||
return resolve(supplierId, suppliedName, "TRUCKING", null);
|
return resolve(supplierId, suppliedName, "TRUCKING");
|
||||||
}
|
}
|
||||||
|
|
||||||
public WarehouseSupplierClient.Snapshot customs(Long supplierId, String suppliedName) {
|
public WarehouseSupplierClient.Snapshot customs(Long supplierId, String suppliedName) {
|
||||||
return resolve(supplierId, suppliedName, "CUSTOMS", null);
|
return resolve(supplierId, suppliedName, "CUSTOMS");
|
||||||
}
|
}
|
||||||
|
|
||||||
private WarehouseSupplierClient.Snapshot resolve(Long supplierId, String suppliedName,
|
private WarehouseSupplierClient.Snapshot resolve(Long supplierId, String suppliedName,
|
||||||
String category, String scope) {
|
String category) {
|
||||||
if (supplierId == null || supplierId <= 0) {
|
if (supplierId == null || supplierId <= 0) {
|
||||||
if (StringUtils.hasText(suppliedName)) throw new BizException(ResultCode.BAD_REQUEST,
|
if (StringUtils.hasText(suppliedName)) throw new BizException(ResultCode.BAD_REQUEST,
|
||||||
"供应商名称必须来自有效的供应商ID,禁止自由文本");
|
"供应商名称必须来自有效的供应商ID,禁止自由文本");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var response = client.get(internalToken, supplierId, true, category, scope);
|
var response = client.get(internalToken, supplierId, true, category);
|
||||||
if (response == null || !response.successful() || response.data() == null
|
if (response == null || !response.successful() || response.data() == null
|
||||||
|
|| response.data().supplierId() != supplierId
|
||||||
|
|| !"ACTIVE".equals(response.data().status())
|
||||||
|
|| !category.equals(response.data().category())
|
||||||
|| !StringUtils.hasText(response.data().name())) {
|
|| !StringUtils.hasText(response.data().name())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "所选供应商不存在、未启用或类别/范围不匹配");
|
throw new BizException(ResultCode.CONFLICT, "所选供应商不存在、未启用或类别不匹配");
|
||||||
}
|
}
|
||||||
if (StringUtils.hasText(suppliedName)
|
if (StringUtils.hasText(suppliedName)
|
||||||
&& !suppliedName.trim().equals(response.data().name())) {
|
&& !suppliedName.trim().equals(response.data().name())) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ components:
|
|||||||
warehouseId: {type: integer, format: int64, minimum: 1, description: ACTIVE domestic transit warehouse ID; warehouseName is resolved and frozen server-side.}
|
warehouseId: {type: integer, format: int64, minimum: 1, description: ACTIVE domestic transit warehouse ID; warehouseName is resolved and frozen server-side.}
|
||||||
shippingCompanyId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional shipping-company master-data ID; the carrier is independent from the logistics channel and booking-agent Supplier.}
|
shippingCompanyId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional shipping-company master-data ID; the carrier is independent from the logistics channel and booking-agent Supplier.}
|
||||||
shippingCompanyName: {type: string, maxLength: 128, nullable: true, description: Optional shipping-company name snapshot.}
|
shippingCompanyName: {type: string, maxLength: 128, nullable: true, description: Optional shipping-company name snapshot.}
|
||||||
bookingAgentId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE Warehouse supplier ID validated as BOOKING with INTERNATIONAL scope.}
|
bookingAgentId: {type: integer, format: int64, minimum: 1, nullable: true, description: Optional ACTIVE Warehouse supplier ID validated only as category BOOKING; booking suppliers do not use domestic/international scope.}
|
||||||
bookingAgentName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot always comes from Warehouse and cannot be supplied without bookingAgentId.}
|
bookingAgentName: {type: string, maxLength: 128, nullable: true, description: Optional consistency hint only; the persisted snapshot always comes from Warehouse and cannot be supplied without bookingAgentId.}
|
||||||
serviceTerm: {type: string, maxLength: 32, description: Required free-text Service Term snapshot, for example CY-CY.}
|
serviceTerm: {type: string, maxLength: 32, description: Required free-text Service Term snapshot, for example CY-CY.}
|
||||||
freightTerm: {type: string, maxLength: 32, description: Required free-text Freight Term snapshot, for example NINGBO.}
|
freightTerm: {type: string, maxLength: 32, description: Required free-text Freight Term snapshot, for example NINGBO.}
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ paths:
|
|||||||
- {$ref: '#/components/parameters/TaskId'}
|
- {$ref: '#/components/parameters/TaskId'}
|
||||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Task with current transport display fields}
|
'200':
|
||||||
|
description: Task with current transport display and ACTIVE container lifecycle fields
|
||||||
|
content: {application/json: {schema: {$ref: '#/components/schemas/Task'}}}
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:
|
||||||
post:
|
post:
|
||||||
summary: Atomically claim an unassigned task for the authenticated operator
|
summary: Atomically claim an unassigned task for the authenticated operator
|
||||||
@@ -124,7 +126,7 @@ components:
|
|||||||
schemas:
|
schemas:
|
||||||
Task:
|
Task:
|
||||||
type: object
|
type: object
|
||||||
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, taskStatus, workflowStatus, version, items]
|
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
|
||||||
properties:
|
properties:
|
||||||
id: {type: integer, format: int64}
|
id: {type: integer, format: int64}
|
||||||
loadingPlanId: {type: integer, format: int64}
|
loadingPlanId: {type: integer, format: int64}
|
||||||
@@ -134,6 +136,9 @@ components:
|
|||||||
loadingVersion: {type: integer, minimum: 0, description: Compatibility alias of inboundVersion from the same fl_inbound_order row; /loading/complete uses this inbound loading-stage version}
|
loadingVersion: {type: integer, minimum: 0, description: Compatibility alias of inboundVersion from the same fl_inbound_order row; /loading/complete uses this inbound loading-stage version}
|
||||||
cabinetNo: {type: string}
|
cabinetNo: {type: string}
|
||||||
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; changing it never resynchronizes the task or item versions and it is never task identity}
|
currentContainerNo: {type: string, nullable: true, description: Mutable transport display field; changing it never resynchronizes the task or item versions and it is never task identity}
|
||||||
|
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING], description: Source of the surviving stable container identity}
|
||||||
|
recordStatus: {type: string, enum: [ACTIVE], description: Task detail is served only from the ACTIVE container booking currently owned by the loading plan}
|
||||||
|
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for the surviving ACTIVE operational container; populated only on hidden MERGED source history}
|
||||||
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
|
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
|
||||||
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
|
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
|
||||||
version: {type: integer, minimum: 0}
|
version: {type: integer, minimum: 0}
|
||||||
@@ -164,13 +169,16 @@ components:
|
|||||||
version: {type: integer, minimum: 0}
|
version: {type: integer, minimum: 0}
|
||||||
PendingTask:
|
PendingTask:
|
||||||
type: object
|
type: object
|
||||||
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
|
required: [taskId, loadingPlanId, containerBookingId, cabinetNo, sourceType, recordStatus, warehouseId, taskStatus, workflowStatus, version, assignmentVersion, itemCount, pendingItemCount]
|
||||||
properties:
|
properties:
|
||||||
taskId: {type: integer, format: int64}
|
taskId: {type: integer, format: int64}
|
||||||
loadingPlanId: {type: integer, format: int64}
|
loadingPlanId: {type: integer, format: int64}
|
||||||
containerBookingId: {type: integer, format: int64}
|
containerBookingId: {type: integer, format: int64}
|
||||||
cabinetNo: {type: string}
|
cabinetNo: {type: string}
|
||||||
currentContainerNo: {type: string, nullable: true}
|
currentContainerNo: {type: string, nullable: true}
|
||||||
|
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING]}
|
||||||
|
recordStatus: {type: string, enum: [ACTIVE], description: Enforced by the server-side pending-task query; PDA clients must not infer or relax this filter}
|
||||||
|
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for an ACTIVE pending task}
|
||||||
warehouseId: {type: integer, format: int64}
|
warehouseId: {type: integer, format: int64}
|
||||||
warehouseName: {type: string, nullable: true}
|
warehouseName: {type: string, nullable: true}
|
||||||
taskStatus: {type: string, enum: [LOADING]}
|
taskStatus: {type: string, enum: [LOADING]}
|
||||||
|
|||||||
+5
-3
@@ -19,13 +19,13 @@ class WarehouseSupplierClientContractTest {
|
|||||||
FeignClient annotation = WarehouseSupplierClient.class.getAnnotation(FeignClient.class);
|
FeignClient annotation = WarehouseSupplierClient.class.getAnnotation(FeignClient.class);
|
||||||
assertThat(annotation.path()).isEqualTo("/internal/warehouse/v1/suppliers");
|
assertThat(annotation.path()).isEqualTo("/internal/warehouse/v1/suppliers");
|
||||||
Method method = WarehouseSupplierClient.class.getDeclaredMethod("get", String.class, long.class,
|
Method method = WarehouseSupplierClient.class.getDeclaredMethod("get", String.class, long.class,
|
||||||
boolean.class, String.class, String.class);
|
boolean.class, String.class);
|
||||||
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly("/{supplierId}");
|
assertThat(method.getAnnotation(GetMapping.class).value()).containsExactly("/{supplierId}");
|
||||||
assertThat(method.getParameterAnnotations()[0][0]).isInstanceOf(RequestHeader.class);
|
assertThat(method.getParameterAnnotations()[0][0]).isInstanceOf(RequestHeader.class);
|
||||||
assertThat(((RequestHeader) method.getParameterAnnotations()[0][0]).value())
|
assertThat(((RequestHeader) method.getParameterAnnotations()[0][0]).value())
|
||||||
.isEqualTo("X-Internal-Service-Token");
|
.isEqualTo("X-Internal-Service-Token");
|
||||||
assertThat(method.getParameterAnnotations()[3][0]).isInstanceOf(RequestParam.class);
|
assertThat(method.getParameterAnnotations()[3][0]).isInstanceOf(RequestParam.class);
|
||||||
assertThat(method.getParameterAnnotations()[4][0]).isInstanceOf(RequestParam.class);
|
assertThat(method.getParameterCount()).isEqualTo(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -33,7 +33,9 @@ class WarehouseSupplierClientContractTest {
|
|||||||
String ocean = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml"), StandardCharsets.UTF_8);
|
String ocean = Files.readString(Path.of("src/main/resources/openapi/ocean-bookings-v1.yaml"), StandardCharsets.UTF_8);
|
||||||
String truck = Files.readString(Path.of("src/main/resources/openapi/truck-appointments-v1.yaml"), StandardCharsets.UTF_8);
|
String truck = Files.readString(Path.of("src/main/resources/openapi/truck-appointments-v1.yaml"), StandardCharsets.UTF_8);
|
||||||
String customs = Files.readString(Path.of("src/main/resources/openapi/customs-v1.yaml"), StandardCharsets.UTF_8);
|
String customs = Files.readString(Path.of("src/main/resources/openapi/customs-v1.yaml"), StandardCharsets.UTF_8);
|
||||||
assertThat(ocean).contains("bookingAgentId:", "BOOKING", "INTERNATIONAL", "persisted snapshot always comes from Warehouse");
|
assertThat(ocean).contains("bookingAgentId:", "category BOOKING",
|
||||||
|
"do not use domestic/international scope", "persisted snapshot always comes from Warehouse")
|
||||||
|
.doesNotContain("BOOKING with INTERNATIONAL scope");
|
||||||
assertThat(truck).contains("carrierId:", "TRUCKING", "persisted snapshot comes from Warehouse");
|
assertThat(truck).contains("carrierId:", "TRUCKING", "persisted snapshot comes from Warehouse");
|
||||||
assertThat(customs).contains("customsAgentId", "CUSTOMS", "free text cannot bypass supplier validation");
|
assertThat(customs).contains("customsAgentId", "CUSTOMS", "free text cannot bypass supplier validation");
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -36,6 +36,9 @@ class PdaConvergenceOpenApiContractTest {
|
|||||||
.contains("evidence identity is taskId+inboundId+loadingPlanId+containerBookingId")
|
.contains("evidence identity is taskId+inboundId+loadingPlanId+containerBookingId")
|
||||||
.contains("Current fl_inbound_order.version for inboundId")
|
.contains("Current fl_inbound_order.version for inboundId")
|
||||||
.contains("Compatibility alias of inboundVersion from the same fl_inbound_order row")
|
.contains("Compatibility alias of inboundVersion from the same fl_inbound_order row")
|
||||||
|
.contains("sourceType", "recordStatus", "mergedIntoContainerBookingId")
|
||||||
|
.contains("Task detail is served only from the ACTIVE container booking")
|
||||||
|
.contains("Enforced by the server-side pending-task query")
|
||||||
.contains("CompleteLoadingRequest");
|
.contains("CompleteLoadingRequest");
|
||||||
|
|
||||||
String taskLookup = yaml.substring(
|
String taskLookup = yaml.substring(
|
||||||
@@ -47,6 +50,10 @@ class PdaConvergenceOpenApiContractTest {
|
|||||||
assertThat(taskLookup)
|
assertThat(taskLookup)
|
||||||
.contains("name: cabinetNo", "name: loadingPlanId")
|
.contains("name: cabinetNo", "name: loadingPlanId")
|
||||||
.doesNotContain("name: containerNo");
|
.doesNotContain("name: containerNo");
|
||||||
|
String taskDetail = yaml.substring(
|
||||||
|
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/{taskId}:"),
|
||||||
|
yaml.indexOf(" /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/claim:"));
|
||||||
|
assertThat(taskDetail).contains("#/components/schemas/Task", "ACTIVE container lifecycle fields");
|
||||||
assertThat(batchRequest)
|
assertThat(batchRequest)
|
||||||
.contains("loadingPlanId:", "containerBookingId:")
|
.contains("loadingPlanId:", "containerBookingId:")
|
||||||
.doesNotContain("containerNo:");
|
.doesNotContain("containerNo:");
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class CustomsServiceTest {
|
|||||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||||
ReflectionTestUtils.setField(service, "internalToken", "token");
|
ReflectionTestUtils.setField(service, "internalToken", "token");
|
||||||
when(suppliers.customs(anyLong(), anyString()))
|
when(suppliers.customs(anyLong(), anyString()))
|
||||||
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", null, "ACTIVE"));
|
.thenReturn(new com.baiyue.firstleg.client.WarehouseSupplierClient.Snapshot(88L, "线下报关代理", "CUSTOMS", "ACTIVE"));
|
||||||
when(repository.completeOperation(anyLong(), anyString(), anyLong())).thenReturn(1);
|
when(repository.completeOperation(anyLong(), anyString(), anyLong())).thenReturn(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
@@ -29,6 +29,9 @@ class LoadingPickPdaTaskSqlContractTest {
|
|||||||
assertThat(sql)
|
assertThat(sql)
|
||||||
.contains("task.task_status = 'LOADING'")
|
.contains("task.task_status = 'LOADING'")
|
||||||
.contains("booking.record_status = 'ACTIVE'")
|
.contains("booking.record_status = 'ACTIVE'")
|
||||||
|
.contains("booking.source_type AS sourceType")
|
||||||
|
.contains("booking.record_status AS recordStatus")
|
||||||
|
.contains("booking.merged_into_container_booking_id AS mergedIntoContainerBookingId")
|
||||||
.contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
|
.contains("assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId}")
|
||||||
.contains("booking.warehouse_id AS warehouseId")
|
.contains("booking.warehouse_id AS warehouseId")
|
||||||
.doesNotContain("tenantId");
|
.doesNotContain("tenantId");
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class ContainerServiceTest {
|
|||||||
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
|
when(warehouses.active(anyString(), anyLong(), anyLong())).thenAnswer(invocation ->
|
||||||
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
|
activeWarehouse(invocation.getArgument(2, Long.class), "DOMESTIC", "上海中转仓"));
|
||||||
when(suppliers.trucking(202L, "拖车公司A"))
|
when(suppliers.trucking(202L, "拖车公司A"))
|
||||||
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", null, "ACTIVE"));
|
.thenReturn(new WarehouseSupplierClient.Snapshot(202L, "拖车公司A", "TRUCKING", "ACTIVE"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||||
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||||
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
|
||||||
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
import com.baiyue.firstleg.loading.mapper.LoadingInboundMapper;
|
||||||
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
|
||||||
@@ -30,6 +31,7 @@ import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEn
|
|||||||
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.mockito.InOrder;
|
import org.mockito.InOrder;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||||
@@ -70,6 +72,16 @@ class LoadingPickServiceTest {
|
|||||||
@Resource
|
@Resource
|
||||||
private LoadingPickService service;
|
private LoadingPickService service;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void activeOperationalContainer() {
|
||||||
|
ContainerBookingEntity booking = new ContainerBookingEntity();
|
||||||
|
booking.setId(99L);
|
||||||
|
booking.setContainerNo("CSLU1234567");
|
||||||
|
booking.setSourceType("SIMULATION");
|
||||||
|
booking.setRecordStatus("ACTIVE");
|
||||||
|
when(smartLoadingRepository.booking(99L)).thenReturn(booking);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
|
void opensTaskOnlyAfterReleasedBookingAndRealContainerNumber() {
|
||||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||||
@@ -212,6 +224,7 @@ class LoadingPickServiceTest {
|
|||||||
task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
|
task.setContainerBookingId(40L); task.setCabinetNo("CAB-001");
|
||||||
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
var booking = new com.baiyue.firstleg.entity.ContainerBookingEntity();
|
||||||
booking.setId(40L); booking.setContainerNo("MSCU-NEW");
|
booking.setId(40L); booking.setContainerNo("MSCU-NEW");
|
||||||
|
booking.setSourceType("SIMULATION"); booking.setRecordStatus("ACTIVE");
|
||||||
when(repository.taskByCabinet("CAB-001", 101L)).thenReturn(task);
|
when(repository.taskByCabinet("CAB-001", 101L)).thenReturn(task);
|
||||||
when(repository.taskByPlan(10L, 101L)).thenReturn(task);
|
when(repository.taskByPlan(10L, 101L)).thenReturn(task);
|
||||||
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
when(smartLoadingRepository.booking(40L)).thenReturn(booking);
|
||||||
@@ -224,6 +237,53 @@ class LoadingPickServiceTest {
|
|||||||
assertThat(byCabinet.id()).isEqualTo(20L);
|
assertThat(byCabinet.id()).isEqualTo(20L);
|
||||||
assertThat(byPlan.id()).isEqualTo(20L);
|
assertThat(byPlan.id()).isEqualTo(20L);
|
||||||
assertThat(byCabinet.currentContainerNo()).isEqualTo("MSCU-NEW");
|
assertThat(byCabinet.currentContainerNo()).isEqualTo("MSCU-NEW");
|
||||||
|
assertThat(byCabinet.sourceType()).isEqualTo("SIMULATION");
|
||||||
|
assertThat(byCabinet.recordStatus()).isEqualTo("ACTIVE");
|
||||||
|
assertThat(byCabinet.mergedIntoContainerBookingId()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void pendingTasksExposeServerFilteredContainerLifecycle() {
|
||||||
|
var row = new com.baiyue.firstleg.mapper.LoadingPickTaskMapper.PendingTaskRow();
|
||||||
|
row.setTaskId(20L);
|
||||||
|
row.setLoadingPlanId(10L);
|
||||||
|
row.setContainerBookingId(99L);
|
||||||
|
row.setCabinetNo("CAB-001");
|
||||||
|
row.setContainerNo("MSCU-001");
|
||||||
|
row.setSourceType("SIMULATION");
|
||||||
|
row.setRecordStatus("ACTIVE");
|
||||||
|
row.setMergedIntoContainerBookingId(null);
|
||||||
|
row.setWarehouseId(101L);
|
||||||
|
row.setTaskStatus("LOADING");
|
||||||
|
row.setWorkflowStatus("UNASSIGNED");
|
||||||
|
row.setVersion(0);
|
||||||
|
row.setAssignmentVersion(0);
|
||||||
|
row.setItemCount(1);
|
||||||
|
row.setPendingItemCount(1);
|
||||||
|
when(repository.pendingTasks(9L, null)).thenReturn(List.of(row));
|
||||||
|
|
||||||
|
var result = service.pendingTasks(9L, null).get(0);
|
||||||
|
|
||||||
|
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
||||||
|
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
||||||
|
assertThat(result.mergedIntoContainerBookingId()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void taskDetailRejectsMergedContainerSource() {
|
||||||
|
LoadingPickTaskEntity task = task();
|
||||||
|
ContainerBookingEntity merged = new ContainerBookingEntity();
|
||||||
|
merged.setId(99L);
|
||||||
|
merged.setSourceType("OCEAN_BOOKING");
|
||||||
|
merged.setRecordStatus("MERGED");
|
||||||
|
merged.setMergedIntoContainerBookingId(88L);
|
||||||
|
when(repository.task(20L)).thenReturn(task);
|
||||||
|
when(smartLoadingRepository.booking(99L)).thenReturn(merged);
|
||||||
|
when(repository.items(20L, 101L)).thenReturn(List.of());
|
||||||
|
when(repository.cartonLabels(20L)).thenReturn(List.of());
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.task(20L, 101L))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("已非有效存续记录");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -345,6 +405,9 @@ class LoadingPickServiceTest {
|
|||||||
assertThat(result.loadingVersion()).isEqualTo(7);
|
assertThat(result.loadingVersion()).isEqualTo(7);
|
||||||
assertThat(result.loadingPlanId()).isEqualTo(10L);
|
assertThat(result.loadingPlanId()).isEqualTo(10L);
|
||||||
assertThat(result.containerBookingId()).isEqualTo(99L);
|
assertThat(result.containerBookingId()).isEqualTo(99L);
|
||||||
|
assertThat(result.sourceType()).isEqualTo("SIMULATION");
|
||||||
|
assertThat(result.recordStatus()).isEqualTo("ACTIVE");
|
||||||
|
assertThat(result.mergedIntoContainerBookingId()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+16
-6
@@ -34,19 +34,29 @@ class WarehouseSupplierSnapshotServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvesActiveBookingSupplierWithFixedScopeAndServerName() {
|
void resolvesActiveBookingSupplierByCategoryWithoutScopeAndUsesServerName() {
|
||||||
when(client.get("internal-token", 7L, true, "BOOKING", "INTERNATIONAL"))
|
when(client.get("internal-token", 7L, true, "BOOKING"))
|
||||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "INTERNATIONAL", "ACTIVE")));
|
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(7L, "海运代理", "BOOKING", "ACTIVE")));
|
||||||
var snapshot = service.booking(7L, "海运代理");
|
var snapshot = service.booking(7L, "海运代理");
|
||||||
assertThat(snapshot.name()).isEqualTo("海运代理");
|
assertThat(snapshot.name()).isEqualTo("海运代理");
|
||||||
verify(client).get("internal-token", 7L, true, "BOOKING", "INTERNATIONAL");
|
verify(client).get("internal-token", 7L, true, "BOOKING");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsClientNameThatDiffersFromWarehouseSnapshot() {
|
void rejectsClientNameThatDiffersFromWarehouseSnapshot() {
|
||||||
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS"), isNull()))
|
when(client.get(anyString(), eq(9L), eq(true), eq("CUSTOMS")))
|
||||||
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", null, "ACTIVE")));
|
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(9L, "真实报关行", "CUSTOMS", "ACTIVE")));
|
||||||
assertThatThrownBy(() -> service.customs(9L, "伪造名称"))
|
assertThatThrownBy(() -> service.customs(9L, "伪造名称"))
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
|
.isInstanceOf(BizException.class).hasMessageContaining("不一致");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsSupplierSnapshotWithWrongCategoryOrInactiveStatus() {
|
||||||
|
when(client.get("internal-token", 7L, true, "BOOKING"))
|
||||||
|
.thenReturn(ApiResponse.success(new WarehouseSupplierClient.Snapshot(
|
||||||
|
7L, "错误代理", "TRUCKING", "ACTIVE")));
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.booking(7L, null))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("类别不匹配");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
const supplierFixture = readFileSync(new URL('./suppliers.mjs', import.meta.url), 'utf8')
|
||||||
|
const financeFixture = readFileSync(new URL('./finance-full-fixtures.mjs', import.meta.url), 'utf8')
|
||||||
|
const supplierOpenapi = readFileSync(new URL('../../warehouse/src/main/resources/openapi/supplier-v1.yaml', import.meta.url), 'utf8')
|
||||||
|
const firstlegSnapshot = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/WarehouseSupplierSnapshotService.java', import.meta.url), 'utf8')
|
||||||
|
const firstlegClient = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/client/WarehouseSupplierClient.java', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
test('booking supplier fixtures keep ACTIVE and BOOKING but never invent a geographic scope gate', () => {
|
||||||
|
assert.match(supplierFixture, /category:'BOOKING'/)
|
||||||
|
assert.match(supplierFixture, /status:'ACTIVE'/)
|
||||||
|
assert.doesNotMatch(supplierFixture, /scope\s*:/)
|
||||||
|
|
||||||
|
const supplierBody = financeFixture.slice(financeFixture.indexOf('export function buildSupplierBody'),
|
||||||
|
financeFixture.indexOf('async function createOrder'))
|
||||||
|
assert.match(supplierBody, /category,/)
|
||||||
|
assert.match(supplierBody, /status: 'ACTIVE'/)
|
||||||
|
assert.doesNotMatch(supplierBody, /scope|DOMESTIC|INTERNATIONAL/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Warehouse supplier contract filters by active category without scope in query, save or snapshot', () => {
|
||||||
|
assert.match(supplierOpenapi, /name: requireActive/)
|
||||||
|
assert.match(supplierOpenapi, /name: category/)
|
||||||
|
assert.match(supplierOpenapi, /required: \[supplierId, name, category, status\]/)
|
||||||
|
assert.doesNotMatch(supplierOpenapi, /\bScope\b|name: scope|\bscope:/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Firstleg booking supplier snapshot requires ACTIVE BOOKING and has no domestic/international argument', () => {
|
||||||
|
assert.match(firstlegSnapshot, /return resolve\(supplierId, suppliedName, "BOOKING"\)/)
|
||||||
|
assert.match(firstlegSnapshot, /!"ACTIVE"\.equals\(response\.data\(\)\.status\(\)\)/)
|
||||||
|
assert.match(firstlegSnapshot, /!category\.equals\(response\.data\(\)\.category\(\)\)/)
|
||||||
|
assert.doesNotMatch(firstlegSnapshot, /"INTERNATIONAL"|"DOMESTIC"|String scope/)
|
||||||
|
assert.doesNotMatch(firstlegClient, /String scope|String category,\s*String scope/s)
|
||||||
|
})
|
||||||
@@ -45,7 +45,7 @@ test('PAYABLE fee selection uses the formal feeCode and feeTypeName fields', ()
|
|||||||
test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => {
|
test('Finance PAYABLE fixture creates an isolated ACTIVE supplier through the formal API', () => {
|
||||||
const body = buildSupplierBody('ADMIN-API-20260815-FINANCE', 'BOOKING')
|
const body = buildSupplierBody('ADMIN-API-20260815-FINANCE', 'BOOKING')
|
||||||
assert.equal(body.category, 'BOOKING')
|
assert.equal(body.category, 'BOOKING')
|
||||||
assert.equal(body.scope, 'DOMESTIC')
|
assert.equal(Object.hasOwn(body, 'scope'), false)
|
||||||
assert.equal(body.status, 'ACTIVE')
|
assert.equal(body.status, 'ACTIVE')
|
||||||
assert.match(body.name, /^财务验收供应商-/)
|
assert.match(body.name, /^财务验收供应商-/)
|
||||||
assert.match(source, /POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
|
assert.match(source, /POST', '\/api\/platform\/v1\/suppliers\?tenantId=0'/)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ export function buildSupplierBody(marker, category) {
|
|||||||
return {
|
return {
|
||||||
name: `财务验收供应商-${suffix}`,
|
name: `财务验收供应商-${suffix}`,
|
||||||
category,
|
category,
|
||||||
...(category === 'BOOKING' ? { scope: 'DOMESTIC' } : {}),
|
|
||||||
region: '宁波', legalRepresentative: '财务验收法人', contactName: '财务验收联系人',
|
region: '宁波', legalRepresentative: '财务验收法人', contactName: '财务验收联系人',
|
||||||
phone: '13800000000', email: `finance-supplier-${suffix || 'fixture'}@example.com`,
|
phone: '13800000000', email: `finance-supplier-${suffix || 'fixture'}@example.com`,
|
||||||
address: '财务验收地址', businessLicenseNo: `FIN${suffix}`.slice(0, 64),
|
address: '财务验收地址', businessLicenseNo: `FIN${suffix}`.slice(0, 64),
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ export async function run(context) {
|
|||||||
`status=${approved?.customsCase?.status || 'missing'}, version=${approved?.customsCase?.version ?? 'missing'}`, OPTIONS)
|
`status=${approved?.customsCase?.status || 'missing'}, version=${approved?.customsCase?.version ?? 'missing'}`, OPTIONS)
|
||||||
|
|
||||||
const supplierCreateResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', {
|
const supplierCreateResponse = await context.request('POST', '/api/platform/v1/suppliers?tenantId=0', {
|
||||||
name: `报关代理-${context.marker}`.slice(0, 128), category: 'CUSTOMS', scope: null, region: '深圳',
|
name: `报关代理-${context.marker}`.slice(0, 128), category: 'CUSTOMS', region: '深圳',
|
||||||
legalRepresentative: '验收法人', contactName: '报关联系人', phone: '13800000000',
|
legalRepresentative: '验收法人', contactName: '报关联系人', phone: '13800000000',
|
||||||
email: 'customs-acceptance@example.com', address: '深圳市验收地址',
|
email: 'customs-acceptance@example.com', address: '深圳市验收地址',
|
||||||
businessLicenseNo: `CUSTOMS${context.marker.replace(/\D/g, '')}`.slice(0, 64), licenseFileId: null,
|
businessLicenseNo: `CUSTOMS${context.marker.replace(/\D/g, '')}`.slice(0, 64), licenseFileId: null,
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ const orchestrator=readFileSync(new URL('../admin-test-env-acceptance.mjs',impor
|
|||||||
const openapi=readFileSync(new URL('../../warehouse/src/main/resources/openapi/supplier-v1.yaml',import.meta.url),'utf8')
|
const openapi=readFileSync(new URL('../../warehouse/src/main/resources/openapi/supplier-v1.yaml',import.meta.url),'utf8')
|
||||||
|
|
||||||
test('supplier acceptance covers full CRUD-like lifecycle and boundary failures',()=>{
|
test('supplier acceptance covers full CRUD-like lifecycle and boundary failures',()=>{
|
||||||
for(const marker of ['列表分页与订舱筛选','查询四类供应商统计','新增供应商','查询供应商详情','编辑供应商','停用供应商','重复名称返回冲突','非订舱分类携带scope被拒绝','tenantId非0被拒绝','客户账号调用平台供应商接口被拒绝','requestId','traceId']) assert.match(source,new RegExp(marker))
|
for(const marker of ['列表分页与订舱筛选','查询四类供应商统计','新增供应商','查询供应商详情','编辑供应商','停用供应商','重复名称返回冲突','tenantId非0被拒绝','客户账号调用平台供应商接口被拒绝','requestId','traceId']) assert.match(source,new RegExp(marker))
|
||||||
|
assert.doesNotMatch(source,/scope\s*:/)
|
||||||
})
|
})
|
||||||
test('supplier module is registered in the default full acceptance orchestration',()=>assert.match(orchestrator,/CONFIGURED_MODULES = \[[^\]]*'suppliers'/s))
|
test('supplier module is registered in the default full acceptance orchestration',()=>assert.match(orchestrator,/CONFIGURED_MODULES = \[[^\]]*'suppliers'/s))
|
||||||
test('supplier fixture is retained only as inactive because API has no delete contract',()=>{assert.match(source,/finalStatus:inactive\?'INACTIVE'/);assert.doesNotMatch(source,/context\.request\('DELETE'/)})
|
test('supplier fixture is retained only as inactive because API has no delete contract',()=>{assert.match(source,/finalStatus:inactive\?'INACTIVE'/);assert.doesNotMatch(source,/context\.request\('DELETE'/)})
|
||||||
test('supplier OpenAPI exposes the internal active category and booking scope validation contract',()=>{
|
test('supplier OpenAPI exposes the internal active category validation contract without scope',()=>{
|
||||||
for(const marker of ['/internal/warehouse/v1/suppliers/{supplierId}','X-Internal-Service-Token','requireActive','InternalSnapshot','SupplierPageResponse','platform:supplier:list']) assert.match(openapi,new RegExp(marker.replace(/[{}]/g,'\\$&')))
|
for(const marker of ['/internal/warehouse/v1/suppliers/{supplierId}','X-Internal-Service-Token','requireActive','InternalSnapshot','SupplierPageResponse','platform:supplier:list']) assert.match(openapi,new RegExp(marker.replace(/[{}]/g,'\\$&')))
|
||||||
|
assert.doesNotMatch(openapi,/\bscope\b/)
|
||||||
assert.doesNotMatch(openapi,/platform:supplier:view/)
|
assert.doesNotMatch(openapi,/platform:supplier:view/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function records(page) { return Array.isArray(page?.records) ? page.records : []
|
|||||||
function id(value) { return value == null ? '' : String(value) }
|
function id(value) { return value == null ? '' : String(value) }
|
||||||
function traced(response) { return Boolean(response?.requestId && response?.traceId) }
|
function traced(response) { return Boolean(response?.requestId && response?.traceId) }
|
||||||
function query(values = {}) { const p = new URLSearchParams(); for (const [k,v] of Object.entries(values)) if (v !== '' && v != null) p.set(k,String(v)); return `${BASE}${p.size ? `?${p}` : ''}` }
|
function query(values = {}) { const p = new URLSearchParams(); for (const [k,v] of Object.entries(values)) if (v !== '' && v != null) p.set(k,String(v)); return `${BASE}${p.size ? `?${p}` : ''}` }
|
||||||
function payload(marker, overrides = {}) { return { name:`供应商验收-${marker}`,category:'BOOKING',scope:'DOMESTIC',region:'宁波',legalRepresentative:'验收法人',contactName:'验收联系人',phone:'13800000000',email:'supplier-acceptance@example.com',address:'验收地址',businessLicenseNo:`ACPT${marker}`.slice(0,64),licenseFileId:null,status:'ACTIVE',settlementPeriod:'MONTHLY_30',remark:marker,...overrides } }
|
function payload(marker, overrides = {}) { return { name:`供应商验收-${marker}`,category:'BOOKING',region:'宁波',legalRepresentative:'验收法人',contactName:'验收联系人',phone:'13800000000',email:'supplier-acceptance@example.com',address:'验收地址',businessLicenseNo:`ACPT${marker}`.slice(0,64),licenseFileId:null,status:'ACTIVE',settlementPeriod:'MONTHLY_30',remark:marker,...overrides } }
|
||||||
|
|
||||||
export async function run(context) {
|
export async function run(context) {
|
||||||
const marker=context.marker.replace(/[^0-9A-Za-z]/g,'').slice(-20)
|
const marker=context.marker.replace(/[^0-9A-Za-z]/g,'').slice(-20)
|
||||||
@@ -31,14 +31,11 @@ export async function run(context) {
|
|||||||
try {
|
try {
|
||||||
const detailResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
const detailResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
||||||
const detail=context.apiSuccess(MODULE,'查询供应商详情',detailResponse,'',OPTIONS)
|
const detail=context.apiSuccess(MODULE,'查询供应商详情',detailResponse,'',OPTIONS)
|
||||||
context.assert(MODULE,'详情字段与平台作用域正确',detailResponse,id(detail?.id)===supplierId&&id(detail?.tenantId)==='0'&&detail?.name===createBody.name&&detail?.scope==='DOMESTIC',`id=${id(detail?.id)}, tenantId=${id(detail?.tenantId)}`,OPTIONS)
|
context.assert(MODULE,'详情字段与平台作用域正确',detailResponse,id(detail?.id)===supplierId&&id(detail?.tenantId)==='0'&&detail?.name===createBody.name&&!Object.hasOwn(detail,'scope'),`id=${id(detail?.id)}, tenantId=${id(detail?.tenantId)}, scopeExposed=${Object.hasOwn(detail||{},'scope')}`,OPTIONS)
|
||||||
|
|
||||||
const duplicateResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}D`,{name:createBody.name}))
|
const duplicateResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}D`,{name:createBody.name}))
|
||||||
context.assert(MODULE,'重复名称返回冲突并保留链路ID',duplicateResponse,code(duplicateResponse,'A0409')&&traced(duplicateResponse),`code=${duplicateResponse.body?.code}, requestId=${duplicateResponse.requestId||'missing'}, traceId=${duplicateResponse.traceId||'missing'}`,OPTIONS)
|
context.assert(MODULE,'重复名称返回冲突并保留链路ID',duplicateResponse,code(duplicateResponse,'A0409')&&traced(duplicateResponse),`code=${duplicateResponse.body?.code}, requestId=${duplicateResponse.requestId||'missing'}, traceId=${duplicateResponse.traceId||'missing'}`,OPTIONS)
|
||||||
|
|
||||||
const scopeResponse=await context.request('POST',`${BASE}?tenantId=0`,payload(`${marker}S`,{name:`非法范围-${marker}`,category:'CUSTOMS',scope:'DOMESTIC'}))
|
|
||||||
context.assert(MODULE,'非订舱分类携带scope被拒绝',scopeResponse,code(scopeResponse,'A0422')&&traced(scopeResponse),`code=${scopeResponse.body?.code}, requestId=${scopeResponse.requestId||'missing'}, traceId=${scopeResponse.traceId||'missing'}`,OPTIONS)
|
|
||||||
|
|
||||||
const tenantResponse=await context.request('GET',query({tenantId:1,page:1,size:20}))
|
const tenantResponse=await context.request('GET',query({tenantId:1,page:1,size:20}))
|
||||||
context.assert(MODULE,'tenantId非0被拒绝',tenantResponse,code(tenantResponse,'A0400')&&traced(tenantResponse),`code=${tenantResponse.body?.code}, requestId=${tenantResponse.requestId||'missing'}, traceId=${tenantResponse.traceId||'missing'}`,OPTIONS)
|
context.assert(MODULE,'tenantId非0被拒绝',tenantResponse,code(tenantResponse,'A0400')&&traced(tenantResponse),`code=${tenantResponse.body?.code}, requestId=${tenantResponse.requestId||'missing'}, traceId=${tenantResponse.traceId||'missing'}`,OPTIONS)
|
||||||
|
|
||||||
@@ -47,12 +44,12 @@ export async function run(context) {
|
|||||||
const forbidden=merchantToken?await context.request('GET',query({tenantId:0,page:1,size:20}),undefined,merchantToken):merchantLogin
|
const forbidden=merchantToken?await context.request('GET',query({tenantId:0,page:1,size:20}),undefined,merchantToken):merchantLogin
|
||||||
context.assert(MODULE,'客户账号调用平台供应商接口被拒绝',forbidden,Boolean(merchantToken)&&(forbidden.http===403||code(forbidden,'A0403'))&&traced(forbidden),`HTTP=${forbidden.http}, code=${forbidden.body?.code??'missing'}, requestId=${forbidden.requestId||'missing'}, traceId=${forbidden.traceId||'missing'}`,OPTIONS)
|
context.assert(MODULE,'客户账号调用平台供应商接口被拒绝',forbidden,Boolean(merchantToken)&&(forbidden.http===403||code(forbidden,'A0403'))&&traced(forbidden),`HTTP=${forbidden.http}, code=${forbidden.body?.code??'missing'}, requestId=${forbidden.requestId||'missing'}, traceId=${forbidden.traceId||'missing'}`,OPTIONS)
|
||||||
|
|
||||||
const updateBody=payload(marker,{name:`供应商验收已编辑-${marker}`,scope:'INTERNATIONAL',contactName:'编辑联系人',settlementPeriod:'MONTHLY_45'})
|
const updateBody=payload(marker,{name:`供应商验收已编辑-${marker}`,contactName:'编辑联系人',settlementPeriod:'MONTHLY_45'})
|
||||||
const updateResponse=await context.request('PUT',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`,updateBody)
|
const updateResponse=await context.request('PUT',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`,updateBody)
|
||||||
context.apiSuccess(MODULE,'编辑供应商',updateResponse,'',OPTIONS)
|
context.apiSuccess(MODULE,'编辑供应商',updateResponse,'',OPTIONS)
|
||||||
const editedResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
const editedResponse=await context.request('GET',`${BASE}/${encodeURIComponent(supplierId)}?tenantId=0`)
|
||||||
const edited=context.apiSuccess(MODULE,'回查编辑结果',editedResponse,'',OPTIONS)
|
const edited=context.apiSuccess(MODULE,'回查编辑结果',editedResponse,'',OPTIONS)
|
||||||
context.assert(MODULE,'编辑字段已持久化',editedResponse,edited?.name===updateBody.name&&edited?.scope==='INTERNATIONAL'&&edited?.contactName==='编辑联系人'&&edited?.settlementPeriod==='MONTHLY_45',`name=${edited?.name||'missing'}`,OPTIONS)
|
context.assert(MODULE,'编辑字段已持久化',editedResponse,edited?.name===updateBody.name&&!Object.hasOwn(edited,'scope')&&edited?.contactName==='编辑联系人'&&edited?.settlementPeriod==='MONTHLY_45',`name=${edited?.name||'missing'}, scopeExposed=${Object.hasOwn(edited||{},'scope')}`,OPTIONS)
|
||||||
|
|
||||||
const disableResponse=await context.request('PATCH',`${BASE}/${encodeURIComponent(supplierId)}/status?tenantId=0`,{status:'INACTIVE'})
|
const disableResponse=await context.request('PATCH',`${BASE}/${encodeURIComponent(supplierId)}/status?tenantId=0`,{status:'INACTIVE'})
|
||||||
context.apiSuccess(MODULE,'停用供应商',disableResponse,'',OPTIONS); inactive=ok(disableResponse)
|
context.apiSuccess(MODULE,'停用供应商',disableResponse,'',OPTIONS); inactive=ok(disableResponse)
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export function truckAppointmentPayload(context, suffix, identity, version = 0)
|
|||||||
|
|
||||||
async function ensureTruckSupplier(context, suffix) {
|
async function ensureTruckSupplier(context, suffix) {
|
||||||
const response = await request(context, 'POST', SUPPLIER_BASE, {
|
const response = await request(context, 'POST', SUPPLIER_BASE, {
|
||||||
name: `拖车供应商-${suffix}`.slice(0, 128), category: 'TRUCKING', scope: null,
|
name: `拖车供应商-${suffix}`.slice(0, 128), category: 'TRUCKING',
|
||||||
region: '深圳', legalRepresentative: '验收法人', contactName: '拖车联系人',
|
region: '深圳', legalRepresentative: '验收法人', contactName: '拖车联系人',
|
||||||
phone: '13800000000', email: 'trucking-acceptance@example.com', address: '深圳市验收地址',
|
phone: '13800000000', email: 'trucking-acceptance@example.com', address: '深圳市验收地址',
|
||||||
businessLicenseNo: `TRUCK${suffix}`.slice(0, 64), licenseFileId: null,
|
businessLicenseNo: `TRUCK${suffix}`.slice(0, 64), licenseFileId: null,
|
||||||
|
|||||||
+2
-4
@@ -22,11 +22,9 @@ public class InternalSupplierController {
|
|||||||
@GetMapping("/{supplierId}")
|
@GetMapping("/{supplierId}")
|
||||||
public ApiResponse<InternalSnapshot> get(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
public ApiResponse<InternalSnapshot> get(@RequestHeader(value="X-Internal-Service-Token", required=false) String token,
|
||||||
@PathVariable long supplierId, @RequestParam(defaultValue="false") boolean requireActive,
|
@PathVariable long supplierId, @RequestParam(defaultValue="false") boolean requireActive,
|
||||||
@RequestParam(required=false) String category, @RequestParam(required=false) String scope) {
|
@RequestParam(required=false) String category) {
|
||||||
if (token == null || !MessageDigest.isEqual(expected, token.getBytes(StandardCharsets.UTF_8))) throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
if (token == null || !MessageDigest.isEqual(expected, token.getBytes(StandardCharsets.UTF_8))) throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
||||||
if (category != null && !category.matches("BOOKING|CUSTOMS|TRUCKING|CERTIFICATE")) throw new BizException(ResultCode.VALIDATION_FAILED, "供应商类别无效");
|
if (category != null && !category.matches("BOOKING|CUSTOMS|TRUCKING|CERTIFICATE")) throw new BizException(ResultCode.VALIDATION_FAILED, "供应商类别无效");
|
||||||
if (scope != null && !scope.matches("DOMESTIC|INTERNATIONAL")) throw new BizException(ResultCode.VALIDATION_FAILED, "订舱范围无效");
|
return ApiResponse.success(service.internalSnapshot(supplierId, requireActive, category));
|
||||||
if (scope != null && !"BOOKING".equals(category)) throw new BizException(ResultCode.VALIDATION_FAILED, "scope仅适用于BOOKING");
|
|
||||||
return ApiResponse.success(service.internalSnapshot(supplierId, requireActive, category, scope));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ public final class SupplierDtos {
|
|||||||
|
|
||||||
public record PageQuery(@Min(1) Integer page, @Min(1) @Max(200) Integer size,
|
public record PageQuery(@Min(1) Integer page, @Min(1) @Max(200) Integer size,
|
||||||
@Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String category,
|
@Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String category,
|
||||||
@Pattern(regexp = "DOMESTIC|INTERNATIONAL") String scope,
|
|
||||||
@Size(max = 80) String region,
|
@Size(max = 80) String region,
|
||||||
@Pattern(regexp = "ACTIVE|INACTIVE") String status,
|
@Pattern(regexp = "ACTIVE|INACTIVE") String status,
|
||||||
String keyword) {
|
String keyword) {
|
||||||
@@ -26,7 +25,6 @@ public final class SupplierDtos {
|
|||||||
public record SaveRequest(
|
public record SaveRequest(
|
||||||
@NotBlank @Size(max = 160) String name,
|
@NotBlank @Size(max = 160) String name,
|
||||||
@NotBlank @Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String category,
|
@NotBlank @Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String category,
|
||||||
@Pattern(regexp = "DOMESTIC|INTERNATIONAL") String scope,
|
|
||||||
@NotBlank @Size(max = 80) String region,
|
@NotBlank @Size(max = 80) String region,
|
||||||
@NotBlank @Size(max = 80) String legalRepresentative,
|
@NotBlank @Size(max = 80) String legalRepresentative,
|
||||||
@NotBlank @Size(max = 80) String contactName,
|
@NotBlank @Size(max = 80) String contactName,
|
||||||
@@ -42,5 +40,5 @@ public final class SupplierDtos {
|
|||||||
public record StatusRequest(@NotNull @Pattern(regexp = "ACTIVE|INACTIVE") String status) {}
|
public record StatusRequest(@NotNull @Pattern(regexp = "ACTIVE|INACTIVE") String status) {}
|
||||||
|
|
||||||
public record Stats(long booking, long customs, long trucking, long certificate) {}
|
public record Stats(long booking, long customs, long trucking, long certificate) {}
|
||||||
public record InternalSnapshot(long supplierId, String name, String category, String scope, String status) {}
|
public record InternalSnapshot(long supplierId, String name, String category, String status) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ public class SupplierEntity extends BaseEntity {
|
|||||||
private Long tenantId;
|
private Long tenantId;
|
||||||
private String name;
|
private String name;
|
||||||
private String category;
|
private String category;
|
||||||
private String scope;
|
|
||||||
private String region;
|
private String region;
|
||||||
private String legalRepresentative;
|
private String legalRepresentative;
|
||||||
private String contactName;
|
private String contactName;
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ public class SupplierRepository {
|
|||||||
public PageResult<SupplierEntity> page(long tenantId, PageQuery query) {
|
public PageResult<SupplierEntity> page(long tenantId, PageQuery query) {
|
||||||
LambdaQueryWrapper<SupplierEntity> w = base(tenantId)
|
LambdaQueryWrapper<SupplierEntity> w = base(tenantId)
|
||||||
.eq(StringUtils.hasText(query.category()), SupplierEntity::getCategory, query.category())
|
.eq(StringUtils.hasText(query.category()), SupplierEntity::getCategory, query.category())
|
||||||
.eq(StringUtils.hasText(query.scope()), SupplierEntity::getScope, query.scope())
|
|
||||||
.eq(StringUtils.hasText(query.region()), SupplierEntity::getRegion, query.region())
|
.eq(StringUtils.hasText(query.region()), SupplierEntity::getRegion, query.region())
|
||||||
.eq(StringUtils.hasText(query.status()), SupplierEntity::getStatus, query.status())
|
.eq(StringUtils.hasText(query.status()), SupplierEntity::getStatus, query.status())
|
||||||
.and(StringUtils.hasText(query.keyword()), c -> c.like(SupplierEntity::getName, query.keyword())
|
.and(StringUtils.hasText(query.keyword()), c -> c.like(SupplierEntity::getName, query.keyword())
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ public class SupplierService {
|
|||||||
@Resource private SupplierRepository repository;
|
@Resource private SupplierRepository repository;
|
||||||
|
|
||||||
public PageResult<SupplierEntity> page(PageQuery query) {
|
public PageResult<SupplierEntity> page(PageQuery query) {
|
||||||
if (query.category() == null && query.scope() != null) throw new BizException(ResultCode.VALIDATION_FAILED, "scope仅适用于订舱供应商");
|
|
||||||
if (query.category() != null && !"BOOKING".equals(query.category()) && query.scope() != null) throw new BizException(ResultCode.VALIDATION_FAILED, "scope仅适用于订舱供应商");
|
|
||||||
return repository.page(PUBLIC_TENANT_ID, query);
|
return repository.page(PUBLIC_TENANT_ID, query);
|
||||||
}
|
}
|
||||||
public SupplierEntity detail(long id) { return owned(id); }
|
public SupplierEntity detail(long id) { return owned(id); }
|
||||||
@@ -28,7 +26,6 @@ public class SupplierService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@BusinessAudit(event = "SUPPLIER_CREATE", resourceType = "SUPPLIER", resourceId = "#id", description = "新增供应商", captureBefore = false)
|
@BusinessAudit(event = "SUPPLIER_CREATE", resourceType = "SUPPLIER", resourceId = "#id", description = "新增供应商", captureBefore = false)
|
||||||
public Long create(long id, SaveRequest request) {
|
public Long create(long id, SaveRequest request) {
|
||||||
validateScope(request);
|
|
||||||
ensureName(request.name(), null);
|
ensureName(request.name(), null);
|
||||||
SupplierEntity entity = new SupplierEntity(); entity.setId(id); entity.setTenantId(0L); copy(entity, request);
|
SupplierEntity entity = new SupplierEntity(); entity.setId(id); entity.setTenantId(0L); copy(entity, request);
|
||||||
try { repository.insert(entity); } catch (DataIntegrityViolationException e) { throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
try { repository.insert(entity); } catch (DataIntegrityViolationException e) { throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
||||||
@@ -38,7 +35,7 @@ public class SupplierService {
|
|||||||
@Transactional
|
@Transactional
|
||||||
@BusinessAudit(event = "SUPPLIER_UPDATE", resourceType = "SUPPLIER", resourceId = "#id", description = "编辑供应商")
|
@BusinessAudit(event = "SUPPLIER_UPDATE", resourceType = "SUPPLIER", resourceId = "#id", description = "编辑供应商")
|
||||||
public void update(long id, SaveRequest request) {
|
public void update(long id, SaveRequest request) {
|
||||||
SupplierEntity e = owned(id); validateScope(request); ensureName(request.name(), id); copy(e, request);
|
SupplierEntity e = owned(id); ensureName(request.name(), id); copy(e, request);
|
||||||
try { repository.save(e); } catch (DataIntegrityViolationException exception) { throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
try { repository.save(e); } catch (DataIntegrityViolationException exception) { throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,16 +43,14 @@ public class SupplierService {
|
|||||||
@BusinessAudit(event = "SUPPLIER_STATUS_CHANGE", resourceType = "SUPPLIER", resourceId = "#id", description = "启停供应商")
|
@BusinessAudit(event = "SUPPLIER_STATUS_CHANGE", resourceType = "SUPPLIER", resourceId = "#id", description = "启停供应商")
|
||||||
public void changeStatus(long id, StatusRequest request) { SupplierEntity e = owned(id); if (!request.status().equals(e.getStatus())) { e.setStatus(request.status()); repository.save(e); } }
|
public void changeStatus(long id, StatusRequest request) { SupplierEntity e = owned(id); if (!request.status().equals(e.getStatus())) { e.setStatus(request.status()); repository.save(e); } }
|
||||||
|
|
||||||
public InternalSnapshot internalSnapshot(long id, boolean requireActive, String category, String scope) {
|
public InternalSnapshot internalSnapshot(long id, boolean requireActive, String category) {
|
||||||
SupplierEntity e = owned(id);
|
SupplierEntity e = owned(id);
|
||||||
if (requireActive && !"ACTIVE".equals(e.getStatus())) throw new BizException(ResultCode.CONFLICT, "供应商未启用");
|
if (requireActive && !"ACTIVE".equals(e.getStatus())) throw new BizException(ResultCode.CONFLICT, "供应商未启用");
|
||||||
if (category != null && !category.equals(e.getCategory())) throw new BizException(ResultCode.VALIDATION_FAILED, "供应商类别不匹配");
|
if (category != null && !category.equals(e.getCategory())) throw new BizException(ResultCode.VALIDATION_FAILED, "供应商类别不匹配");
|
||||||
if (scope != null && (!"BOOKING".equals(e.getCategory()) || !scope.equals(e.getScope()))) throw new BizException(ResultCode.VALIDATION_FAILED, "订舱范围不匹配");
|
return new InternalSnapshot(e.getId(), e.getName(), e.getCategory(), e.getStatus());
|
||||||
return new InternalSnapshot(e.getId(), e.getName(), e.getCategory(), e.getScope(), e.getStatus());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private SupplierEntity owned(long id) { SupplierEntity e = repository.find(0L, id); if (e == null) throw new BizException(ResultCode.NOT_FOUND, "供应商不存在"); return e; }
|
private SupplierEntity owned(long id) { SupplierEntity e = repository.find(0L, id); if (e == null) throw new BizException(ResultCode.NOT_FOUND, "供应商不存在"); return e; }
|
||||||
private void ensureName(String name, Long id) { if (repository.existsName(0L, name, id)) throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
private void ensureName(String name, Long id) { if (repository.existsName(0L, name, id)) throw new BizException(ResultCode.CONFLICT, "供应商名称已存在"); }
|
||||||
private static void validateScope(SaveRequest r) { if (r.category().equals("BOOKING") != (r.scope() != null)) throw new BizException(ResultCode.VALIDATION_FAILED, "国内/国外范围仅适用于订舱供应商"); }
|
private static void copy(SupplierEntity e, SaveRequest r) { e.setName(r.name()); e.setCategory(r.category()); e.setRegion(r.region()); e.setLegalRepresentative(r.legalRepresentative()); e.setContactName(r.contactName()); e.setPhone(r.phone()); e.setEmail(r.email()); e.setAddress(r.address()); e.setBusinessLicenseNo(r.businessLicenseNo()); e.setLicenseFileId(r.licenseFileId()); e.setStatus(r.status()); e.setSettlementPeriod(r.settlementPeriod()); e.setRemark(r.remark()); }
|
||||||
private static void copy(SupplierEntity e, SaveRequest r) { e.setName(r.name()); e.setCategory(r.category()); e.setScope(r.scope()); e.setRegion(r.region()); e.setLegalRepresentative(r.legalRepresentative()); e.setContactName(r.contactName()); e.setPhone(r.phone()); e.setEmail(r.email()); e.setAddress(r.address()); e.setBusinessLicenseNo(r.businessLicenseNo()); e.setLicenseFileId(r.licenseFileId()); e.setStatus(r.status()); e.setSettlementPeriod(r.settlementPeriod()); e.setRemark(r.remark()); }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS wh_supplier (
|
|||||||
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
|
tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
|
||||||
name VARCHAR(160) NOT NULL COMMENT '供应商名称',
|
name VARCHAR(160) NOT NULL COMMENT '供应商名称',
|
||||||
category VARCHAR(20) NOT NULL COMMENT '供应商类别:BOOKING订舱、CUSTOMS报关、TRUCKING拖车、CERTIFICATE认证',
|
category VARCHAR(20) NOT NULL COMMENT '供应商类别:BOOKING订舱、CUSTOMS报关、TRUCKING拖车、CERTIFICATE认证',
|
||||||
scope VARCHAR(20) NULL COMMENT '订舱分类范围:DOMESTIC/INTERNATIONAL',
|
|
||||||
region VARCHAR(80) NOT NULL COMMENT '地区',
|
region VARCHAR(80) NOT NULL COMMENT '地区',
|
||||||
legal_representative VARCHAR(80) NOT NULL COMMENT '法人代表',
|
legal_representative VARCHAR(80) NOT NULL COMMENT '法人代表',
|
||||||
contact_name VARCHAR(80) NOT NULL COMMENT '联系人',
|
contact_name VARCHAR(80) NOT NULL COMMENT '联系人',
|
||||||
@@ -30,7 +29,6 @@ CREATE TABLE IF NOT EXISTS wh_supplier (
|
|||||||
KEY idx_wh_supplier_filter (tenant_id,category,status,region),
|
KEY idx_wh_supplier_filter (tenant_id,category,status,region),
|
||||||
CONSTRAINT chk_wh_supplier_category CHECK (category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')),
|
CONSTRAINT chk_wh_supplier_category CHECK (category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')),
|
||||||
CONSTRAINT chk_wh_supplier_tenant CHECK (tenant_id = 0),
|
CONSTRAINT chk_wh_supplier_tenant CHECK (tenant_id = 0),
|
||||||
CONSTRAINT chk_wh_supplier_scope CHECK ((category='BOOKING' AND scope IN ('DOMESTIC','INTERNATIONAL')) OR (category<>'BOOKING' AND scope IS NULL)),
|
|
||||||
CONSTRAINT chk_wh_supplier_status CHECK (status IN ('ACTIVE','INACTIVE')),
|
CONSTRAINT chk_wh_supplier_status CHECK (status IN ('ACTIVE','INACTIVE')),
|
||||||
CONSTRAINT chk_wh_supplier_settlement CHECK (settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
CONSTRAINT chk_wh_supplier_settlement CHECK (settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
||||||
@@ -46,7 +44,6 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL supplier_add_column_if_missing('scope', "VARCHAR(20) NULL COMMENT '订舱分类范围:DOMESTIC/INTERNATIONAL' AFTER category");
|
|
||||||
CALL supplier_add_column_if_missing('license_file_id', "VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID' AFTER business_license_no");
|
CALL supplier_add_column_if_missing('license_file_id', "VARCHAR(128) NULL COMMENT 'File服务营业执照文件ID' AFTER business_license_no");
|
||||||
CALL supplier_add_column_if_missing('created_by', "BIGINT UNSIGNED NULL COMMENT '创建人ID'");
|
CALL supplier_add_column_if_missing('created_by', "BIGINT UNSIGNED NULL COMMENT '创建人ID'");
|
||||||
CALL supplier_add_column_if_missing('created_at', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'");
|
CALL supplier_add_column_if_missing('created_at', "DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间'");
|
||||||
@@ -73,7 +70,6 @@ END$$
|
|||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_category', "category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_category', "category IN ('BOOKING','CUSTOMS','TRUCKING','CERTIFICATE')");
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_tenant', "tenant_id = 0");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_tenant', "tenant_id = 0");
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_scope', "(category='BOOKING' AND scope IN ('DOMESTIC','INTERNATIONAL')) OR (category<>'BOOKING' AND scope IS NULL)");
|
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_status', "status IN ('ACTIVE','INACTIVE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_status', "status IN ('ACTIVE','INACTIVE')");
|
||||||
CALL supplier_add_check_if_missing('chk_wh_supplier_settlement', "settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE')");
|
CALL supplier_add_check_if_missing('chk_wh_supplier_settlement', "settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE')");
|
||||||
DROP PROCEDURE supplier_add_check_if_missing;
|
DROP PROCEDURE supplier_add_check_if_missing;
|
||||||
@@ -84,7 +80,7 @@ CREATE PROCEDURE assert_supplier_schema()
|
|||||||
BEGIN
|
BEGIN
|
||||||
IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier') <> 1
|
IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier') <> 1
|
||||||
OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND index_name IN ('PRIMARY','uk_wh_supplier_name','idx_wh_supplier_filter')) <> 3
|
OR (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND index_name IN ('PRIMARY','uk_wh_supplier_name','idx_wh_supplier_filter')) <> 3
|
||||||
OR (SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND constraint_type='CHECK' AND constraint_name IN ('chk_wh_supplier_category','chk_wh_supplier_tenant','chk_wh_supplier_scope','chk_wh_supplier_status','chk_wh_supplier_settlement')) <> 5 THEN
|
OR (SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND constraint_type='CHECK' AND constraint_name IN ('chk_wh_supplier_category','chk_wh_supplier_tenant','chk_wh_supplier_status','chk_wh_supplier_settlement')) <> 4 THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商表、索引或约束初始化不完整';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商表、索引或约束初始化不完整';
|
||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
|
|||||||
@@ -45,7 +45,6 @@
|
|||||||
| `page` | integer | 否 | 默认 `1`,最小 `1` |
|
| `page` | integer | 否 | 默认 `1`,最小 `1` |
|
||||||
| `size` | integer | 否 | 默认 `20`,范围 `1..200` |
|
| `size` | integer | 否 | 默认 `20`,范围 `1..200` |
|
||||||
| `category` | enum | 否 | 供应商分类 |
|
| `category` | enum | 否 | 供应商分类 |
|
||||||
| `scope` | enum | 否 | 仅订舱分类适用 |
|
|
||||||
| `region` | string | 否 | 地区精确匹配,最长 80 |
|
| `region` | string | 否 | 地区精确匹配,最长 80 |
|
||||||
| `status` | enum | 否 | 合作状态 |
|
| `status` | enum | 否 | 合作状态 |
|
||||||
| `keyword` | string | 否 | 按供应商名称或联系人模糊匹配 |
|
| `keyword` | string | 否 | 按供应商名称或联系人模糊匹配 |
|
||||||
@@ -72,7 +71,6 @@
|
|||||||
| `tenantId` | string | 固定为 `0` |
|
| `tenantId` | string | 固定为 `0` |
|
||||||
| `name` | string | 供应商名称,平台内唯一 |
|
| `name` | string | 供应商名称,平台内唯一 |
|
||||||
| `category` | enum | 分类 |
|
| `category` | enum | 分类 |
|
||||||
| `scope` | enum/null | 订舱供应商必填;其他分类为 `null` |
|
|
||||||
| `region` | string | 地区 |
|
| `region` | string | 地区 |
|
||||||
| `legalRepresentative` | string | 法人代表 |
|
| `legalRepresentative` | string | 法人代表 |
|
||||||
| `contactName` | string | 联系人 |
|
| `contactName` | string | 联系人 |
|
||||||
@@ -101,7 +99,6 @@
|
|||||||
{
|
{
|
||||||
"name": "示例供应商有限公司",
|
"name": "示例供应商有限公司",
|
||||||
"category": "BOOKING",
|
"category": "BOOKING",
|
||||||
"scope": "DOMESTIC",
|
|
||||||
"region": "宁波",
|
"region": "宁波",
|
||||||
"legalRepresentative": "张三",
|
"legalRepresentative": "张三",
|
||||||
"contactName": "李经理",
|
"contactName": "李经理",
|
||||||
@@ -116,7 +113,7 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
必填字段:`name`、`category`、`region`、`legalRepresentative`、`contactName`、`phone`、`status`、`settlementPeriod`。`BOOKING` 时 `scope` 必填;其他分类 `scope` 必须传 `null` 或省略。
|
必填字段:`name`、`category`、`region`、`legalRepresentative`、`contactName`、`phone`、`status`、`settlementPeriod`。订舱供应商不区分国内或国外。
|
||||||
|
|
||||||
新增成功的 `data` 是供应商 ID 字符串。编辑成功的 `data` 为 `null`。
|
新增成功的 `data` 是供应商 ID 字符串。编辑成功的 `data` 为 `null`。
|
||||||
|
|
||||||
@@ -152,13 +149,6 @@
|
|||||||
| `TRUCKING` | 拖车 |
|
| `TRUCKING` | 拖车 |
|
||||||
| `CERTIFICATE` | 证书 |
|
| `CERTIFICATE` | 证书 |
|
||||||
|
|
||||||
订舱范围:
|
|
||||||
|
|
||||||
| 值 | 展示文案 |
|
|
||||||
|---|---|
|
|
||||||
| `DOMESTIC` | 国内 |
|
|
||||||
| `INTERNATIONAL` | 国外 |
|
|
||||||
|
|
||||||
合作状态:
|
合作状态:
|
||||||
|
|
||||||
| 值 | 展示文案 |
|
| 值 | 展示文案 |
|
||||||
@@ -187,7 +177,7 @@
|
|||||||
| `A0403` | 非平台用户或缺少对应权限 | 显示无权限 |
|
| `A0403` | 非平台用户或缺少对应权限 | 显示无权限 |
|
||||||
| `A0404` | 供应商不存在 | 提示记录不存在并刷新列表 |
|
| `A0404` | 供应商不存在 | 提示记录不存在并刷新列表 |
|
||||||
| `A0409` | 供应商名称重复或并发唯一键冲突 | 保留表单并提示名称已存在 |
|
| `A0409` | 供应商名称重复或并发唯一键冲突 | 保留表单并提示名称已存在 |
|
||||||
| `A0422` | 字段校验失败、订舱范围规则不满足 | 使用 `message`/`data` 显示字段错误 |
|
| `A0422` | 字段校验失败 | 使用 `message`/`data` 显示字段错误 |
|
||||||
| `S0500` | 未预期服务异常 | 展示通用错误并记录 `requestId/traceId` |
|
| `S0500` | 未预期服务异常 | 展示通用错误并记录 `requestId/traceId` |
|
||||||
| `S0503` | Warehouse 数据库不可用 | 展示稍后重试并记录 `requestId/traceId` |
|
| `S0503` | Warehouse 数据库不可用 | 展示稍后重试并记录 `requestId/traceId` |
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ paths:
|
|||||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||||
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||||
- { name: category, in: query, schema: { $ref: '#/components/schemas/Category' } }
|
- { name: category, in: query, schema: { $ref: '#/components/schemas/Category' } }
|
||||||
- { name: scope, in: query, description: 仅订舱供应商适用, schema: { $ref: '#/components/schemas/Scope' } }
|
|
||||||
- { name: region, in: query, schema: { type: string } }
|
- { name: region, in: query, schema: { type: string } }
|
||||||
- { name: status, in: query, schema: { $ref: '#/components/schemas/Status' } }
|
- { name: status, in: query, schema: { $ref: '#/components/schemas/Status' } }
|
||||||
- { name: keyword, in: query, description: 供应商名称或联系人, schema: { type: string } }
|
- { name: keyword, in: query, description: 供应商名称或联系人, schema: { type: string } }
|
||||||
@@ -57,13 +56,12 @@ paths:
|
|||||||
/internal/warehouse/v1/suppliers/{supplierId}:
|
/internal/warehouse/v1/suppliers/{supplierId}:
|
||||||
get:
|
get:
|
||||||
summary: 内部服务查询并校验供应商快照
|
summary: 内部服务查询并校验供应商快照
|
||||||
description: 仅服务间调用,需 X-Internal-Service-Token;不经 Gateway。可要求 ACTIVE,并校验类别与订舱范围。
|
description: 仅服务间调用,需 X-Internal-Service-Token;不经 Gateway。可要求 ACTIVE,并校验供应商类别。
|
||||||
security: [{ internalServiceToken: [] }]
|
security: [{ internalServiceToken: [] }]
|
||||||
parameters:
|
parameters:
|
||||||
- { name: supplierId, in: path, required: true, schema: { type: integer, format: int64, minimum: 1 } }
|
- { name: supplierId, in: path, required: true, schema: { type: integer, format: int64, minimum: 1 } }
|
||||||
- { name: requireActive, in: query, schema: { type: boolean, default: false } }
|
- { name: requireActive, in: query, schema: { type: boolean, default: false } }
|
||||||
- { name: category, in: query, schema: { $ref: '#/components/schemas/Category' } }
|
- { name: category, in: query, schema: { $ref: '#/components/schemas/Category' } }
|
||||||
- { name: scope, in: query, schema: { $ref: '#/components/schemas/Scope' } }
|
|
||||||
responses:
|
responses:
|
||||||
'200': { description: 成功, content: { application/json: { schema: { $ref: '#/components/schemas/InternalSnapshotResponse' } } } }
|
'200': { description: 成功, content: { application/json: { schema: { $ref: '#/components/schemas/InternalSnapshotResponse' } } } }
|
||||||
'403': { $ref: '#/components/responses/Forbidden' }
|
'403': { $ref: '#/components/responses/Forbidden' }
|
||||||
@@ -74,7 +72,6 @@ components:
|
|||||||
RequiredTenantId: { name: tenantId, in: query, required: true, schema: { type: integer, format: int64, enum: [0] } }
|
RequiredTenantId: { name: tenantId, in: query, required: true, schema: { type: integer, format: int64, enum: [0] } }
|
||||||
schemas:
|
schemas:
|
||||||
Category: { type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE] }
|
Category: { type: string, enum: [BOOKING, CUSTOMS, TRUCKING, CERTIFICATE] }
|
||||||
Scope: { type: string, enum: [DOMESTIC, INTERNATIONAL] }
|
|
||||||
Status: { type: string, enum: [ACTIVE, INACTIVE] }
|
Status: { type: string, enum: [ACTIVE, INACTIVE] }
|
||||||
SupplierSave:
|
SupplierSave:
|
||||||
type: object
|
type: object
|
||||||
@@ -82,7 +79,6 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
name: { type: string, maxLength: 160 }
|
name: { type: string, maxLength: 160 }
|
||||||
category: { $ref: '#/components/schemas/Category' }
|
category: { $ref: '#/components/schemas/Category' }
|
||||||
scope: { $ref: '#/components/schemas/Scope', description: BOOKING 必填,其他分类必须为空 }
|
|
||||||
region: { type: string, maxLength: 80 }
|
region: { type: string, maxLength: 80 }
|
||||||
legalRepresentative: { type: string, maxLength: 80 }
|
legalRepresentative: { type: string, maxLength: 80 }
|
||||||
contactName: { type: string, maxLength: 80 }
|
contactName: { type: string, maxLength: 80 }
|
||||||
@@ -140,7 +136,6 @@ components:
|
|||||||
supplierId: { type: integer, format: int64 }
|
supplierId: { type: integer, format: int64 }
|
||||||
name: { type: string }
|
name: { type: string }
|
||||||
category: { $ref: '#/components/schemas/Category' }
|
category: { $ref: '#/components/schemas/Category' }
|
||||||
scope: { $ref: '#/components/schemas/Scope' }
|
|
||||||
status: { $ref: '#/components/schemas/Status' }
|
status: { $ref: '#/components/schemas/Status' }
|
||||||
InternalSnapshotResponse:
|
InternalSnapshotResponse:
|
||||||
allOf: [{ $ref: '#/components/schemas/ApiResponse' }, { type: object, properties: { data: { $ref: '#/components/schemas/InternalSnapshot' } } }]
|
allOf: [{ $ref: '#/components/schemas/ApiResponse' }, { type: object, properties: { data: { $ref: '#/components/schemas/InternalSnapshot' } } }]
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
package com.baiyue.warehouse.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class InternalSupplierControllerContractTest {
|
||||||
|
@Test void validatesSupplierByActiveStatusAndCategoryWithoutBookingScope() {
|
||||||
|
assertThat(InternalSupplierController.class.getAnnotation(RequestMapping.class).value())
|
||||||
|
.containsExactly("/internal/warehouse/v1/suppliers");
|
||||||
|
Method get = Arrays.stream(InternalSupplierController.class.getDeclaredMethods())
|
||||||
|
.filter(method -> method.getName().equals("get"))
|
||||||
|
.findFirst().orElseThrow();
|
||||||
|
assertThat(Arrays.stream(get.getParameters()).map(parameter -> parameter.getName()))
|
||||||
|
.containsExactly("token", "supplierId", "requireActive", "category");
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-2
@@ -8,17 +8,18 @@ import java.nio.file.Path;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
class SupplierSqlContractTest {
|
class SupplierSqlContractTest {
|
||||||
@Test void freezesPublicSupplierSchemaAndBookingScope() throws IOException {
|
@Test void freezesPublicSupplierSchemaWithoutBookingScope() throws IOException {
|
||||||
try (var stream = getClass().getResourceAsStream("/db/warehouse-supplier.sql")) {
|
try (var stream = getClass().getResourceAsStream("/db/warehouse-supplier.sql")) {
|
||||||
assertThat(stream).isNotNull();
|
assertThat(stream).isNotNull();
|
||||||
String sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
String sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0",
|
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0",
|
||||||
"chk_wh_supplier_tenant CHECK (tenant_id = 0)",
|
"chk_wh_supplier_tenant CHECK (tenant_id = 0)",
|
||||||
"category='BOOKING' AND scope IN ('DOMESTIC','INTERNATIONAL')", "license_file_id VARCHAR(128)",
|
"license_file_id VARCHAR(128)",
|
||||||
"MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE",
|
"MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE",
|
||||||
"created_by BIGINT UNSIGNED", "updated_by BIGINT UNSIGNED",
|
"created_by BIGINT UNSIGNED", "updated_by BIGINT UNSIGNED",
|
||||||
"supplier_add_column_if_missing", "uk_wh_supplier_name", "idx_wh_supplier_filter",
|
"supplier_add_column_if_missing", "uk_wh_supplier_name", "idx_wh_supplier_filter",
|
||||||
"assert_supplier_schema");
|
"assert_supplier_schema");
|
||||||
|
assertThat(sql).doesNotContain("scope VARCHAR", "chk_wh_supplier_scope", "DOMESTIC/INTERNATIONAL");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ class SupplierSqlContractTest {
|
|||||||
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "uk_wh_supplier_name",
|
assertThat(sql).contains("CREATE TABLE IF NOT EXISTS wh_supplier", "uk_wh_supplier_name",
|
||||||
"idx_wh_supplier_filter", "platform:supplier:list", "platform:supplier:create",
|
"idx_wh_supplier_filter", "platform:supplier:list", "platform:supplier:create",
|
||||||
"platform:supplier:edit", "platform:supplier:status", "assert_platform_supplier_permissions");
|
"platform:supplier:edit", "platform:supplier:status", "assert_platform_supplier_permissions");
|
||||||
|
String supplierSection = sql.substring(sql.indexOf("-- 平台公共供应商业务档案及权限"));
|
||||||
|
assertThat(supplierSection).doesNotContain("chk_wh_supplier_scope", "订舱分类范围");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Path resolveUnifiedSql() {
|
private static Path resolveUnifiedSql() {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.baiyue.warehouse.service;
|
|||||||
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.warehouse.dto.SupplierDtos.SaveRequest;
|
import com.baiyue.warehouse.dto.SupplierDtos.SaveRequest;
|
||||||
import com.baiyue.warehouse.dto.SupplierDtos.PageQuery;
|
|
||||||
import com.baiyue.warehouse.dto.SupplierDtos.StatusRequest;
|
import com.baiyue.warehouse.dto.SupplierDtos.StatusRequest;
|
||||||
import com.baiyue.warehouse.entity.SupplierEntity;
|
import com.baiyue.warehouse.entity.SupplierEntity;
|
||||||
import com.baiyue.warehouse.repository.SupplierRepository;
|
import com.baiyue.warehouse.repository.SupplierRepository;
|
||||||
@@ -13,8 +12,8 @@ import org.mockito.InjectMocks;
|
|||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
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;
|
||||||
@@ -26,7 +25,7 @@ class SupplierServiceTest {
|
|||||||
private SaveRequest booking;
|
private SaveRequest booking;
|
||||||
|
|
||||||
@BeforeEach void setUp() {
|
@BeforeEach void setUp() {
|
||||||
booking = new SaveRequest("测试供应商", "BOOKING", "DOMESTIC", "宁波", "法人", "联系人",
|
booking = new SaveRequest("测试供应商", "BOOKING", "宁波", "法人", "联系人",
|
||||||
"13800000000", "supplier@example.com", "地址", "91330000TEST", "192000000000000001",
|
"13800000000", "supplier@example.com", "地址", "91330000TEST", "192000000000000001",
|
||||||
"ACTIVE", "MONTHLY_30", null);
|
"ACTIVE", "MONTHLY_30", null);
|
||||||
}
|
}
|
||||||
@@ -37,24 +36,6 @@ class SupplierServiceTest {
|
|||||||
verify(repository).insert(any(SupplierEntity.class));
|
verify(repository).insert(any(SupplierEntity.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test void rejectsScopeForNonBookingSupplier() {
|
|
||||||
SaveRequest customs = new SaveRequest("报关供应商", "CUSTOMS", "DOMESTIC", "宁波", "法人", "联系人",
|
|
||||||
"13800000000", null, null, null, null, "ACTIVE", "PER_TRANSACTION", null);
|
|
||||||
assertThatThrownBy(() -> service.create(101L, customs)).isInstanceOf(BizException.class)
|
|
||||||
.hasMessageContaining("仅适用于订舱供应商");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test void requiresScopeForBookingSupplier() {
|
|
||||||
SaveRequest missingScope = new SaveRequest("订舱供应商", "BOOKING", null, "宁波", "法人", "联系人",
|
|
||||||
"13800000000", null, null, null, null, "ACTIVE", "PER_TRANSACTION", null);
|
|
||||||
assertThatThrownBy(() -> service.create(101L, missingScope)).isInstanceOf(BizException.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test void rejectsScopeFilterWithoutBookingCategory() {
|
|
||||||
assertThatThrownBy(() -> service.page(new PageQuery(1, 20, "CUSTOMS", "DOMESTIC", null, null, null)))
|
|
||||||
.isInstanceOf(BizException.class).hasMessageContaining("scope仅适用于订舱供应商");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test void repeatedStatusChangeIsIdempotent() {
|
@Test void repeatedStatusChangeIsIdempotent() {
|
||||||
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L); entity.setStatus("ACTIVE");
|
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L); entity.setStatus("ACTIVE");
|
||||||
when(repository.find(0L, 101L)).thenReturn(entity);
|
when(repository.find(0L, 101L)).thenReturn(entity);
|
||||||
@@ -64,9 +45,23 @@ class SupplierServiceTest {
|
|||||||
|
|
||||||
@Test void validatesInternalSupplierSnapshot() {
|
@Test void validatesInternalSupplierSnapshot() {
|
||||||
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L); entity.setName("测试供应商");
|
SupplierEntity entity = new SupplierEntity(); entity.setId(101L); entity.setTenantId(0L); entity.setName("测试供应商");
|
||||||
entity.setCategory("BOOKING"); entity.setScope("DOMESTIC"); entity.setStatus("ACTIVE");
|
entity.setCategory("BOOKING"); entity.setStatus("ACTIVE");
|
||||||
when(repository.find(0L, 101L)).thenReturn(entity);
|
when(repository.find(0L, 101L)).thenReturn(entity);
|
||||||
var snapshot = service.internalSnapshot(101L, true, "BOOKING", "DOMESTIC");
|
var snapshot = service.internalSnapshot(101L, true, "BOOKING");
|
||||||
org.assertj.core.api.Assertions.assertThat(snapshot.supplierId()).isEqualTo(101L);
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void internalSnapshotRequiresActiveSupplierAndMatchingCategory() {
|
||||||
|
SupplierEntity inactive = new SupplierEntity(); inactive.setId(101L); inactive.setTenantId(0L);
|
||||||
|
inactive.setName("停用订舱供应商"); inactive.setCategory("BOOKING"); inactive.setStatus("INACTIVE");
|
||||||
|
when(repository.find(0L, 101L)).thenReturn(inactive);
|
||||||
|
assertThatThrownBy(() -> service.internalSnapshot(101L, true, "BOOKING"))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("未启用");
|
||||||
|
|
||||||
|
inactive.setStatus("ACTIVE");
|
||||||
|
assertThatThrownBy(() -> service.internalSnapshot(101L, true, "CUSTOMS"))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("类别不匹配");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user