优化
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
|
||||
| 任务关键词 | 首选服务 | 先看入口 |
|
||||
|---|---|---|
|
||||
| 登录、Token、密码、注册 | `auth/` | `auth/AGENTS.md` |
|
||||
| 登录、Token、密码、注册与顶层授权快照 | `auth/` | `auth/AGENTS.md`;海外 Token 只携带稳定身份与版本,角色、权限和冻结海外仓从登录/刷新响应顶层快照读取,海外 Gateway 再按 IAM 安全投影失败关闭校验 |
|
||||
| 客户档案、成员、审核、RBAC、授权快照 | `iam/` | `iam/AGENTS.md` |
|
||||
| 客户建档默认尺寸初始化与补偿 | `iam/`、`firstleg/` | IAM `CustomerDimensionRuleInitializationService` 持久化意图、`CustomerDimensionInitializationRelay` 后台投递;Firstleg 内部入口 `/internal/firstleg/v1/customer-dimension-configs/defaults`,未 `READY` 不得审核通过 |
|
||||
| 消息中心、站内通知、消息模板、收件箱 | `iam/` | `iam/AGENTS.md`,再按 `src/main/java/com/baiyue/iam/notification/` 定位;业务事件生产者回到所属服务 |
|
||||
@@ -26,7 +26,8 @@
|
||||
| 客户头程订单中文批量导入 | `order/` | `MerchantBatchImportService.java`、`MerchantWarehouseCatalogClient.java`、`MerchantPricingCatalogClient.java`、`order-v1.yaml`;模板和导入都按当前客户读取动态地址、全部有效仓库与渠道,最多 50 行,只创建草稿,支持 `PARTIAL/ATOMIC` |
|
||||
| 平台修改客户原始申报、履约渠道变更与跨服务同步 | `order/`、`firstleg/` | Order `OrderDeclarationUpdateService.java`、`OrderInboundOutboxPublisher.java`、`order-v1.yaml`;Firstleg `OrderDeclarationSyncService.java`、`OrderDeclarationSyncMapper.java`、`firstleg-v1.yaml`。只更新申报快照;渠道变化先失败关闭校验履约可逆性与重新计价,成功后由版本化 `ORDER_DECLARATION_UPDATED_V1` 幂等同步,不覆盖实测、箱码或已确认费用 |
|
||||
| 履约分配、入库、测量、箱规算法、审核、FBA 计费 | `firstleg/` | `firstleg/AGENTS.md`;客户四档箱规与 `ADD/MULTIPLY` 默认算法从 `CustomerDimensionConfigService` 进入,单箱测量和最终计价分别由 `CustomerDimensionWeightingService`、`FbaPricingService` 保存并消费服务端快照 |
|
||||
| 称重确认、整票库位块与 PDA 资源结算 | `firstleg/`、`warehouse/` | Firstleg `MeasurementLocationOccupationService.java`、`InboundMeasurementService.java`、`WarehousePalletLocationClient.java`;Warehouse `LocationBlockService.java` 与 `location-block-v1.yaml`。首张称重确认分配整票块,测量提交只复用;部分拣货整段转移给余量子单,整票拣完才整块释放,A0 共享资源按 `reservationId` 区分 |
|
||||
| 入库扣货、解除、履约门禁与凭证 | `firstleg/`、`file/` | Firstleg `InboundHoldService.java`、`InboundHoldGuard.java`、`InboundHoldEvidenceService.java`;File 业务类型 `INBOUND_HOLD_EVIDENCE`。扣货阻断物理履约写入,已发生实物作业后禁止手工切换,命令按入库版本和幂等键执行 |
|
||||
| 称重确认、整票库位块与 PDA 资源结算 | `firstleg/`、`warehouse/` | Firstleg `MeasurementLocationOccupationService.java`、`InboundMeasurementService.java`、`MeasurementSubmissionService.java`、`WarehousePalletLocationClient.java`;Warehouse `LocationBlockService.java` 与 `location-block-v1.yaml`。称重确认尽力建块、提交时再次补齐,库位指引失败不阻断测量主流程;部分拣货整段转移给余量子单,整票拣完才整块释放,A0 共享资源按 `reservationId` 区分 |
|
||||
| Firstleg 内部模块化与依赖基线 | `firstleg/` | `firstleg/docs/firstleg-internal-modularization-baseline.md`、`firstleg/scripts/analyze-firstleg-dependencies.mjs`;价格簿查询和客户统计按端口/适配器切片,不改变服务与数据库边界 |
|
||||
| Mapper 新增、移动、Bean 缺失与扫描遗漏 | 目标持久化服务 | 所有 Mapper 显式 `@Mapper`,启动类按服务根包注解过滤扫描;先运行 `node scripts/mapper-scan-contract.mjs`,再按目标模块执行 `test-compile/test`,不得等部署启动发现 |
|
||||
| 脚本管理、全量验证、测试造数与诊断 | 仓库根 `scripts/`;服务专属脚本留在 `<service>/scripts/` | 先读 `scripts/AGENTS.md` 和 `scripts/README.md`;Admin HTTP 内部模块继续读 `scripts/admin-test-env-modules/README.md`。新增入口必须登记用途、目标环境、数据副作用和报告位置,并通过 `scripts/script-documentation-contract.test.mjs` |
|
||||
@@ -36,6 +37,7 @@
|
||||
| 客户入库结果与头程费用确认 | `firstleg/` | `controller/MerchantInboundBillingController.java`、`service/MerchantInboundBillingService.java`、`FbaPricingService.java`,接口 `/api/merchant/v1/first-leg/orders/{orderId}/inbound-billing` 与 `/confirm`;客户按登录态范围读取并以费用版本和幂等键确认当前 `SENT` 费用,确认后锁定 |
|
||||
| 订舱资料/打印、船期、订舱级拖车、报关 | `firstleg/` | `oceanbooking/`、`portschedule/`、`TruckAppointmentService.java`、`outbound/dispatch/`、`customs/`;拖车只绑定 `RELEASED` 订舱,不携带客户、柜次、装柜计划或真实柜号,每订舱最多一张非终态预约,订舱文件联查 `file/AGENTS.md` |
|
||||
| 客户账期、费用池、账单、入仓码联查、收款、成本、核销 | `finance/` | `finance/AGENTS.md`;客户账单行 `inboundCode` 由 `FinanceQueryService` 从费用来源快照提供,Client 复用头程入仓关系详情;费用来源和柜次成本快照再联查 `firstleg/AGENTS.md`,凭证再联查 `file/AGENTS.md` |
|
||||
| 客户资金账户、充值、授信、冻结与结算 | `finance/` | `FinanceWalletService.java`、`ClientFinanceWalletController.java`、`PlatformFinanceWalletController.java`、`InternalFinanceWalletController.java`、`finance-wallet-v1.yaml`;Finance 持有 CNY 主账和不可变流水,业务服务只能经内部命令或跨区 Wallet 事件冻结、结算、释放 |
|
||||
| 柜级公共费用跨客户分摊 | `firstleg/`、`finance/` | Firstleg `PlatformContainerFeeDetailController.java`、`ContainerFeeDetailService.java`、`container-fees-v1.yaml` 按柜内真实客户与订单原子创建 `allocation-groups`;金额合计必须等于总额,分组审核、驳回、调整、作废均整组处理,Finance 只消费各客户不可变来源快照 |
|
||||
| 海外业务费用来源跨区入池 | `lastleg-service/`、`common-event-contract/`、`event-bridge-service/`、`finance/` | Lastleg `financefee/` 审核五类海外费用并发布 `BUSINESS_FEE_SOURCE_CHANGED` 完整快照;Event Bridge 对 `targetRegion=CN` 定向投递 Finance `/internal/finance/v1/business-fee-source-events`;Finance 按事件 ID、Payload 哈希、来源系统/费用 ID/版本幂等入池,`PAYABLE + DIRECT_PAID` 单独形成已确认成本且不进入 AP 待结算 |
|
||||
| 入库/柜次应付、直接已付、付款流水与凭证 | `firstleg/`、`finance/`、`file/` | 业务费用与付款快照见 `firstleg/AGENTS.md` 的 `FeePaymentSnapshotValidator`;`/api/platform/finance/v1/vouchers` 上传凭证,File 按 `FINANCE_VOUCHER` 和客户范围校验 |
|
||||
|
||||
@@ -1776,3 +1776,69 @@ CREATE TABLE IF NOT EXISTS ll_destuff_pallet_cost_allocation (
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_destuff_customer(cost_id,customer_id), UNIQUE KEY uk_ll_destuff_fee_source(fee_source_id),
|
||||
CONSTRAINT chk_ll_destuff_allocation CHECK(actual_volume_cbm>0 AND allocated_amount>=0)
|
||||
) ENGINE=InnoDB COMMENT='拆柜打托成本按客户实收体积分摊';
|
||||
|
||||
|
||||
-- 海外接柜仓由平台在尾程建单时预分配;历史测试数据不回填,缺失事实由业务接口失败关闭。
|
||||
USE baiyue_lastleg;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_order' AND column_name='overseas_warehouse_code'),
|
||||
'ALTER TABLE ll_order MODIFY COLUMN overseas_warehouse_code VARCHAR(128) NULL COMMENT ''海外接柜仓业务编码快照''',
|
||||
'ALTER TABLE ll_order ADD COLUMN overseas_warehouse_code VARCHAR(128) NULL COMMENT ''海外接柜仓业务编码快照'' AFTER overseas_warehouse_id'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_order' AND column_name='receiving_warehouse_version'),
|
||||
'ALTER TABLE ll_order MODIFY COLUMN receiving_warehouse_version INT NOT NULL DEFAULT 0 COMMENT ''柜级接柜仓分配乐观锁版本''',
|
||||
'ALTER TABLE ll_order ADD COLUMN receiving_warehouse_version INT NOT NULL DEFAULT 0 COMMENT ''柜级接柜仓分配乐观锁版本'' AFTER overseas_warehouse_name'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
ALTER TABLE ll_order
|
||||
MODIFY COLUMN overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外接柜仓ID;新建尾程订单默认洛杉矶仓',
|
||||
MODIFY COLUMN overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外接柜仓名称快照';
|
||||
|
||||
ALTER TABLE ll_inbound_plan
|
||||
MODIFY COLUMN overseas_warehouse_id BIGINT NULL COMMENT '从尾程订单继承的平台预分配海外接柜仓ID';
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||
AND table_name='ll_receiving_container_session' AND column_name='overseas_warehouse_code'),
|
||||
'ALTER TABLE ll_receiving_container_session MODIFY COLUMN overseas_warehouse_code VARCHAR(128) NULL COMMENT ''海外接柜仓业务编码快照''',
|
||||
'ALTER TABLE ll_receiving_container_session ADD COLUMN overseas_warehouse_code VARCHAR(128) NULL COMMENT ''海外接柜仓业务编码快照'' AFTER overseas_warehouse_id'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
ALTER TABLE ll_receiving_container_session
|
||||
MODIFY COLUMN overseas_warehouse_id BIGINT NOT NULL COMMENT '平台预分配并由操作员Token匹配的海外接柜仓ID',
|
||||
MODIFY COLUMN overseas_warehouse_name VARCHAR(255) NOT NULL COMMENT '仓库服务校验后的海外接柜仓名称快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_warehouse_operation (
|
||||
id BIGINT NOT NULL COMMENT '柜级接柜仓修改操作ID',
|
||||
source_container_booking_id VARCHAR(128) NOT NULL COMMENT '稳定来源柜预订ID',
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '平台请求幂等键',
|
||||
request_hash VARCHAR(64) NOT NULL COMMENT '规范化修改请求SHA-256摘要',
|
||||
result_version INT NOT NULL COMMENT '修改成功后的柜级分配版本',
|
||||
operator_name VARCHAR(128) NOT NULL COMMENT '平台操作人名称',
|
||||
remark VARCHAR(500) NULL COMMENT '接柜仓修改备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_receiving_warehouse_idempotency(idempotency_key),
|
||||
KEY idx_ll_receiving_warehouse_container(source_container_booking_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='柜级海外接柜仓修改幂等审计记录';
|
||||
|
||||
DROP PROCEDURE IF EXISTS assert_receiving_warehouse_assignment_ready;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_receiving_warehouse_assignment_ready()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='baiyue_lastleg'
|
||||
AND table_name='ll_order' AND column_name IN ('overseas_warehouse_code','receiving_warehouse_version')) <> 2 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='尾程订单海外接柜仓字段未完整部署';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='baiyue_lastleg'
|
||||
AND table_name='ll_receiving_container_session' AND column_name='overseas_warehouse_code') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='海外PDA接柜会话仓库编码字段未部署';
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='baiyue_lastleg'
|
||||
AND table_name='ll_receiving_warehouse_operation') <> 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='柜级海外接柜仓修改幂等审计表未部署';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_receiving_warehouse_assignment_ready();
|
||||
DROP PROCEDURE assert_receiving_warehouse_assignment_ready;
|
||||
|
||||
SELECT 'PASS' AS migration_status, '海外接柜仓预分配与柜级修改结构已就绪' AS message;
|
||||
|
||||
+14
@@ -102,6 +102,20 @@ public class LastlegOrderController {
|
||||
return ApiResponse.success(orderService.containerProgressDetail(sourceContainerBookingId));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}/receiving-warehouse")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
|
||||
@Operation(summary = "修改整柜海外接柜仓",
|
||||
description = "仅在海外PDA建立接柜会话前允许修改;按 expectedVersion 乐观锁原子同步柜内订单与入库计划。")
|
||||
public ApiResponse<ContainerPlanProgressSummary> changeReceivingWarehouse(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String sourceContainerBookingId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody LastlegOrderRequests.ChangeReceivingWarehouse request) {
|
||||
platformScope(user, null);
|
||||
return ApiResponse.success(orderService.changeReceivingWarehouse(sourceContainerBookingId,
|
||||
idempotencyKey, request, operatorName(user)));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/{id}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
@Operation(summary = "Get one inbound-code-level tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_LIST)
|
||||
|
||||
+14
-2
@@ -36,7 +36,13 @@ public interface DispatchCandidateMapper {
|
||||
"(SELECT MAX(fa.expected_loading_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||
"MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
"ELSE MIN(o.overseas_warehouse_name) END AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
@@ -94,7 +100,13 @@ public interface DispatchCandidateMapper {
|
||||
"(SELECT MAX(fa.expected_loading_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||
"MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " +
|
||||
"AND pos.available_packages > 0) ELSE CAST(ip.overseas_warehouse_id AS CHAR) END AS overseasWarehouseId," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_name) " +
|
||||
"FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " +
|
||||
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " +
|
||||
"ELSE MIN(o.overseas_warehouse_name) END AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
|
||||
+9
-3
@@ -220,10 +220,16 @@ public class DispatchService {
|
||||
if (selected.size() != palletIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
||||
}
|
||||
boolean missingWarehouse = selected.stream().map(DispatchPalletCandidate::overseasWarehouseId)
|
||||
.anyMatch(value -> value == null || value.isBlank());
|
||||
if (missingWarehouse) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"所选托盘中存在尚未关联海外仓的托盘");
|
||||
}
|
||||
Set<String> warehouseIds = selected.stream().map(DispatchPalletCandidate::overseasWarehouseId)
|
||||
.filter(Objects::nonNull).map(String::trim).filter(value -> !value.isEmpty()).collect(Collectors.toSet());
|
||||
if (warehouseIds.size() != 1 || selected.stream().anyMatch(candidate -> candidate.overseasWarehouseId() == null)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "A dispatch must belong to exactly one overseas warehouse");
|
||||
.map(String::trim).collect(Collectors.toSet());
|
||||
if (warehouseIds.size() != 1) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "一次排车只能包含同一海外仓的托盘");
|
||||
}
|
||||
String draftToken = trimToNull(request.draftToken());
|
||||
for (DispatchPalletCandidate candidate : selected) {
|
||||
|
||||
+16
@@ -6,6 +6,12 @@ import java.time.LocalDateTime;
|
||||
public record ContainerPlanProgressSummary(
|
||||
String sourceContainerBookingId,
|
||||
String containerNo,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
int assignmentVersion,
|
||||
boolean canChangeReceivingWarehouse,
|
||||
String receivingWarehouseLockReason,
|
||||
long totalOrderCount,
|
||||
long inboundCodeCount,
|
||||
long missingInboundCodeCount,
|
||||
@@ -16,4 +22,14 @@ public record ContainerPlanProgressSummary(
|
||||
boolean allPlansReady,
|
||||
String blockingReason
|
||||
) {
|
||||
public ContainerPlanProgressSummary(String sourceContainerBookingId, String containerNo,
|
||||
long totalOrderCount, long inboundCodeCount,
|
||||
long missingInboundCodeCount, long planCount,
|
||||
long plannedInboundCodeCount, long pendingPlanCount,
|
||||
LocalDateTime plannedArrivalAt, boolean allPlansReady,
|
||||
String blockingReason) {
|
||||
this(sourceContainerBookingId, containerNo, null, null, null, -1, false, null,
|
||||
totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount,
|
||||
plannedInboundCodeCount, pendingPlanCount, plannedArrivalAt, allPlansReady, blockingReason);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,12 @@ public final class LastlegOrderRequests {
|
||||
}
|
||||
}
|
||||
|
||||
public record ChangeReceivingWarehouse(
|
||||
@NotNull Long overseasWarehouseId,
|
||||
@NotNull @Min(0) Integer expectedVersion,
|
||||
@Size(max = 500) String remark
|
||||
) { }
|
||||
|
||||
public record TransferDestination(
|
||||
@NotEmpty @Size(max = 200) List<Long> orderIds,
|
||||
@NotBlank @Size(max = 128) String warehouseId,
|
||||
|
||||
@@ -22,6 +22,7 @@ public record LastlegOrderSummary(
|
||||
String customerId,
|
||||
String customerName,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String transferWarehouseId,
|
||||
String transferWarehouseCode,
|
||||
|
||||
@@ -28,7 +28,9 @@ public class LastlegOrder {
|
||||
private String sourceWarehouseId;
|
||||
private String sourceWarehouseName;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
private String overseasWarehouseName;
|
||||
private Integer receivingWarehouseVersion;
|
||||
private String fbaWarehouseCode;
|
||||
private String containerNo;
|
||||
private String transferWarehouseId;
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_receiving_warehouse_operation")
|
||||
public class ReceivingWarehouseOperation {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String sourceContainerBookingId;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private Integer resultVersion;
|
||||
private String operatorName;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+3
-15
@@ -12,6 +12,8 @@ public record InboundPlanListResponse(
|
||||
String inboundCode,
|
||||
String lxCode,
|
||||
String orderType,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String warehouseId,
|
||||
String warehouseCode,
|
||||
String warehouseName,
|
||||
@@ -29,18 +31,4 @@ public record InboundPlanListResponse(
|
||||
String dispatchStatus,
|
||||
LocalDateTime createdAt,
|
||||
Integer version
|
||||
) {
|
||||
public InboundPlanListResponse(Long lineId, Long planId, String planNo, String customerId,
|
||||
Long orderId, String inboundCode, String orderType,
|
||||
String warehouseId, String warehouseCode, String warehouseName,
|
||||
String fbaWarehouseCode, String customerName, Integer packageCount,
|
||||
BigDecimal volumeCbm, Integer palletCount, String status,
|
||||
String appointmentNo, String appointmentStatus,
|
||||
LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt,
|
||||
String dispatchStatus, LocalDateTime createdAt) {
|
||||
this(lineId, planId, planNo, customerId, orderId, inboundCode, null, orderType, warehouseId,
|
||||
warehouseCode, warehouseName, fbaWarehouseCode, customerName, packageCount, 0,
|
||||
volumeCbm, palletCount, status, appointmentNo, appointmentStatus, appointmentAt,
|
||||
expectedLoadingAt, dispatchStatus, createdAt, 0);
|
||||
}
|
||||
}
|
||||
) { }
|
||||
|
||||
+2
-16
@@ -10,6 +10,8 @@ public record InboundPlanResponse(
|
||||
String customerId,
|
||||
Long sourceOrderId,
|
||||
String containerNo,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String status,
|
||||
int lineCount,
|
||||
int palletCount,
|
||||
@@ -21,22 +23,6 @@ public record InboundPlanResponse(
|
||||
List<Allocation> allocations,
|
||||
int version
|
||||
) {
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines, pallets, allocations, 0);
|
||||
}
|
||||
|
||||
public InboundPlanResponse(Long id, String planNo, String customerId, Long sourceOrderId,
|
||||
String containerNo, String status, int lineCount, int palletCount,
|
||||
BigDecimal totalVolumeCbm, List<Line> lines, List<Pallet> pallets,
|
||||
List<Allocation> allocations, int version) {
|
||||
this(id, planNo, customerId, sourceOrderId, containerNo, status, lineCount, palletCount,
|
||||
totalVolumeCbm, lines.stream().mapToInt(Line::requiredPackageCount).sum(),
|
||||
allocations.stream().mapToInt(Allocation::pickedPackageCount).sum(), lines, pallets, allocations, version);
|
||||
}
|
||||
public record Line(Long id, Long orderId, String inboundCode, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
BigDecimal volumeCbm, int requiredPackageCount, int plannedPalletCount) {
|
||||
|
||||
+12
-2
@@ -14,6 +14,8 @@ import java.util.List;
|
||||
public interface InboundPlanOrderMapper {
|
||||
record ContainerOrderRef(String customerId, Long orderId, String currentStatus, String containerNo,
|
||||
String orderType) { }
|
||||
record ReceivingWarehouseSnapshot(String overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName) { }
|
||||
|
||||
@Select("SELECT customer_id AS customerId,id AS orderId,current_status AS currentStatus,container_no AS containerNo," +
|
||||
"order_type AS orderType " +
|
||||
@@ -26,7 +28,8 @@ public interface InboundPlanOrderMapper {
|
||||
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
|
||||
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
|
||||
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
|
||||
"COALESCE(o.pallet_count,0) AS recommendedPalletCount," +
|
||||
"COALESCE(o.pallet_count,0) AS recommendedPalletCount,o.overseas_warehouse_id AS overseasWarehouseId," +
|
||||
"o.overseas_warehouse_code AS overseasWarehouseCode,o.overseas_warehouse_name AS overseasWarehouseName," +
|
||||
"NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_id,''),MIN(NULLIF(d.destination_warehouse_id,''))) AS destinationWarehouseId," +
|
||||
"COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,''))) AS destinationWarehouseCode," +
|
||||
@@ -36,12 +39,19 @@ public interface InboundPlanOrderMapper {
|
||||
"0 AS destinationMismatchCount " +
|
||||
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
|
||||
"WHERE o.customer_id=#{customerId} AND o.id=#{orderId} " +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count,o.fba_warehouse_code," +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.pallet_count," +
|
||||
"o.overseas_warehouse_id,o.overseas_warehouse_code,o.overseas_warehouse_name,o.fba_warehouse_code," +
|
||||
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
|
||||
"ORDER BY o.id FOR UPDATE")
|
||||
List<SourceOrderSnapshot> lockOrder(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
|
||||
@Select("SELECT overseas_warehouse_id AS overseasWarehouseId," +
|
||||
"overseas_warehouse_code AS overseasWarehouseCode,overseas_warehouse_name AS overseasWarehouseName " +
|
||||
"FROM ll_order WHERE customer_id=#{customerId} AND id=#{orderId} LIMIT 1")
|
||||
ReceivingWarehouseSnapshot findReceivingWarehouse(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
|
||||
@Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," +
|
||||
"pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{orderId}")
|
||||
|
||||
+2
-1
@@ -25,7 +25,7 @@ public interface InboundPlanQueryMapper {
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.lxCode != null and q.lxCode != \"\"'> AND o.lx_code LIKE CONCAT('%',#{q.lxCode},'%')</if>" +
|
||||
"<if test='q.status != null and q.status != \"\"'> AND p.status=#{q.status}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND l.destination_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND o.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND l.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.warehouse != null and q.warehouse != \"\"'> AND (l.destination_warehouse_id LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_code LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_name LIKE CONCAT('%',#{q.warehouse},'%'))</if>" +
|
||||
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%')</if>" +
|
||||
@@ -38,6 +38,7 @@ public interface InboundPlanQueryMapper {
|
||||
|
||||
@Select("<script>SELECT l.id lineId,p.id planId,p.plan_no planNo,p.customer_id customerId," +
|
||||
"l.order_id orderId,l.inbound_code inboundCode,o.lx_code lxCode,o.order_type orderType," +
|
||||
"o.overseas_warehouse_code overseasWarehouseCode,o.overseas_warehouse_name overseasWarehouseName," +
|
||||
"l.destination_warehouse_id warehouseId,l.destination_warehouse_code warehouseCode," +
|
||||
"l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," +
|
||||
"o.customer_name customerName,l.required_package_count packageCount," +
|
||||
|
||||
+4
-21
@@ -12,30 +12,13 @@ public record SourceOrderSnapshot(
|
||||
BigDecimal volumeCbm,
|
||||
int packageCount,
|
||||
int recommendedPalletCount,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String fbaWarehouseCode,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String destinationWarehouseName,
|
||||
int destinationCount,
|
||||
int destinationMismatchCount
|
||||
) {
|
||||
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
|
||||
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
|
||||
int packageCount, String fbaWarehouseCode, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int destinationCount, int destinationMismatchCount) {
|
||||
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
packageCount, 0, fbaWarehouseCode, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, destinationCount, destinationMismatchCount);
|
||||
}
|
||||
|
||||
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
|
||||
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
|
||||
int packageCount, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int destinationCount, int destinationMismatchCount) {
|
||||
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
packageCount, 0, null, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, destinationCount, destinationMismatchCount);
|
||||
}
|
||||
}
|
||||
) { }
|
||||
|
||||
+28
-4
@@ -6,22 +6,46 @@ import java.util.List;
|
||||
public final class ReceivingContainerResponses {
|
||||
private ReceivingContainerResponses() { }
|
||||
|
||||
public record Candidate(String sourceContainerBookingId, String containerNo, String status,
|
||||
public record Candidate(String sourceContainerBookingId, String containerNo,
|
||||
String overseasWarehouseCode, String overseasWarehouseName, String status,
|
||||
int totalOrderCount, int inboundCodeCount, int missingInboundCodeCount,
|
||||
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
|
||||
List<String> destinationCodes, int plannedPalletCount,
|
||||
int totalPackages, int receivedPackages,
|
||||
boolean operable, boolean allPlansReady, String blockReason,
|
||||
LocalDateTime plannedArrivalAt, boolean currentAccount) { }
|
||||
LocalDateTime plannedArrivalAt, boolean currentAccount) {
|
||||
public Candidate(String sourceContainerBookingId, String containerNo, String status,
|
||||
int totalOrderCount, int inboundCodeCount, int missingInboundCodeCount,
|
||||
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
|
||||
List<String> destinationCodes, int plannedPalletCount, int totalPackages,
|
||||
int receivedPackages, boolean operable, boolean allPlansReady,
|
||||
String blockReason, LocalDateTime plannedArrivalAt, boolean currentAccount) {
|
||||
this(sourceContainerBookingId, containerNo, null, null, status, totalOrderCount,
|
||||
inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount,
|
||||
pendingPlanCount, destinationCodes, plannedPalletCount, totalPackages,
|
||||
receivedPackages, operable, allPlansReady, blockReason, plannedArrivalAt, currentAccount);
|
||||
}
|
||||
}
|
||||
|
||||
public record WarehouseProfile(String warehouseId, String warehouseCode, String warehouseName) { }
|
||||
|
||||
public record Session(String containerNo, Long overseasWarehouseId, String overseasWarehouseName,
|
||||
public record Session(String containerNo, Long overseasWarehouseId, String overseasWarehouseCode,
|
||||
String overseasWarehouseName,
|
||||
String status, String operatorName, int version,
|
||||
List<SignedPhoto> photos, LocalDateTime preparedAt, LocalDateTime signedAt,
|
||||
LocalDateTime receivingAt, LocalDateTime completedAt,
|
||||
int totalPackages, int receivedPackages, int pendingPackages,
|
||||
List<Inbound> inbounds) { }
|
||||
List<Inbound> inbounds) {
|
||||
public Session(String containerNo, Long overseasWarehouseId, String overseasWarehouseName,
|
||||
String status, String operatorName, int version, List<SignedPhoto> photos,
|
||||
LocalDateTime preparedAt, LocalDateTime signedAt, LocalDateTime receivingAt,
|
||||
LocalDateTime completedAt, int totalPackages, int receivedPackages,
|
||||
int pendingPackages, List<Inbound> inbounds) {
|
||||
this(containerNo, overseasWarehouseId, null, overseasWarehouseName, status, operatorName,
|
||||
version, photos, preparedAt, signedAt, receivingAt, completedAt, totalPackages,
|
||||
receivedPackages, pendingPackages, inbounds);
|
||||
}
|
||||
}
|
||||
|
||||
public record SignedPhoto(ReceivingPhotoRole role, String fileId, String downloadPath) { }
|
||||
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ public class ReceivingContainerSession {
|
||||
private String containerFactHash;
|
||||
private String containerNo;
|
||||
private Long overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
private String overseasWarehouseName;
|
||||
private String operatorKey;
|
||||
private String operatorName;
|
||||
|
||||
+18
-34
@@ -126,7 +126,7 @@ public class ReceivingContainerService {
|
||||
public ReceivingContainerResponses.Session detail(String operatorKey, long overseasWarehouseId, String containerNo) {
|
||||
List<InboundPlan> containerPlans = requirePlans(containerNo);
|
||||
String customerId = requireSingleCustomer(containerPlans, containerNo);
|
||||
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw notFound("Container is assigned to another overseas warehouse");
|
||||
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw conflict("当前柜已分配至其他海外接柜仓,无法在本仓操作");
|
||||
requireReceivingAvailable(customerId, containerNo);
|
||||
ReceivingContainerSession current = sessions.findActiveByOperator(activeOperatorKey(operatorKey));
|
||||
requireSessionWarehouse(current, overseasWarehouseId);
|
||||
@@ -158,7 +158,7 @@ public class ReceivingContainerService {
|
||||
}
|
||||
List<InboundPlan> containerPlans = requirePlans(containerNo);
|
||||
String customerId = requireSingleCustomer(containerPlans, containerNo);
|
||||
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw conflict("Container is assigned to another overseas warehouse");
|
||||
if (!warehouseEligible(containerPlans, overseasWarehouseId)) throw conflict("当前柜已分配至其他海外接柜仓,无法在本仓操作");
|
||||
requireReceivingAvailable(customerId, containerNo);
|
||||
String activeOperatorKey = activeOperatorKey(operatorKey);
|
||||
ReceivingContainerSession active = sessions.lockActiveByOperator(activeOperatorKey);
|
||||
@@ -167,7 +167,6 @@ public class ReceivingContainerService {
|
||||
if (!containerNo.equals(active.getContainerNo())) throw conflict("Finish the current container before selecting another one");
|
||||
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
|
||||
warehouse.requireActiveSharedWarehouse(overseasWarehouseId);
|
||||
claimReceivingWarehouse(customerId, containerPlans, receivingWarehouse);
|
||||
ReceivingContainerResponses.Session result = snapshot(active, containerPlans);
|
||||
save(active, "PREPARE", request.idempotencyKey(), command, result); return result;
|
||||
}
|
||||
@@ -176,13 +175,13 @@ public class ReceivingContainerService {
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference receivingWarehouse =
|
||||
warehouse.requireActiveSharedWarehouse(overseasWarehouseId);
|
||||
claimReceivingWarehouse(customerId, containerPlans, receivingWarehouse);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ReceivingContainerSession session = new ReceivingContainerSession();
|
||||
session.setId(IdWorker.getId()); session.setCustomerId(customerId); session.setContainerNo(containerNo);
|
||||
session.setSourceContainerBookingId(readiness.sourceContainerBookingId());
|
||||
session.setContainerFactHash(readiness.factHash());
|
||||
session.setOverseasWarehouseId(overseasWarehouseId);
|
||||
session.setOverseasWarehouseCode(receivingWarehouse.warehouseCode());
|
||||
session.setOverseasWarehouseName(receivingWarehouse.warehouseName());
|
||||
session.setOperatorKey(operatorKey); session.setOperatorName(operatorName);
|
||||
session.setActiveOperatorKey(activeOperatorKey); session.setActiveContainerKey(activeContainerKey(containerNo));
|
||||
@@ -412,7 +411,7 @@ public class ReceivingContainerService {
|
||||
ContainerReadiness value = readiness(bookingIds.iterator().next(), orderFacts, planFacts, lineFacts,
|
||||
overseasWarehouseId);
|
||||
if (value == null || !value.operable()) {
|
||||
throw conflict(value == null ? "Container is assigned to another overseas warehouse" : value.blockReason());
|
||||
throw conflict(value == null ? "当前柜已分配至其他海外接柜仓,无法在本仓操作" : value.blockReason());
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -444,8 +443,13 @@ public class ReceivingContainerService {
|
||||
try { assignedWarehouses.add(Long.valueOf(value)); }
|
||||
catch (NumberFormatException ignored) { assignedWarehouses.add(Long.MIN_VALUE); }
|
||||
});
|
||||
if (assignedWarehouses.size() > 1) throw conflict("Container plans have inconsistent receiving warehouse assignments");
|
||||
if (!assignedWarehouses.isEmpty() && !assignedWarehouses.contains(overseasWarehouseId)) return null;
|
||||
if (assignedWarehouses.size() > 1) throw conflict("柜内接柜仓分配不一致");
|
||||
if (assignedWarehouses.size() != 1 || !assignedWarehouses.contains(overseasWarehouseId)) return null;
|
||||
Set<String> warehouseCodes = orderFacts.stream().map(LastlegOrder::getOverseasWarehouseCode)
|
||||
.map(ReceivingContainerService::trim).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
Set<String> warehouseNames = orderFacts.stream().map(LastlegOrder::getOverseasWarehouseName)
|
||||
.map(ReceivingContainerService::trim).filter(Objects::nonNull).collect(Collectors.toSet());
|
||||
if (warehouseCodes.size() != 1 || warehouseNames.size() != 1) throw conflict("柜内接柜仓业务快照不一致");
|
||||
Set<String> customers = orderFacts.stream().map(LastlegOrder::getCustomerId).collect(Collectors.toSet());
|
||||
String containerNo = orderFacts.stream().map(LastlegOrder::getContainerNo).filter(Objects::nonNull).findFirst().orElse(null);
|
||||
int totalOrders = orderFacts.size();
|
||||
@@ -488,7 +492,8 @@ public class ReceivingContainerService {
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::plannedPalletCount).sum(),
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum(),
|
||||
inboundValues.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum());
|
||||
return new ContainerReadiness(sourceContainerBookingId, containerNo, totalOrders, inboundCodes,
|
||||
return new ContainerReadiness(sourceContainerBookingId, containerNo, warehouseCodes.iterator().next(),
|
||||
warehouseNames.iterator().next(), totalOrders, inboundCodes,
|
||||
totalOrders - inboundCodes, planCount, plannedOrders, pending, allPlansReady, allPlansCompleted, shippingReady, operable,
|
||||
blockReason, arrival, factHash, totals);
|
||||
}
|
||||
@@ -498,6 +503,7 @@ public class ReceivingContainerService {
|
||||
boolean active = current != null && value.containerNo().equals(current.getContainerNo());
|
||||
Totals totals = value.totals();
|
||||
return new ReceivingContainerResponses.Candidate(value.sourceContainerBookingId(), value.containerNo(),
|
||||
value.overseasWarehouseCode(), value.overseasWarehouseName(),
|
||||
active ? current.getStatus() : "WAIT_SIGN", value.totalOrderCount(), value.inboundCodeCount(),
|
||||
value.missingInboundCodeCount(), value.planCount(), value.plannedInboundCodeCount(),
|
||||
value.pendingPlanCount(), totals.destinationCodes(), totals.plannedPalletCount(), totals.total(),
|
||||
@@ -663,7 +669,7 @@ public class ReceivingContainerService {
|
||||
int total = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum();
|
||||
int received = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum();
|
||||
return new ReceivingContainerResponses.Session(session.getContainerNo(), session.getOverseasWarehouseId(),
|
||||
session.getOverseasWarehouseName(), session.getStatus(), session.getOperatorName(),
|
||||
session.getOverseasWarehouseCode(), session.getOverseasWarehouseName(), session.getStatus(), session.getOperatorName(),
|
||||
value(session.getVersion()), signPhotos(session), session.getPreparedAt(), session.getSignedAt(),
|
||||
session.getReceivingAt(), session.getCompletedAt(), total, received, Math.max(0, total - received), inbounds);
|
||||
}
|
||||
@@ -864,6 +870,7 @@ public class ReceivingContainerService {
|
||||
String containerNo) {
|
||||
ReceivingContainerSession value = new ReceivingContainerSession(); value.setCustomerId(customerId);
|
||||
value.setOverseasWarehouseId(warehouse.warehouseId());
|
||||
value.setOverseasWarehouseCode(warehouse.warehouseCode());
|
||||
value.setOverseasWarehouseName(warehouse.warehouseName());
|
||||
value.setOperatorKey(operatorKey); value.setContainerNo(containerNo); value.setStatus("WAIT_SIGN"); value.setVersion(0);
|
||||
return value;
|
||||
@@ -881,7 +888,7 @@ public class ReceivingContainerService {
|
||||
if (assigned.size() > 1) {
|
||||
throw conflict("Container plans have inconsistent receiving warehouse assignments");
|
||||
}
|
||||
return assigned.isEmpty() || assigned.contains(overseasWarehouseId);
|
||||
return assigned.size() == 1 && assigned.contains(overseasWarehouseId);
|
||||
}
|
||||
|
||||
private void requireReceivingAvailable(String customerId, String containerNo) {
|
||||
@@ -890,30 +897,6 @@ public class ReceivingContainerService {
|
||||
}
|
||||
}
|
||||
|
||||
private void claimReceivingWarehouse(String customerId, List<InboundPlan> containerPlans,
|
||||
WarehouseResourceGateway.WarehouseReference warehouse) {
|
||||
long overseasWarehouseId = warehouse.warehouseId();
|
||||
String warehouseId = String.valueOf(overseasWarehouseId);
|
||||
for (InboundPlan plan : containerPlans) {
|
||||
int claimed = plans.claimReceivingWarehouse(customerId, plan.getId(), overseasWarehouseId);
|
||||
InboundPlan actual = plans.selectById(plan.getId());
|
||||
if ((claimed == 0 && actual == null) || actual == null
|
||||
|| !Objects.equals(actual.getOverseasWarehouseId(), overseasWarehouseId)) {
|
||||
throw conflict("Container was claimed by another overseas warehouse");
|
||||
}
|
||||
plan.setOverseasWarehouseId(overseasWarehouseId);
|
||||
for (InboundPlanLine line : lines.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getPlanId, plan.getId()))) {
|
||||
orders.claimReceivingWarehouse(customerId, line.getOrderId(), warehouseId,
|
||||
warehouse.warehouseName());
|
||||
LastlegOrder order = orders.selectById(line.getOrderId());
|
||||
if (order == null || !warehouseId.equals(order.getOverseasWarehouseId())) {
|
||||
throw conflict("Inbound order was claimed by another overseas warehouse");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireSessionWarehouse(ReceivingContainerSession session, long overseasWarehouseId) {
|
||||
if (session != null && !Objects.equals(session.getOverseasWarehouseId(), overseasWarehouseId)) {
|
||||
throw conflict("Current container belongs to another overseas warehouse");
|
||||
@@ -990,6 +973,7 @@ public class ReceivingContainerService {
|
||||
private record Totals(int inboundCount, List<String> destinationCodes, int plannedPalletCount,
|
||||
int total, int received) { }
|
||||
private record ContainerReadiness(String sourceContainerBookingId, String containerNo,
|
||||
String overseasWarehouseCode, String overseasWarehouseName,
|
||||
int totalOrderCount, int inboundCodeCount, int missingInboundCodeCount,
|
||||
int planCount, int plannedInboundCodeCount, int pendingPlanCount,
|
||||
boolean allPlansReady, boolean allPlansCompleted, boolean shippingReady, boolean operable, String blockReason,
|
||||
|
||||
+19
-1
@@ -126,6 +126,15 @@ public class InboundPlanService {
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(selectedOrderId);
|
||||
plan.setContainerNo(containerNo);
|
||||
String receivingWarehouseId = trimToNull(orders.get(0).overseasWarehouseId());
|
||||
if (receivingWarehouseId == null || orders.stream().anyMatch(order ->
|
||||
!Objects.equals(receivingWarehouseId, trimToNull(order.overseasWarehouseId())))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓未预分配或存在不一致");
|
||||
}
|
||||
try { plan.setOverseasWarehouseId(Long.parseLong(receivingWarehouseId)); }
|
||||
catch (NumberFormatException exception) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "接柜仓ID格式无效", exception);
|
||||
}
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(orders.size());
|
||||
plan.setTotalVolumeCbm(orders.stream().map(SourceOrderSnapshot::volumeCbm)
|
||||
@@ -495,6 +504,13 @@ public class InboundPlanService {
|
||||
if (customerId != null && !customerId.equals(plan.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
InboundPlanOrderMapper.ReceivingWarehouseSnapshot receivingWarehouse = orderMapper.findReceivingWarehouse(
|
||||
plan.getCustomerId(), plan.getSourceOrderId());
|
||||
if (receivingWarehouse == null || trimToNull(receivingWarehouse.overseasWarehouseId()) == null
|
||||
|| trimToNull(receivingWarehouse.overseasWarehouseCode()) == null
|
||||
|| trimToNull(receivingWarehouse.overseasWarehouseName()) == null) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "入库计划关联订单缺少接柜仓预分配事实");
|
||||
}
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
.eq(InboundPlanLine::getPlanId, plan.getId()).orderByAsc(InboundPlanLine::getId));
|
||||
List<InboundPlanPallet> pallets = palletMapper.selectList(new LambdaQueryWrapper<InboundPlanPallet>()
|
||||
@@ -504,8 +520,10 @@ public class InboundPlanService {
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanPalletAllocation::getId));
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getSourceOrderId(),
|
||||
plan.getContainerNo(),
|
||||
plan.getContainerNo(), receivingWarehouse.overseasWarehouseCode(), receivingWarehouse.overseasWarehouseName(),
|
||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
|
||||
lines.stream().mapToInt(InboundPlanLine::getRequiredPackageCount).sum(),
|
||||
allocations.stream().mapToInt(InboundPlanPalletAllocation::getPickedPackageCount).sum(),
|
||||
lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(),
|
||||
line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(),
|
||||
line.getVolumeCbm(), line.getRequiredPackageCount(), line.getPlannedPalletCount())).toList(),
|
||||
|
||||
@@ -28,6 +28,13 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
String CONTAINER_PROGRESS_COLUMNS = "SELECT o.source_container_booking_id AS sourceContainerBookingId," +
|
||||
"CASE WHEN COUNT(NULLIF(o.container_no,''))=COUNT(*) AND COUNT(DISTINCT NULLIF(o.container_no,''))=1 " +
|
||||
"THEN MAX(NULLIF(o.container_no,'')) ELSE NULL END AS containerNo," +
|
||||
"CASE WHEN COUNT(DISTINCT NULLIF(o.overseas_warehouse_id,''))=1 THEN MAX(o.overseas_warehouse_id) ELSE NULL END AS overseasWarehouseId," +
|
||||
"CASE WHEN COUNT(DISTINCT NULLIF(o.overseas_warehouse_code,''))=1 THEN MAX(o.overseas_warehouse_code) ELSE NULL END AS overseasWarehouseCode," +
|
||||
"CASE WHEN COUNT(DISTINCT NULLIF(o.overseas_warehouse_name,''))=1 THEN MAX(o.overseas_warehouse_name) ELSE NULL END AS overseasWarehouseName," +
|
||||
"CASE WHEN COUNT(DISTINCT o.receiving_warehouse_version)=1 THEN MAX(o.receiving_warehouse_version) ELSE -1 END AS assignmentVersion," +
|
||||
"CASE WHEN COUNT(DISTINCT o.receiving_warehouse_version)=1 AND NOT EXISTS (SELECT 1 FROM ll_receiving_container_session s WHERE s.source_container_booking_id=o.source_container_booking_id) THEN TRUE ELSE FALSE END AS canChangeReceivingWarehouse," +
|
||||
"CASE WHEN COUNT(DISTINCT o.receiving_warehouse_version)<>1 THEN '柜内接柜仓分配版本不一致' " +
|
||||
"WHEN EXISTS (SELECT 1 FROM ll_receiving_container_session s WHERE s.source_container_booking_id=o.source_container_booking_id) THEN '海外PDA已建立接柜会话,接柜仓已永久锁定' ELSE NULL END AS receivingWarehouseLockReason," +
|
||||
"COUNT(*) AS totalOrderCount,COUNT(DISTINCT NULLIF(TRIM(o.inbound_code),'')) AS inboundCodeCount," +
|
||||
"SUM(CASE WHEN NULLIF(TRIM(o.inbound_code),'') IS NULL THEN 1 ELSE 0 END) AS missingInboundCodeCount," +
|
||||
"COUNT(DISTINCT p.id) AS planCount," +
|
||||
@@ -114,6 +121,25 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
List<String> lockContainerNumbers(
|
||||
@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Select("SELECT * FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
|
||||
List<LastlegOrder> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_receiving_container_session WHERE source_container_booking_id=#{sourceContainerBookingId}")
|
||||
long countReceivingSessions(@Param("sourceContainerBookingId") String sourceContainerBookingId);
|
||||
|
||||
@Update("UPDATE ll_order SET overseas_warehouse_id=#{warehouseId},overseas_warehouse_code=#{warehouseCode}," +
|
||||
"overseas_warehouse_name=#{warehouseName},receiving_warehouse_version=receiving_warehouse_version+1,updated_at=NOW(3) " +
|
||||
"WHERE source_container_booking_id=#{sourceContainerBookingId} AND receiving_warehouse_version=#{expectedVersion}")
|
||||
int updateContainerReceivingWarehouse(@Param("sourceContainerBookingId") String sourceContainerBookingId,
|
||||
@Param("warehouseId") String warehouseId, @Param("warehouseCode") String warehouseCode,
|
||||
@Param("warehouseName") String warehouseName, @Param("expectedVersion") int expectedVersion);
|
||||
|
||||
@Update("UPDATE ll_inbound_plan p JOIN ll_order o ON o.id=p.source_order_id AND o.customer_id=p.customer_id " +
|
||||
"SET p.overseas_warehouse_id=#{warehouseId},p.version=p.version+1,p.updated_at=NOW(3) " +
|
||||
"WHERE o.source_container_booking_id=#{sourceContainerBookingId}")
|
||||
int updateContainerPlanReceivingWarehouse(@Param("sourceContainerBookingId") String sourceContainerBookingId,
|
||||
@Param("warehouseId") long warehouseId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_order WHERE customer_id=#{customerId} AND container_no=#{containerNo}")
|
||||
long countContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
|
||||
|
||||
@@ -135,13 +161,6 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
int transferPickedToShipped(@Param("customerId") String customerId, @Param("id") Long id,
|
||||
@Param("packages") long packages, @Param("outboundNo") String outboundNo);
|
||||
|
||||
@Update("UPDATE ll_order SET overseas_warehouse_id=#{warehouseId},overseas_warehouse_name=#{warehouseName}," +
|
||||
"updated_at=NOW(3) WHERE id=#{orderId} AND customer_id=#{customerId} " +
|
||||
"AND (overseas_warehouse_id IS NULL OR overseas_warehouse_id=#{warehouseId})")
|
||||
int claimReceivingWarehouse(@Param("customerId") String customerId, @Param("orderId") long orderId,
|
||||
@Param("warehouseId") String warehouseId,
|
||||
@Param("warehouseName") String warehouseName);
|
||||
|
||||
@Update("UPDATE ll_order SET pallet_merge_flag=#{flag}," +
|
||||
"pallet_merge_batch_id=CASE WHEN #{flag} IS NULL THEN NULL ELSE pallet_merge_batch_id END," +
|
||||
"updated_at=#{updatedAt} " +
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface ReceivingWarehouseOperationMapper extends BaseMapper<ReceivingWarehouseOperation> {
|
||||
@Select("SELECT * FROM ll_receiving_warehouse_operation WHERE idempotency_key=#{key} LIMIT 1")
|
||||
ReceivingWarehouseOperation findByKey(@Param("key") String key);
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderFee;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
|
||||
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
@@ -23,6 +25,7 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||||
import com.baiyue.lastleg.model.LastlegOrderType;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -37,6 +40,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
@@ -60,6 +64,8 @@ public class LastlegOrderService {
|
||||
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Resource private WarehouseResourceGateway warehouseResourceGateway;
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
@@ -99,9 +105,58 @@ public class LastlegOrderService {
|
||||
orderMapper.findContainerProgressItems(bookingId));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ContainerPlanProgressSummary changeReceivingWarehouse(String sourceContainerBookingId,
|
||||
String idempotencyKey, LastlegOrderRequests.ChangeReceivingWarehouse request, String operatorName) {
|
||||
String bookingId = normalizeBookingId(sourceContainerBookingId);
|
||||
String key = normalizeIdempotencyKey(idempotencyKey);
|
||||
String requestHash = hash(bookingId + "\n" + request.overseasWarehouseId() + "\n"
|
||||
+ request.expectedVersion() + "\n" + Objects.toString(request.remark(), ""));
|
||||
ReceivingWarehouseOperation replay = receivingWarehouseOperations.findByKey(key);
|
||||
if (replay != null) {
|
||||
if (!bookingId.equals(replay.getSourceContainerBookingId()) || !requestHash.equals(replay.getRequestHash())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Idempotency-Key 已用于其他接柜仓修改请求");
|
||||
}
|
||||
return requireFrozenContainer(orderMapper.findContainerProgress(bookingId));
|
||||
}
|
||||
|
||||
List<LastlegOrder> orders = orderMapper.lockContainerOrders(bookingId);
|
||||
if (orders.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||||
int version = requireSingleAssignmentVersion(orders);
|
||||
if (version != request.expectedVersion()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "接柜仓分配版本已变化,请刷新后重试");
|
||||
}
|
||||
if (orderMapper.countReceivingSessions(bookingId) > 0) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"海外PDA已建立接柜会话,接柜仓已永久锁定");
|
||||
}
|
||||
WarehouseResourceGateway.WarehouseReference warehouse =
|
||||
warehouseResourceGateway.requireActiveSharedWarehouse(request.overseasWarehouseId());
|
||||
int updated = orderMapper.updateContainerReceivingWarehouse(bookingId,
|
||||
Long.toString(warehouse.warehouseId()), warehouse.warehouseCode(), warehouse.warehouseName(), version);
|
||||
if (updated != orders.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓分配发生并发变化");
|
||||
}
|
||||
orderMapper.updateContainerPlanReceivingWarehouse(bookingId, warehouse.warehouseId());
|
||||
ReceivingWarehouseOperation operation = new ReceivingWarehouseOperation();
|
||||
operation.setId(IdWorker.getId()); operation.setSourceContainerBookingId(bookingId);
|
||||
operation.setIdempotencyKey(key); operation.setRequestHash(requestHash);
|
||||
operation.setResultVersion(version + 1); operation.setOperatorName(operatorName);
|
||||
operation.setRemark(request.remark() == null ? null : request.remark().trim());
|
||||
operation.setCreatedAt(LocalDateTime.now()); receivingWarehouseOperations.insert(operation);
|
||||
return requireFrozenContainer(orderMapper.findContainerProgress(bookingId));
|
||||
}
|
||||
|
||||
private ContainerPlanProgressSummary requireFrozenContainer(ContainerPlanProgressSummary summary) {
|
||||
if (summary == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
|
||||
try {
|
||||
ContainerNumbers.requireCanonical(summary.containerNo());
|
||||
if (!StringUtils.hasText(summary.overseasWarehouseId())
|
||||
|| !StringUtils.hasText(summary.overseasWarehouseCode())
|
||||
|| !StringUtils.hasText(summary.overseasWarehouseName()) || summary.assignmentVersion() < 0) {
|
||||
throw new IllegalArgumentException("receiving warehouse assignment drift");
|
||||
}
|
||||
return summary;
|
||||
} catch (IllegalArgumentException exception) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
@@ -109,6 +164,42 @@ public class LastlegOrderService {
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeBookingId(String value) {
|
||||
String result = value == null ? null : value.trim();
|
||||
if (!StringUtils.hasText(result) || result.length() > 128) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能为空");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String normalizeIdempotencyKey(String value) {
|
||||
String result = value == null ? null : value.trim();
|
||||
if (!StringUtils.hasText(result) || result.length() > 255) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Idempotency-Key 必填且不得超过255字符");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private int requireSingleAssignmentVersion(List<LastlegOrder> orders) {
|
||||
Integer version = orders.get(0).getReceivingWarehouseVersion();
|
||||
if (version == null || orders.stream().anyMatch(order -> !Objects.equals(version,
|
||||
order.getReceivingWarehouseVersion()) || !StringUtils.hasText(order.getOverseasWarehouseId())
|
||||
|| !StringUtils.hasText(order.getOverseasWarehouseCode())
|
||||
|| !StringUtils.hasText(order.getOverseasWarehouseName()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内接柜仓分配事实不一致");
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
private String hash(String value) {
|
||||
try {
|
||||
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (Exception exception) {
|
||||
throw new IllegalStateException(exception);
|
||||
}
|
||||
}
|
||||
|
||||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||||
LastlegOrder order = findOwned(customerId, orderId);
|
||||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
@@ -434,7 +525,7 @@ public class LastlegOrderService {
|
||||
return new LastlegOrderSummary(o.getId(), o.getOrderNo(), o.getOrderType(),
|
||||
LastlegOrderType.labelOf(o.getOrderType()), o.getDestinationType(), o.getSourceInboundId(),
|
||||
displayInboundCode(o), o.getLxCode(), o.getSourceOrderNo(), o.getCustomerId(), o.getCustomerName(), o.getOverseasWarehouseId(),
|
||||
o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||||
o.getOverseasWarehouseCode(), o.getOverseasWarehouseName(), o.getTransferWarehouseId(), o.getTransferWarehouseCode(),
|
||||
o.getTransferWarehouseName(), o.getFbaWarehouseCode(), o.getContainerNo(), o.getContainerStatus(),
|
||||
o.getAbnormal(), o.getDetained(), o.getPalletCount(), o.getPackageCount(), o.getVolumeCbm(),
|
||||
o.getProductQuantity(), o.getVolumeWeightKg(), o.getEstimatedArrivalAt(), o.getActualArrivalAt(),
|
||||
|
||||
+7
-1
@@ -30,6 +30,9 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Service
|
||||
public class LoadingCompletedConsumer {
|
||||
static final String DEFAULT_RECEIVING_WAREHOUSE_ID = "193000000000200003";
|
||||
static final String DEFAULT_RECEIVING_WAREHOUSE_CODE = "US-LAX-01";
|
||||
static final String DEFAULT_RECEIVING_WAREHOUSE_NAME = "洛杉矶仓";
|
||||
@Resource private LastlegEventInboxMapper inboxMapper;
|
||||
@Resource private LastlegOrderMapper orderMapper;
|
||||
@Resource private LastlegInboundOrderMapper inboundOrderMapper;
|
||||
@@ -240,7 +243,10 @@ public class LoadingCompletedConsumer {
|
||||
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
|
||||
order.setCustomerName(customerName);
|
||||
order.setSourceWarehouseId(payload.warehouseId()); order.setSourceWarehouseName(payload.warehouseName());
|
||||
order.setOverseasWarehouseId(null); order.setOverseasWarehouseName(null);
|
||||
order.setOverseasWarehouseId(DEFAULT_RECEIVING_WAREHOUSE_ID);
|
||||
order.setOverseasWarehouseCode(DEFAULT_RECEIVING_WAREHOUSE_CODE);
|
||||
order.setOverseasWarehouseName(DEFAULT_RECEIVING_WAREHOUSE_NAME);
|
||||
order.setReceivingWarehouseVersion(0);
|
||||
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(containerNo);
|
||||
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
|
||||
order.setPackageCount(source.packageCount());
|
||||
|
||||
@@ -58,8 +58,10 @@ CREATE TABLE IF NOT EXISTS ll_order (
|
||||
customer_name VARCHAR(255) COMMENT '货主名称',
|
||||
source_warehouse_id VARCHAR(128) COMMENT '来源国内装柜仓 ID 快照',
|
||||
source_warehouse_name VARCHAR(255) COMMENT '来源国内装柜仓名称快照',
|
||||
overseas_warehouse_id VARCHAR(128) COMMENT '海外实际收货仓 ID,由绑定仓库的PDA操作员认领柜号时冻结',
|
||||
overseas_warehouse_name VARCHAR(255) COMMENT '海外仓名称',
|
||||
overseas_warehouse_id VARCHAR(128) NOT NULL DEFAULT '193000000000200003' COMMENT '海外接柜仓ID,新产生普通尾程整柜默认洛杉矶仓',
|
||||
overseas_warehouse_code VARCHAR(128) NOT NULL DEFAULT 'US-LAX-01' COMMENT '海外接柜仓业务编码快照',
|
||||
overseas_warehouse_name VARCHAR(255) NOT NULL DEFAULT '洛杉矶仓' COMMENT '海外接柜仓名称快照',
|
||||
receiving_warehouse_version INT NOT NULL DEFAULT 0 COMMENT '柜级接柜仓分配乐观锁版本',
|
||||
transfer_warehouse_id VARCHAR(128) COMMENT '转入目的仓 ID',
|
||||
transfer_warehouse_code VARCHAR(128) COMMENT '转入目的仓编码',
|
||||
transfer_warehouse_name VARCHAR(255) COMMENT '转入目的仓名称',
|
||||
@@ -177,6 +179,20 @@ CREATE TABLE IF NOT EXISTS ll_order_operation_log (
|
||||
KEY idx_ll_order_operation_customer_order(customer_id,order_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='尾程订单操作记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_warehouse_operation (
|
||||
id BIGINT NOT NULL COMMENT '柜级接柜仓修改操作ID',
|
||||
source_container_booking_id VARCHAR(128) NOT NULL COMMENT '稳定来源柜预订ID',
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '平台请求幂等键',
|
||||
request_hash VARCHAR(64) NOT NULL COMMENT '规范化修改请求SHA-256摘要',
|
||||
result_version INT NOT NULL COMMENT '修改成功后的柜级分配版本',
|
||||
operator_name VARCHAR(128) NOT NULL COMMENT '平台操作人名称',
|
||||
remark VARCHAR(500) NULL COMMENT '接柜仓修改备注',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_receiving_warehouse_idempotency(idempotency_key),
|
||||
KEY idx_ll_receiving_warehouse_container(source_container_booking_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='柜级海外接柜仓修改幂等审计记录';
|
||||
|
||||
-- 派送单列表与详情快照、转派送费用及操作记录。
|
||||
CREATE TABLE IF NOT EXISTS ll_delivery_order (
|
||||
id BIGINT NOT NULL COMMENT '派送单主键ID',
|
||||
@@ -357,8 +373,10 @@ ALTER TABLE ll_order
|
||||
MODIFY COLUMN source_order_no VARCHAR(128) NULL COMMENT '来源订单号',
|
||||
MODIFY COLUMN root_order_id VARCHAR(128) NULL COMMENT '根订单 ID',
|
||||
MODIFY COLUMN customer_name VARCHAR(255) NULL COMMENT '货主名称',
|
||||
MODIFY COLUMN overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外仓 ID',
|
||||
MODIFY COLUMN overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称',
|
||||
MODIFY COLUMN overseas_warehouse_id VARCHAR(128) NOT NULL DEFAULT '193000000000200003' COMMENT '海外接柜仓ID,新产生普通尾程整柜默认洛杉矶仓',
|
||||
MODIFY COLUMN overseas_warehouse_code VARCHAR(128) NOT NULL DEFAULT 'US-LAX-01' COMMENT '海外接柜仓业务编码快照',
|
||||
MODIFY COLUMN overseas_warehouse_name VARCHAR(255) NOT NULL DEFAULT '洛杉矶仓' COMMENT '海外接柜仓名称快照',
|
||||
MODIFY COLUMN receiving_warehouse_version INT NOT NULL DEFAULT 0 COMMENT '柜级接柜仓分配乐观锁版本',
|
||||
MODIFY COLUMN fba_warehouse_code VARCHAR(128) NULL COMMENT 'FBA 仓库编码',
|
||||
MODIFY COLUMN container_no VARCHAR(64) NULL COMMENT '集装箱号',
|
||||
MODIFY COLUMN container_status VARCHAR(32) NULL DEFAULT '待转柜' COMMENT '货柜状态',
|
||||
@@ -485,7 +503,7 @@ CREATE TABLE IF NOT EXISTS ll_inbound_plan (
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '租户/客户账号标识',
|
||||
source_order_id BIGINT NOT NULL COMMENT '来源尾程订单ID,一张入仓单对应一张入库计划',
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号',
|
||||
overseas_warehouse_id BIGINT NULL COMMENT '海外实际收货共享物理仓ID,由PDA认领柜号时冻结',
|
||||
overseas_warehouse_id BIGINT NOT NULL DEFAULT 193000000000200003 COMMENT '平台预分配的海外实际收货共享物理仓ID',
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'PLANNED' COMMENT '入库计划状态',
|
||||
line_count INT NOT NULL DEFAULT 0 COMMENT '入仓码明细数量',
|
||||
pallet_count INT NOT NULL DEFAULT 0 COMMENT '系统自动分配托盘数量',
|
||||
@@ -851,7 +869,7 @@ WHERE (l.fba_warehouse_code IS NULL AND o.fba_warehouse_code IS NOT NULL)
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_container_session (
|
||||
id BIGINT NOT NULL COMMENT '柜号签收会话ID', customer_id VARCHAR(128) NOT NULL COMMENT '所选柜解析出的客户标识',
|
||||
source_container_booking_id VARCHAR(128) NOT NULL COMMENT '稳定来源柜预订ID', container_fact_hash VARCHAR(64) NOT NULL COMMENT '领取时整柜订单与计划事实摘要',
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号', overseas_warehouse_id BIGINT NOT NULL COMMENT '操作员Token绑定并冻结的海外实际收货共享物理仓ID', overseas_warehouse_name VARCHAR(255) NOT NULL COMMENT '仓库服务校验后的海外实际收货共享物理仓名称快照', operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识',
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号', overseas_warehouse_id BIGINT NOT NULL COMMENT '平台预分配并由操作员Token匹配的海外接柜仓ID', overseas_warehouse_code VARCHAR(128) NOT NULL COMMENT '海外接柜仓业务编码快照', overseas_warehouse_name VARCHAR(255) NOT NULL COMMENT '仓库服务校验后的海外接柜仓名称快照', operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识',
|
||||
operator_name VARCHAR(255) NULL COMMENT '操作账号名称', active_operator_key VARCHAR(255) NULL COMMENT '未完成会话平台操作账号全局唯一锁键',
|
||||
active_container_key VARCHAR(255) NULL COMMENT '未完成会话柜号全局唯一锁键', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_SIGN' COMMENT '会话状态:WAIT_SIGN、SIGNED、RECEIVING、COMPLETED',
|
||||
sign_photos JSON NULL COMMENT '签收卸柜照片角色与文件ID列表', prepared_at DATETIME(3) NULL COMMENT '选择柜号时间',
|
||||
|
||||
@@ -298,6 +298,23 @@ paths:
|
||||
code: { type: string, enum: ['200'] }
|
||||
data: { $ref: '#/components/schemas/ContainerPlanProgressDetail' }
|
||||
'404': { description: 稳定柜 ID 不存在 }
|
||||
/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}/receiving-warehouse:
|
||||
put:
|
||||
summary: 修改整柜海外接柜仓
|
||||
description: 权限 platform:overseas-warehouse:order:edit;目标必须为国内 Warehouse Service 有效 OVERSEAS 仓。使用柜级版本和 Idempotency-Key,事务内原子同步柜内全部尾程订单及已有入库计划;海外PDA一旦建立接柜会话即永久锁定。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: sourceContainerBookingId, in: path, required: true, schema: { type: string, minLength: 1, maxLength: 128 } }
|
||||
- { name: Idempotency-Key, in: header, required: true, schema: { type: string, minLength: 1, maxLength: 255 } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/ChangeReceivingWarehouseRequest' }
|
||||
responses:
|
||||
'200': { description: 更新后的柜级摘要, content: { application/json: { schema: { type: object, properties: { code: { type: string, enum: ['200'] }, data: { $ref: '#/components/schemas/ContainerPlanProgressSummary' } } } } }
|
||||
'400': { description: 请求无效或目标不是有效 OVERSEAS 仓 }
|
||||
'409': { description: 版本冲突、幂等键冲突或PDA接柜会话已建立 }
|
||||
/api/platform/v1/last-leg/orders/{id}/destination:
|
||||
put:
|
||||
summary: 将单票尾程订单转入 FBA 目的仓
|
||||
@@ -331,6 +348,39 @@ paths:
|
||||
'200': { description: 统一 ApiResponse 包装的更新订单数 }
|
||||
'400': { description: 所选FBA仓不存在、已停用、类型错误或字段不一致 }
|
||||
'503': { description: 国内FBA仓权威主数据暂不可用,写入失败关闭 }
|
||||
/api/platform/v1/last-leg/inbound-plans:
|
||||
get:
|
||||
summary: 平台分页查询海外入库订单
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;列表中的 overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实,warehouseCode/warehouseName 仍表示目的仓。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||
- { 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: planNo, in: query, schema: { type: string } }
|
||||
- { name: inboundCode, in: query, schema: { type: string } }
|
||||
- { name: lxCode, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: overseasWarehouseId, in: query, schema: { type: string } }
|
||||
- { name: destinationWarehouseCode, in: query, schema: { type: string } }
|
||||
- { name: warehouse, in: query, schema: { type: string } }
|
||||
- { name: fbaWarehouseCode, in: query, schema: { type: string } }
|
||||
- { name: appointmentStatus, in: query, schema: { type: string, enum: [UNAPPOINTED, PENDING, SUCCESS, FAILED, CANCELLED] } }
|
||||
- { name: dispatchStatus, in: query, schema: { type: string, enum: [UNSCHEDULED, SCHEDULED] } }
|
||||
- { name: createdFrom, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: createdTo, in: query, schema: { type: string, format: date-time } }
|
||||
responses: { '200': { description: 平台入库订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/InboundPlanPageResponse' } } } } }
|
||||
/api/platform/v1/last-leg/inbound-plans/{planNo}:
|
||||
get:
|
||||
summary: 平台查询海外入库订单详情
|
||||
description: 权限 platform:overseas-warehouse:inbound-order:list;详情中的 overseasWarehouseCode/overseasWarehouseName 只取当前计划来源尾程订单的预分配接柜仓事实。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: planNo, in: path, required: true, schema: { type: string } }
|
||||
- { name: customerId, in: query, required: false, schema: { type: string, pattern: '^[1-9][0-9]*$' } }
|
||||
responses:
|
||||
'200': { description: 平台入库订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/InboundPlanDetailResponse' } } } }
|
||||
'404': { description: 入库订单不存在或不属于指定客户 }
|
||||
/api/platform/v1/last-leg/inbound-plans/convert:
|
||||
post:
|
||||
summary: 平台生成入库计划
|
||||
@@ -811,7 +861,7 @@ paths:
|
||||
/api/merchant/v1/last-leg/inbound-plans:
|
||||
get:
|
||||
summary: 客户分页查询自己的海外入库订单
|
||||
description: 权限 merchant:warehouse:inbound:list;客户范围只取认证 Token,不接收 customerId。
|
||||
description: 权限 merchant:warehouse:inbound:list;客户范围只取认证 Token,不接收 customerId;overseasWarehouseCode/overseasWarehouseName 只取该客户尾程订单预分配接柜仓事实,warehouseCode/warehouseName 仍表示目的仓。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||
@@ -829,7 +879,7 @@ paths:
|
||||
/api/merchant/v1/last-leg/inbound-plans/{planNo}:
|
||||
get:
|
||||
summary: 客户查询自己的海外入库订单详情
|
||||
description: 权限 merchant:warehouse:inbound:list;列表权限包含详情只读,不接收 customerId。
|
||||
description: 权限 merchant:warehouse:inbound:list;列表权限包含详情只读,不接收 customerId;overseasWarehouseCode/overseasWarehouseName 只取该计划来源尾程订单的预分配接柜仓事实。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: planNo, in: path, required: true, schema: { type: string } }]
|
||||
responses: { '200': { description: 当前客户入库订单详情, content: { application/json: { schema: { $ref: '#/components/schemas/InboundPlanDetailResponse' } } } }, '404': { description: 当前客户下入库订单不存在 } }
|
||||
@@ -1093,10 +1143,16 @@ components:
|
||||
items: { type: string, pattern: '^[1-9][0-9]*$' }
|
||||
ContainerPlanProgressSummary:
|
||||
type: object
|
||||
required: [sourceContainerBookingId, containerNo, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, allPlansReady]
|
||||
required: [sourceContainerBookingId, containerNo, overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName, assignmentVersion, canChangeReceivingWarehouse, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, allPlansReady]
|
||||
properties:
|
||||
sourceContainerBookingId: { type: string }
|
||||
containerNo: { type: string, pattern: '^[A-Z]{3}[UJZ][0-9]{7}$', description: 装柜完成事件非空且通过 ISO 6346 校验位验证的规范化真实集装箱号;不使用 cabinetNo、订单号或内部 ID 回退 }
|
||||
overseasWarehouseId: { type: string, pattern: '^[1-9][0-9]*$', description: 平台预分配的海外接柜仓ID,不是FBA目的仓 }
|
||||
overseasWarehouseCode: { type: string, description: 海外接柜仓业务编码 }
|
||||
overseasWarehouseName: { type: string, description: 海外接柜仓业务名称 }
|
||||
assignmentVersion: { type: integer, minimum: 0, description: 柜级接柜仓乐观锁版本 }
|
||||
canChangeReceivingWarehouse: { type: boolean }
|
||||
receivingWarehouseLockReason: { type: string, nullable: true }
|
||||
totalOrderCount: { type: integer, format: int64 }
|
||||
inboundCodeCount: { type: integer, format: int64 }
|
||||
missingInboundCodeCount: { type: integer, format: int64 }
|
||||
@@ -1106,6 +1162,13 @@ components:
|
||||
plannedArrivalAt: { type: string, format: date-time, nullable: true, description: 仅来源于装柜完成事件 estimatedWarehouseArrivalAt 冻结的 ll_order.estimated_warehouse_at }
|
||||
allPlansReady: { type: boolean, description: 全柜每票均有入仓码且均存在有效独立计划时为 true }
|
||||
blockingReason: { type: string, nullable: true }
|
||||
ChangeReceivingWarehouseRequest:
|
||||
type: object
|
||||
required: [overseasWarehouseId, expectedVersion]
|
||||
properties:
|
||||
overseasWarehouseId: { type: integer, format: int64, minimum: 1 }
|
||||
expectedVersion: { type: integer, minimum: 0 }
|
||||
remark: { type: string, maxLength: 500, nullable: true }
|
||||
ContainerPlanProgressDetail:
|
||||
type: object
|
||||
required: [summary, orders]
|
||||
@@ -1329,10 +1392,12 @@ components:
|
||||
warehouseName: { type: string, description: 国内Warehouse Service主档维护的当前中文名称 }
|
||||
ReceivingCandidate:
|
||||
type: object
|
||||
required: [sourceContainerBookingId, containerNo, status, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, destinationCodes, plannedPalletCount, totalPackages, receivedPackages, operable, allPlansReady, currentAccount]
|
||||
required: [sourceContainerBookingId, containerNo, overseasWarehouseCode, overseasWarehouseName, status, totalOrderCount, inboundCodeCount, missingInboundCodeCount, planCount, plannedInboundCodeCount, pendingPlanCount, destinationCodes, plannedPalletCount, totalPackages, receivedPackages, operable, allPlansReady, currentAccount]
|
||||
properties:
|
||||
sourceContainerBookingId: { type: string, description: 头程与尾程共享的稳定柜身份 }
|
||||
containerNo: { type: string }
|
||||
overseasWarehouseCode: { type: string }
|
||||
overseasWarehouseName: { type: string }
|
||||
status: { type: string, enum: [WAIT_SIGN, SIGNED, RECEIVING, COMPLETED] }
|
||||
totalOrderCount: { type: integer, minimum: 1 }
|
||||
missingInboundCodeCount: { type: integer, minimum: 0 }
|
||||
@@ -1351,10 +1416,11 @@ components:
|
||||
currentAccount: { type: boolean }
|
||||
ReceivingSession:
|
||||
type: object
|
||||
required: [containerNo, overseasWarehouseId, overseasWarehouseName, status, version, photos, totalPackages, receivedPackages, pendingPackages, inbounds]
|
||||
required: [containerNo, overseasWarehouseId, overseasWarehouseCode, overseasWarehouseName, status, version, photos, totalPackages, receivedPackages, pendingPackages, inbounds]
|
||||
properties:
|
||||
containerNo: { type: string }
|
||||
overseasWarehouseId: { type: string, pattern: '^\d+$', description: PDA Token绑定并在认领柜号时冻结的共享实际海外收货仓ID }
|
||||
overseasWarehouseId: { type: string, pattern: '^\d+$', description: 平台预分配并与PDA Token匹配的海外接柜仓ID }
|
||||
overseasWarehouseCode: { type: string, description: 海外接柜仓业务编码快照 }
|
||||
overseasWarehouseName: { type: string, description: Warehouse Overseas服务校验后的共享实际海外收货仓名称快照 }
|
||||
status: { type: string, enum: [WAIT_SIGN, SIGNED, RECEIVING, COMPLETED] }
|
||||
operatorName: { type: string, nullable: true }
|
||||
@@ -1911,7 +1977,7 @@ components:
|
||||
properties: { records: { type: array, items: { $ref: '#/components/schemas/InboundPlanSummary' } } }
|
||||
InboundPlanSummary:
|
||||
type: object
|
||||
required: [lineId, planId, planNo, customerId, orderId, inboundCode, packageCount, pickedPackageCount, palletCount, status, createdAt, version]
|
||||
required: [lineId, planId, planNo, customerId, orderId, inboundCode, overseasWarehouseCode, overseasWarehouseName, packageCount, pickedPackageCount, palletCount, status, createdAt, version]
|
||||
properties:
|
||||
lineId: { type: string, pattern: '^[0-9]+$' }
|
||||
planId: { type: string, pattern: '^[0-9]+$' }
|
||||
@@ -1920,6 +1986,8 @@ components:
|
||||
orderId: { type: string, pattern: '^[0-9]+$' }
|
||||
inboundCode: { type: string }
|
||||
orderType: { type: string, nullable: true }
|
||||
overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }
|
||||
overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }
|
||||
warehouseId: { type: string, nullable: true }
|
||||
warehouseCode: { type: string, nullable: true }
|
||||
warehouseName: { type: string, nullable: true }
|
||||
@@ -1939,13 +2007,15 @@ components:
|
||||
version: { type: integer }
|
||||
InboundPlanDetail:
|
||||
type: object
|
||||
required: [id, planNo, customerId, status, lineCount, palletCount, totalVolumeCbm, expectedPackageCount, pickedPackageCount, lines, pallets, allocations, version]
|
||||
required: [id, planNo, customerId, overseasWarehouseCode, overseasWarehouseName, status, lineCount, palletCount, totalVolumeCbm, expectedPackageCount, pickedPackageCount, lines, pallets, allocations, version]
|
||||
properties:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
planNo: { type: string }
|
||||
customerId: { type: string }
|
||||
sourceOrderId: { type: string, pattern: '^[0-9]+$', nullable: true }
|
||||
containerNo: { type: string, nullable: true }
|
||||
overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }
|
||||
overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }
|
||||
status: { type: string }
|
||||
lineCount: { type: integer }
|
||||
palletCount: { type: integer }
|
||||
|
||||
+14
@@ -35,6 +35,20 @@ class DispatchCandidateMapperContractTest {
|
||||
assertTrue(sql.contains("pos.available_packages > 0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void candidateWarehouseComesFromPhysicalInventoryOrFrozenInboundPlan() throws Exception {
|
||||
for (Method method : List.of(
|
||||
DispatchCandidateMapper.class.getMethod("available", String.class, List.class, String.class,
|
||||
String.class, String.class, String.class),
|
||||
DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class))) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL"));
|
||||
assertTrue(sql.contains("pos.overseas_warehouse_id"));
|
||||
assertTrue(sql.contains("CAST(ip.overseas_warehouse_id AS CHAR)"));
|
||||
assertTrue(sql.contains("ll_inventory_balance"));
|
||||
}
|
||||
}
|
||||
|
||||
private void assertConstructorColumnOrder(Method method) {
|
||||
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||
int previous = -1;
|
||||
|
||||
+15
@@ -127,6 +127,21 @@ class DispatchServiceTest {
|
||||
verify(items).insert(any(DispatchItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void preArrangeRejectsPalletWithoutResolvedOverseasWarehouseWithPreciseReason() {
|
||||
DispatchPalletCandidate unresolved = new DispatchPalletCandidate(1L, 10L, 20L, 2L,
|
||||
null, null, "LMO1", "LMO1", "RK1", "LIP1-P001", "ONT8", "ONT8", "ONT8", "ONT8",
|
||||
12, new BigDecimal("1.2000"), "RECEIVED");
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(unresolved));
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.preArrange("100", "7",
|
||||
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
|
||||
|
||||
assertEquals("所选托盘中存在尚未关联海外仓的托盘", exception.getReason());
|
||||
verify(orders, never()).insert(any(DispatchOrder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void removingPalletFromOwnedDraftImmediatelyReleasesLock() {
|
||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||
|
||||
+16
@@ -17,5 +17,21 @@ class InboundPlanOrderMapperContractTest {
|
||||
assertTrue(sql.contains("COALESCE(NULLIF(o.transfer_warehouse_code,''),MIN(NULLIF(d.destination_warehouse_code,'')))"));
|
||||
assertTrue(sql.contains("THEN 1 ELSE COUNT(DISTINCT"));
|
||||
assertTrue(sql.contains("0 AS destinationMismatchCount"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_id AS overseasWarehouseId"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("o.overseas_warehouse_name AS overseasWarehouseName"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivingWarehouseSnapshotIsReadFromTenantScopedOrderFact() throws Exception {
|
||||
Select select = InboundPlanOrderMapper.class
|
||||
.getMethod("findReceivingWarehouse", String.class, Long.class)
|
||||
.getAnnotation(Select.class);
|
||||
String sql = String.join(" ", select.value());
|
||||
|
||||
assertTrue(sql.contains("overseas_warehouse_code AS overseasWarehouseCode"));
|
||||
assertTrue(sql.contains("overseas_warehouse_name AS overseasWarehouseName"));
|
||||
assertTrue(sql.contains("customer_id=#{customerId} AND id=#{orderId}"));
|
||||
assertTrue(!sql.contains("destination_warehouse"));
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -112,9 +112,6 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
when(sessions.insert(any(ReceivingContainerSession.class))).thenReturn(1);
|
||||
when(operations.insert(any(ReceivingContainerOperation.class))).thenReturn(1);
|
||||
when(warehouse.requireActiveSharedWarehouse(100L)).thenReturn(warehouseReference());
|
||||
when(plans.claimReceivingWarehouse("100", 2L, 100L)).thenReturn(1);
|
||||
InboundPlan claimed = plan("CONT-A"); claimed.setOverseasWarehouseId(100L);
|
||||
when(plans.selectById(2L)).thenReturn(claimed);
|
||||
var response = service.prepare("7", "receiver", 100L, "CONT-A",
|
||||
new ReceivingContainerRequests.Action("same-key"));
|
||||
assertThat(response.overseasWarehouseId()).isEqualTo(100L);
|
||||
@@ -652,7 +649,7 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static InboundPlan plan(String containerNo) {
|
||||
InboundPlan value = new InboundPlan();
|
||||
value.setId(2L); value.setPlanNo("PLAN-1"); value.setCustomerId("100"); value.setContainerNo(containerNo);
|
||||
value.setStatus("SIGNED");
|
||||
value.setStatus("SIGNED"); value.setOverseasWarehouseId(100L);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -668,7 +665,9 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static LastlegOrder order(String destinationType) {
|
||||
LastlegOrder value = new LastlegOrder();
|
||||
value.setId(1L); value.setCustomerId("100"); value.setDestinationType(destinationType);
|
||||
value.setOverseasWarehouseId("100"); value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
value.setOverseasWarehouseId("100"); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setFbaWarehouseCode("FBA".equals(destinationType) ? "FBA-1" : null);
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -690,7 +689,8 @@ class ReceivingContainerServiceBehaviorTest {
|
||||
private static ReceivingContainerSession session(String containerNo, String status) {
|
||||
ReceivingContainerSession value = new ReceivingContainerSession();
|
||||
value.setId(8L); value.setCustomerId("100"); value.setContainerNo(containerNo); value.setOperatorKey("7");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setOverseasWarehouseId(100L); value.setOverseasWarehouseCode("SHARED-LAX");
|
||||
value.setOverseasWarehouseName("Shared LAX");
|
||||
value.setStatus(status); value.setVersion(0);
|
||||
return value;
|
||||
}
|
||||
|
||||
+9
-3
@@ -48,9 +48,9 @@ class InboundPlanPaginationTest {
|
||||
null, "PENDING_ASSIGNMENT", null, null, "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
|
||||
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
|
||||
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
|
||||
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0));
|
||||
3L, "IN-1", null, "FIRST_LEG", "US-LAX-01", "洛杉矶仓", "W1", "ONT", "安大略仓",
|
||||
"ONT8", "客户A", 10, 0, BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null,
|
||||
null, "UNSCHEDULED", LocalDateTime.of(2026, 8, 20, 10, 0), 0);
|
||||
when(queryMapper.count("100", query)).thenReturn(11L);
|
||||
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
|
||||
|
||||
@@ -59,6 +59,8 @@ class InboundPlanPaginationTest {
|
||||
assertThat(result.records()).containsExactly(row);
|
||||
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
|
||||
assertThat(result.records().get(0).appointmentStatus()).isNull();
|
||||
assertThat(result.records().get(0).overseasWarehouseCode()).isEqualTo("US-LAX-01");
|
||||
assertThat(result.records().get(0).overseasWarehouseName()).isEqualTo("洛杉矶仓");
|
||||
assertThat(result.pages()).isEqualTo(2);
|
||||
verify(queryMapper).count("100", query);
|
||||
verify(queryMapper).rows("100", query, 10, 10);
|
||||
@@ -71,6 +73,10 @@ class InboundPlanPaginationTest {
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
|
||||
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
|
||||
assertThat(sql).contains("di.customer_id=#{customerId} AND </if>di.order_id=l.order_id");
|
||||
assertThat(sql).contains("o.overseas_warehouse_code overseasWarehouseCode",
|
||||
"o.overseas_warehouse_name overseasWarehouseName",
|
||||
"o.overseas_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).doesNotContain("l.destination_warehouse_id=#{q.overseasWarehouseId}");
|
||||
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.overseasWarehouseId",
|
||||
"q.destinationWarehouseCode", "q.warehouse",
|
||||
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
|
||||
|
||||
+45
-5
@@ -64,6 +64,9 @@ class InboundPlanServiceTest {
|
||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(orderMapper.findReceivingWarehouse(any(), any())).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot(
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,12 +108,15 @@ class InboundPlanServiceTest {
|
||||
verify(orderMapper).lockOrder("100", 2L);
|
||||
assertEquals(2L, result.sourceOrderId());
|
||||
assertEquals("MSCU6639870", result.containerNo());
|
||||
assertEquals("US-LAX-01", result.overseasWarehouseCode());
|
||||
assertEquals("洛杉矶仓", result.overseasWarehouseName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void convertsAConsistentDestinationAssignedOrder() {
|
||||
SourceOrderSnapshot assigned = new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10,
|
||||
"DESTINATION_ASSIGNED", null, new BigDecimal("1.0000"), 10, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"2090000000000000001", "LAX-02", "Los Angeles 02", 1, 0);
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(assigned));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
@@ -148,6 +154,32 @@ class InboundPlanServiceTest {
|
||||
verify(inventoryBalanceMapper, never()).bindInboundPlan(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailReturnsReceivingWarehouseFromTenantScopedOrderFact() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(
|
||||
new InboundPlanOrderMapper.ReceivingWarehouseSnapshot("88", "US-NY-01", "纽约仓"));
|
||||
|
||||
var result = service.detailByPlanNo("100", "LIP-1");
|
||||
|
||||
assertEquals("US-NY-01", result.overseasWarehouseCode());
|
||||
assertEquals("纽约仓", result.overseasWarehouseName());
|
||||
verify(orderMapper).findReceivingWarehouse("100", 1L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void detailFailsClosedWhenReceivingWarehouseFactIsMissing() {
|
||||
InboundPlan plan = existingPlan("100", 1L, "LIP-1");
|
||||
when(planMapper.selectOne(any())).thenReturn(plan);
|
||||
when(orderMapper.findReceivingWarehouse("100", 1L)).thenReturn(null);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class,
|
||||
() -> service.detailByPlanNo("100", "LIP-1"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsReversedInboundPlanDateRange() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null, null,
|
||||
@@ -160,7 +192,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void rejectsMissingDestinationWarehouse() {
|
||||
SourceOrderSnapshot invalid = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0);
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(invalid));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convert("100", 1L));
|
||||
@@ -190,7 +224,9 @@ class InboundPlanServiceTest {
|
||||
@Test
|
||||
void transferWarehouseOverridesOriginalCargoDestination() {
|
||||
SourceOrderSnapshot transferred = new SourceOrderSnapshot(1L, "100", "MSCU6639870", "RK-1",
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, "WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
"DESTINATION_ASSIGNED", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
"WH-B", "WH-B", "Warehouse B", 1, 1);
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(transferred));
|
||||
when(orderMapper.markPlanned(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt(), any()))
|
||||
.thenReturn(1);
|
||||
@@ -332,7 +368,9 @@ class InboundPlanServiceTest {
|
||||
new InboundPlanOrderMapper.ContainerOrderRef("100", 2L, "PENDING_ASSIGNMENT", "MSCU6639870", "STANDARD")));
|
||||
when(orderMapper.lockOrder("100", 1L)).thenReturn(List.of(order(1L, "RK-1", "1", "WH-A")));
|
||||
when(orderMapper.lockOrder("100", 2L)).thenReturn(List.of(new SourceOrderSnapshot(2L, "100", "MSCU6639870", "RK-2",
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, null, null, null, 1, 0)));
|
||||
"PENDING_ASSIGNMENT", null, BigDecimal.ONE, 1, 0,
|
||||
"193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
null, null, null, 1, 0)));
|
||||
|
||||
assertThrows(ResponseStatusException.class, () -> service.convertContainer("BOOKING-1"));
|
||||
|
||||
@@ -419,12 +457,14 @@ class InboundPlanServiceTest {
|
||||
|
||||
private SourceOrderSnapshot order(String customerId, Long id, String inboundCode, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, customerId, "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
BigDecimal.ONE, 10, warehouse, warehouse, warehouse, 1, 0);
|
||||
BigDecimal.ONE, 10, 0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
|
||||
private SourceOrderSnapshot order(Long id, String inboundCode, String volume, String warehouse) {
|
||||
return new SourceOrderSnapshot(id, "100", "MSCU6639870", inboundCode, "PENDING_ASSIGNMENT", null,
|
||||
new BigDecimal(volume), new BigDecimal(volume).multiply(BigDecimal.TEN).intValue(),
|
||||
0, "193000000000200003", "US-LAX-01", "洛杉矶仓", null,
|
||||
warehouse, warehouse, warehouse, 1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -35,8 +35,14 @@ class MerchantOverseasOpenApiContractTest {
|
||||
.contains("#/components/schemas/TruckDetailResponse")
|
||||
.contains("#/components/schemas/LooseboardDetailResponse")
|
||||
.contains("#/components/schemas/InboundPlanDetailResponse")
|
||||
.contains("overseasWarehouseCode: { type: string, description: 接柜仓编码,取自尾程订单预分配接收仓事实 }")
|
||||
.contains("overseasWarehouseName: { type: string, description: 接柜仓名称,取自尾程订单预分配接收仓事实 }")
|
||||
.contains("PENDING_REVIEW").contains("PENDING_ACCEPTANCE")
|
||||
.contains("version 必填").contains("幂等冲突");
|
||||
assertThat(yaml).contains("/api/platform/v1/last-leg/inbound-plans:")
|
||||
.contains("/api/platform/v1/last-leg/inbound-plans/{planNo}:")
|
||||
.contains("overseasWarehouseCode/overseasWarehouseName 只取尾程订单预分配接柜仓事实")
|
||||
.contains("客户范围只取认证 Token,不接收 customerId");
|
||||
}
|
||||
|
||||
private static String pathBlock(String yaml, String path) {
|
||||
|
||||
+8
-1
@@ -17,7 +17,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class SchemaChineseCommentContractTest {
|
||||
|
||||
private static final Set<String> MODULE_BASELINE_ONLY_TABLES = Set.of(
|
||||
"ll_us_truck_order_operation", "ll_local_looseboard_operation");
|
||||
"ll_us_truck_order_operation", "ll_local_looseboard_operation",
|
||||
"ll_receiving_warehouse_operation");
|
||||
private static final Set<String> MODULE_BASELINE_ONLY_COLUMNS = Set.of(
|
||||
"ll_order.overseas_warehouse_code", "ll_order.receiving_warehouse_version",
|
||||
"ll_receiving_container_session.overseas_warehouse_code");
|
||||
|
||||
private static final Pattern CHINESE = Pattern.compile("[\\u4e00-\\u9fff]");
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
@@ -138,6 +142,9 @@ class SchemaChineseCommentContractTest {
|
||||
continue;
|
||||
}
|
||||
for (String column : table.columns()) {
|
||||
if (MODULE_BASELINE_ONLY_COLUMNS.contains(table.name() + "." + column)) {
|
||||
continue;
|
||||
}
|
||||
boolean repaired = migration.contains("table_name='" + table.name()
|
||||
+ "' AND column_name='" + column + "'");
|
||||
boolean addedWithComment = hasCommentedAdd(migration, column)
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
class ContainerPlanProgressContractTest {
|
||||
@Test
|
||||
void responseFieldsAndServerAggregateStayFrozen() throws Exception {
|
||||
assertEquals(List.of("sourceContainerBookingId", "containerNo", "totalOrderCount", "inboundCodeCount",
|
||||
assertEquals(List.of("sourceContainerBookingId", "containerNo", "overseasWarehouseId",
|
||||
"overseasWarehouseCode", "overseasWarehouseName", "assignmentVersion",
|
||||
"canChangeReceivingWarehouse", "receivingWarehouseLockReason", "totalOrderCount", "inboundCodeCount",
|
||||
"missingInboundCodeCount", "planCount", "plannedInboundCodeCount", "pendingPlanCount",
|
||||
"plannedArrivalAt", "allPlansReady", "blockingReason"),
|
||||
Arrays.stream(ContainerPlanProgressSummary.class.getRecordComponents())
|
||||
|
||||
+51
-1
@@ -14,6 +14,8 @@ import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderFeeMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
|
||||
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -38,6 +40,7 @@ import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
|
||||
@@ -53,6 +56,8 @@ class LastlegOrderServiceTest {
|
||||
@Mock FbaWarehouseValidator fbaWarehouses;
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InboundOrderDetainedOutboxService detainedOutbox;
|
||||
@Mock ReceivingWarehouseOperationMapper receivingWarehouseOperations;
|
||||
@Mock WarehouseResourceGateway warehouseResourceGateway;
|
||||
LastlegOrderService service;
|
||||
|
||||
@BeforeEach
|
||||
@@ -71,6 +76,50 @@ class LastlegOrderServiceTest {
|
||||
"US", "CA"));
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "detainedOutbox", detainedOutbox);
|
||||
ReflectionTestUtils.setField(service, "receivingWarehouseOperations", receivingWarehouseOperations);
|
||||
ReflectionTestUtils.setField(service, "warehouseResourceGateway", warehouseResourceGateway);
|
||||
}
|
||||
|
||||
@Test
|
||||
void changesReceivingWarehouseForWholeContainerBeforePdaSession() {
|
||||
LastlegOrder first = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
||||
LastlegOrder second = order(2L, "200", "PENDING_ASSIGNMENT", null);
|
||||
for (LastlegOrder value : List.of(first, second)) {
|
||||
value.setReceivingWarehouseVersion(0); value.setOverseasWarehouseId("193000000000200003");
|
||||
value.setOverseasWarehouseCode("US-LAX-01"); value.setOverseasWarehouseName("洛杉矶仓");
|
||||
}
|
||||
var updated = new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "900",
|
||||
"US-NYC-01", "纽约仓", 1, true, null, 2, 2, 0, 0, 0, 2,
|
||||
null, false, "存在2票尚未生成有效入库计划");
|
||||
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(first, second));
|
||||
when(warehouseResourceGateway.requireActiveSharedWarehouse(900L)).thenReturn(
|
||||
new WarehouseResourceGateway.WarehouseReference(900L, "US-NYC-01", "纽约仓", "ACTIVE"));
|
||||
when(orders.updateContainerReceivingWarehouse("BOOKING-1", "900", "US-NYC-01", "纽约仓", 0))
|
||||
.thenReturn(2);
|
||||
when(orders.findContainerProgress("BOOKING-1")).thenReturn(updated);
|
||||
|
||||
var result = service.changeReceivingWarehouse("BOOKING-1", "change-1",
|
||||
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, "改派纽约"), "admin");
|
||||
|
||||
assertEquals("US-NYC-01", result.overseasWarehouseCode());
|
||||
verify(orders).updateContainerPlanReceivingWarehouse("BOOKING-1", 900L);
|
||||
verify(receivingWarehouseOperations).insert(any(com.baiyue.lastleg.entity.ReceivingWarehouseOperation.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void permanentlyLocksReceivingWarehouseAfterPdaSessionExists() {
|
||||
LastlegOrder order = order(1L, "100", "PENDING_ASSIGNMENT", null);
|
||||
order.setReceivingWarehouseVersion(0); order.setOverseasWarehouseId("193000000000200003");
|
||||
order.setOverseasWarehouseCode("US-LAX-01"); order.setOverseasWarehouseName("洛杉矶仓");
|
||||
when(orders.lockContainerOrders("BOOKING-1")).thenReturn(List.of(order));
|
||||
when(orders.countReceivingSessions("BOOKING-1")).thenReturn(1L);
|
||||
|
||||
ResponseStatusException error = assertThrows(ResponseStatusException.class, () ->
|
||||
service.changeReceivingWarehouse("BOOKING-1", "change-2",
|
||||
new LastlegOrderRequests.ChangeReceivingWarehouse(900L, 0, null), "admin"));
|
||||
|
||||
assertEquals(409, error.getStatusCode().value());
|
||||
verifyNoInteractions(warehouseResourceGateway);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -283,7 +332,8 @@ class LastlegOrderServiceTest {
|
||||
}
|
||||
|
||||
private static ContainerPlanProgressSummary summary(boolean ready, long pending, String reason) {
|
||||
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", 15, 15, 0,
|
||||
return new ContainerPlanProgressSummary("BOOKING-1", "MSCU6639870", "193000000000200003",
|
||||
"US-LAX-01", "洛杉矶仓", 0, true, null, 15, 15, 0,
|
||||
15 - pending, 15 - pending, pending, LocalDateTime.of(2026, 10, 3, 9, 0), ready, reason);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ class LoadingCompletedConsumerTest {
|
||||
private final Map<String,String> frozenContainerNos=new HashMap<>();
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock LastlegOrderNoDailySequenceMapper sequences; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(3); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
|
||||
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&o.getOverseasWarehouseId()==null)); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
|
||||
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(2,service.consume(event)); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&"193000000000200003".equals(o.getOverseasWarehouseId())&&"US-LAX-01".equals(o.getOverseasWarehouseCode())&&"洛杉矶仓".equals(o.getOverseasWarehouseName())&&Integer.valueOf(0).equals(o.getReceivingWarehouseVersion()))); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
|
||||
@Test void oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); com.fasterxml.jackson.databind.node.ArrayNode rows=json.createArrayNode(); for(int index=1;index<=15;index++){ com.fasterxml.jackson.databind.node.ObjectNode row=node.path("orders").get(0).deepCopy(); row.put("sourceInboundId","in-"+index).put("rootInboundId","root-"+index).put("sourceOrderId","order-"+index).put("sourceOrderNo","FL-"+index).put("rootOrderId","root-order-"+index).put("inboundCode","RK-"+index); rows.add(row); } node.set("orders",rows); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope fifteen=new EventEnvelope("123e4567-e89b-12d3-a456-426614174015",source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(15,service.consume(fifteen)); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(15)).insert(captured.capture()); assertEquals(1,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(captured.getAllValues().get(0).getOrderNo().matches("^FL-WH-\\d{8}-\\d{6}$")); verify(sequences,never()).nextValue(any(java.time.LocalDate.class)); }
|
||||
@Test void rejectsMissingOwnerSnapshotBeforeAnyLastlegWrite(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node.path("orders").get(0)).put("customerName"," "); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope missing=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(missing)); assertTrue(error.getMessage().contains("尾程事件负载无效")||error.getMessage().contains("货主快照")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
|
||||
@Test void freezesFormalPlannedWarehouseArrivalFromLoadingEvent(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("estimatedWarehouseArrivalAt","2026-10-03T09:00"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.sourceRegion(),source.targetRegion(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); service.consume(changed); ArgumentCaptor<LastlegOrder> captor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captor.capture()); assertTrue(captor.getAllValues().stream().allMatch(order->java.time.LocalDateTime.of(2026,10,3,9,0).equals(order.getEstimatedWarehouseAt()))); }
|
||||
|
||||
@@ -126,7 +126,12 @@ Assert-Contains $docsIndex '[测试环境独立任务协作登记](独立任务
|
||||
Assert-Contains $docsIndex '[开发环境独立任务协作登记](开发环境独立任务协作登记.md)' '文档开发登记入口'
|
||||
Assert-Contains $testTaskRegistry 'projectId `c64729ff-6710-43c6-8ef2-d84e9666cc1e`' '测试登记保存项目'
|
||||
Assert-Contains $testTaskRegistry 'C:\Users\BY-OY\Desktop\code\chongchong-global`' '测试登记工作目录'
|
||||
Assert-Contains $testTaskRegistry '当前无已登记总控或长期专项。' '测试登记清空状态'
|
||||
Assert-Contains $testTaskRegistry '已重新启用测试环境总控调度' '测试登记启用状态'
|
||||
Assert-Contains $testTaskRegistry '`01a046ea-cfc4-73a1-85f0-c69c714bff22`' '测试环境海外接柜仓总控'
|
||||
Assert-Contains $testTaskRegistry '【测试专项】海外接柜后端' '测试环境海外接柜后端专项'
|
||||
Assert-Contains $testTaskRegistry '【测试专项】海外接柜Admin' '测试环境海外接柜 Admin 专项'
|
||||
Assert-Contains $testTaskRegistry '【测试专项】海外接柜Client' '测试环境海外接柜 Client 专项'
|
||||
Assert-Contains $testTaskRegistry '【测试专项】海外接柜PDA' '测试环境海外接柜 PDA 专项'
|
||||
Assert-NotContains $testTaskRegistry '01a0194a-259c-7171-8967-074731a27618' '测试登记历史总控'
|
||||
Assert-NotContains $testTaskRegistry '01a05b10-a5fc-7d51-8203-c671f3a823ad' '测试登记开发总控'
|
||||
Assert-NotContains $testTaskRegistry '01a06c0b-0515-7c60-b329-77342b54e666' '测试登记当前开发总控'
|
||||
@@ -136,6 +141,31 @@ Assert-Contains $devTaskRegistry 'C:\Users\BY-OY\Desktop\code\chongchong-global-
|
||||
Assert-Contains $devTaskRegistry '`01a06c0b-0515-7c60-b329-77342b54e666`' '开发环境全流程总控'
|
||||
Assert-NotContains $devTaskRegistry '01a0194a-259c-7171-8967-074731a27618' '开发登记测试总控'
|
||||
Assert-NotContains $devTaskRegistry 'c64729ff-6710-43c6-8ef2-d84e9666cc1e' '开发登记测试 projectId'
|
||||
|
||||
# 业务文档已由开发批次目录收敛为稳定业务域,根快速路由不得继续引用已删除阶段目录。
|
||||
foreach ($businessDocsEntry in @(
|
||||
'docs/订单与客户/README.md',
|
||||
'docs/入库测量与计费/README.md',
|
||||
'docs/国内货柜履约/README.md',
|
||||
'docs/海外仓业务/README.md'
|
||||
)) {
|
||||
Assert-Contains $rootAgents $businessDocsEntry "根业务域入口 $businessDocsEntry"
|
||||
}
|
||||
foreach ($removedPhaseDocsEntry in @('docs/一期/', 'docs/二期/', 'docs/三期/', 'docs/四期/')) {
|
||||
Assert-NotContains $rootAgents $removedPhaseDocsEntry "根已删除阶段目录 $removedPhaseDocsEntry"
|
||||
}
|
||||
Assert-NotContains $rootAgents '不含出库和派送' '根海外仓全流程范围'
|
||||
|
||||
# 新增稳定业务能力必须从根表和目标部署单元直接命中。
|
||||
Assert-Contains $rootAgents '入库扣货、解除与凭证' '根入库扣货路由'
|
||||
Assert-Contains $adminAgents '入库扣货、解除与凭证历史' 'Admin 入库扣货路由'
|
||||
Assert-Contains $backendAgents '入库扣货、解除、履约门禁与凭证' '后端入库扣货路由'
|
||||
Assert-Contains $rootAgents '客户资金账户、充值、授信与业务扣费' '根客户资金账户路由'
|
||||
Assert-Contains $adminAgents '客户资金账户与柜级公共费用分摊' 'Admin 客户资金账户路由'
|
||||
Assert-Contains $clientAgents '客户资金账户、授信与充值申请' 'Client 客户资金账户路由'
|
||||
Assert-Contains $backendAgents '客户资金账户、充值、授信、冻结与结算' '后端客户资金账户路由'
|
||||
Assert-Contains $rootAgents '顶层授权快照' '根海外顶层授权快照'
|
||||
Assert-Contains $backendAgents '顶层授权快照' '后端海外顶层授权快照'
|
||||
$testTaskIds = @([regex]::Matches($testTaskRegistry, '(?m)^\|\s*`([0-9a-f-]{36})`\s*\|') | ForEach-Object { $_.Groups[1].Value })
|
||||
$devTaskIds = @([regex]::Matches($devTaskRegistry, '(?m)^\|\s*`([0-9a-f-]{36})`\s*\|') | ForEach-Object { $_.Groups[1].Value })
|
||||
foreach ($sharedTaskId in @($testTaskIds | Where-Object { $devTaskIds -contains $_ } | Select-Object -Unique)) {
|
||||
@@ -655,7 +685,7 @@ $financeEventConsumer = Read-RequiredText (Join-Path $backendRoot 'finance/src/m
|
||||
$financePhase1Service = Read-RequiredText (Join-Path $backendRoot 'finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java')
|
||||
Assert-Contains $rootAgents '财务管理与柜级公共费用分摊' '根柜费分摊快速路由'
|
||||
Assert-Contains $rootAgents '海外业务费用来源与客户财务' '根海外费用快速路由'
|
||||
Assert-Contains $adminAgents '财务与柜级公共费用分摊' 'Admin 柜费分摊快速路由'
|
||||
Assert-Contains $adminAgents '财务、客户资金账户与柜级公共费用分摊' 'Admin 柜费分摊快速路由'
|
||||
Assert-Contains $backendAgents '柜级公共费用跨客户分摊' '后端柜费分摊快速路由'
|
||||
Assert-Contains $backendAgents '海外业务费用来源跨区入池' '后端海外费用快速路由'
|
||||
Assert-Contains $firstlegAgents '单行入口拒绝分摊组' 'Firstleg 分摊组原子边界'
|
||||
|
||||
Reference in New Issue
Block a user