Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b1b4ab9b2 | ||
|
|
9d54c57b7c | ||
|
|
e72ac14425 | ||
|
|
ec8f84c1c0 | ||
|
|
2b7f66cb04 | ||
|
|
c7061b8a74 | ||
|
|
5250f304be | ||
|
|
f5f72d681d | ||
|
|
c226b6a99b | ||
|
|
600b2a2dc4 | ||
|
|
76c24867a4 | ||
|
|
61a97cd057 | ||
|
|
ecf48886d5 | ||
|
|
1db3e48506 | ||
|
|
0f2e201c38 | ||
|
|
adabec13d3 | ||
|
|
0fb51211aa | ||
|
|
d7bff980ca | ||
|
|
ed3c1dafcc | ||
|
|
0a3ec0b8a6 | ||
|
|
3b38fdd33d | ||
|
|
daf0ee2acd | ||
|
|
086a0f9c89 | ||
|
|
c80307a838 | ||
|
|
575cee7a86 | ||
|
|
c1b136c3d6 | ||
|
|
4612181ab0 | ||
|
|
b4c46280de | ||
|
|
15b563610c | ||
|
|
079ff43f73 | ||
|
|
554b6c93b7 | ||
|
|
817e356c21 | ||
|
|
ca12f58d38 | ||
|
|
8fb63e9adc | ||
|
|
d736fdba1b |
Binary file not shown.
@@ -1,39 +0,0 @@
|
|||||||
import java.sql.Connection;
|
|
||||||
import java.sql.DriverManager;
|
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.ResultSetMetaData;
|
|
||||||
import java.sql.Statement;
|
|
||||||
|
|
||||||
public final class DbQuery {
|
|
||||||
public static void main(String[] args) throws Exception {
|
|
||||||
if (args.length < 2) {
|
|
||||||
throw new IllegalArgumentException("usage: DbQuery <database> <sql>");
|
|
||||||
}
|
|
||||||
String url = "jdbc:mysql://127.0.0.1:3306/" + args[0]
|
|
||||||
+ "?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"
|
|
||||||
+ "&useSSL=false&allowPublicKeyRetrieval=true";
|
|
||||||
try (Connection connection = DriverManager.getConnection(url, "root", "123456");
|
|
||||||
Statement statement = connection.createStatement()) {
|
|
||||||
boolean hasResult = statement.execute(args[1]);
|
|
||||||
if (!hasResult) {
|
|
||||||
System.out.println("updateCount=" + statement.getUpdateCount());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try (ResultSet result = statement.getResultSet()) {
|
|
||||||
ResultSetMetaData meta = result.getMetaData();
|
|
||||||
for (int column = 1; column <= meta.getColumnCount(); column++) {
|
|
||||||
if (column > 1) System.out.print("\t");
|
|
||||||
System.out.print(meta.getColumnLabel(column));
|
|
||||||
}
|
|
||||||
System.out.println();
|
|
||||||
while (result.next()) {
|
|
||||||
for (int column = 1; column <= meta.getColumnCount(); column++) {
|
|
||||||
if (column > 1) System.out.print("\t");
|
|
||||||
System.out.print(String.valueOf(result.getObject(column)));
|
|
||||||
}
|
|
||||||
System.out.println();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
@@ -40,6 +40,7 @@
|
|||||||
| 海外 API 路由、海外 Token、Audience/JWKS | `gateway-overseas/` | `gateway-overseas/AGENTS.md` |
|
| 海外 API 路由、海外 Token、Audience/JWKS | `gateway-overseas/` | `gateway-overseas/AGENTS.md` |
|
||||||
| 海外 IAM 状态与权限版本投影 | `lastleg-service/security/` | `lastleg-service/AGENTS.md`;内部入口 `/internal/lastleg/security/v1/events`、`/validate`;消费三类 IAM 安全事件并为海外 Gateway 提供 fail-closed 校验,投影表归 `baiyue_lastleg` |
|
| 海外 IAM 状态与权限版本投影 | `lastleg-service/security/` | `lastleg-service/AGENTS.md`;内部入口 `/internal/lastleg/security/v1/events`、`/validate`;消费三类 IAM 安全事件并为海外 Gateway 提供 fail-closed 校验,投影表归 `baiyue_lastleg` |
|
||||||
| 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` | `lastleg-service/AGENTS.md`,再按 `inboundplan/`、`relabel/`、`inventory/`、`procurement/`、`trucking/` 定位 |
|
| 尾程订单、海外入仓计划、换标、库存、采购、本土卡车 | `lastleg-service/` | `lastleg-service/AGENTS.md`,再按 `inboundplan/`、`relabel/`、`inventory/`、`procurement/`、`trucking/` 定位 |
|
||||||
|
| 待分配柜级摘要与整柜入仓计划转换 | `lastleg-service/` | `orders/container-summaries` 按 `sourceContainerBookingId` 跨客户聚合分页;`inbound-plans/convert-container` 锁定同柜订单,每票生成独立计划,整批原子且幂等;详见 `lastleg-service/AGENTS.md` |
|
||||||
| 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` | 平台入口 `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`;PDA 公网入口 `inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,旧 planNo/allocationId Controller 已移除 |
|
| 海外入仓分页、FBA 预约、PDA 柜号签收与入仓 | `lastleg-service/` | 平台入口 `inboundplan/controller/InboundPlanController.java`、`InboundPlanQueryMapper.java`、`InboundFbaAppointmentService.java`;PDA 公网入口 `inboundplan/pda/controller/ReceivingContainerController.java`、`ReceivingContainerService.java`,旧 planNo/allocationId Controller 已移除 |
|
||||||
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 |
|
| 海外仓排车查询、候选与智能预排 | `lastleg-service/` | `dispatch/controller/DispatchQueryController.java`、`SmartDispatchController.java`;草稿柜、托盘锁和预排归 `dispatch/`,只读排车和预排创建独立开关,不自动开放编辑、取消、发车、PDA 出库或派送 |
|
||||||
| 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` |
|
| 客户海外仓贴标、转运、废弃与退货换标 | `lastleg-service/` | `relabel/`、`transfer/`、`discard/` 的 Merchant Controller/Service;Client 消费端 `../apps/client-web/src/modules/overseas-warehouse/`,统一经 `gateway-overseas/` |
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
| 头程装柜完成 V2(当前生产) | `FirstLegLoadingPlanCompletedV2.java`;订单级冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;同时冻结来源/根订单与入仓标识、客户、业务类型、目的仓/备注/托盘及一键代发托盘 SKU 快照;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java` |
|
| 头程装柜完成 V2(当前生产) | `FirstLegLoadingPlanCompletedV2.java`;订单级冻结 `destinationType=FBA|OVERSEAS`,FBA 可按箱数接收,OVERSEAS 逐明细冻结与实装箱数一致的 SKU/箱号;同时冻结来源/根订单与入仓标识、客户、业务类型、目的仓/备注/托盘及一键代发托盘 SKU 快照;国内生产者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingCompletedOutboxService.java`,海外消费者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java` |
|
||||||
| 头程装柜完成 V1 解析模型 | `FirstLegLoadingPlanCompletedV1.java`;用于消费端冻结字段映射,新增生产不得回退为 V1 |
|
| 头程装柜完成 V1 解析模型 | `FirstLegLoadingPlanCompletedV1.java`;用于消费端冻结字段映射,新增生产不得回退为 V1 |
|
||||||
| 海外卸柜完成 V1 | `OverseasContainerUnloadedV1.java`;按来源柜预订 ID 做柜级聚合/幂等,同一柜只发布一次;`unloadingCompletedAt` 必须是全柜 complete 时刻,不得用签收拍照时刻代替;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`,国内消费者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java` |
|
| 海外卸柜完成 V1 | `OverseasContainerUnloadedV1.java`;按来源柜预订 ID 做柜级聚合/幂等,同一柜只发布一次;`unloadingCompletedAt` 必须是全柜 complete 时刻,不得用签收拍照时刻代替;海外生产者见 `../lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java`,国内消费者见 `../firstleg/src/main/java/com/baiyue/firstleg/service/OverseasContainerEventConsumer.java` |
|
||||||
| 海外入仓单暂扣变更 V1 | `OverseasInboundOrderDetainedChangedV1.java`;聚合键固定为来源头程入仓单 ID,暂扣与解除暂扣均由尾程 Outbox 回传,国内按入仓单绑定、客户归属及聚合版本单调消费 |
|
|
||||||
| 国内船节点记录 V1 | `FirstLegContainerShippingNodeRecordedV1.java`;只接受十节点中带实际时间和完整供应商审计字段的事实,国内生产者 `../firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxService.java`,海外消费者 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
| 国内船节点记录 V1 | `FirstLegContainerShippingNodeRecordedV1.java`;只接受十节点中带实际时间和完整供应商审计字段的事实,国内生产者 `../firstleg/src/main/java/com/baiyue/firstleg/portschedule/service/CarrierScheduleNodeOutboxService.java`,海外消费者 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||||
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
|
| IAM 安全事件 V1 | `IamSecurityEventsV1.java`;账号、授权快照等安全事件的冻结模型 |
|
||||||
| 契约说明 | `package-info.java` |
|
| 契约说明 | `package-info.java` |
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ public final class EventTypes {
|
|||||||
public static final String FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED =
|
public static final String FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED =
|
||||||
"FirstLegContainerShippingNodeRecorded";
|
"FirstLegContainerShippingNodeRecorded";
|
||||||
public static final String OVERSEAS_CONTAINER_UNLOADED = "OverseasContainerUnloaded";
|
public static final String OVERSEAS_CONTAINER_UNLOADED = "OverseasContainerUnloaded";
|
||||||
public static final String OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED = "OverseasInboundOrderDetainedChanged";
|
|
||||||
public static final String IAM_USER_STATUS_CHANGED = "IamUserStatusChanged";
|
public static final String IAM_USER_STATUS_CHANGED = "IamUserStatusChanged";
|
||||||
public static final String IAM_TENANT_STATUS_CHANGED = "IamTenantStatusChanged";
|
public static final String IAM_TENANT_STATUS_CHANGED = "IamTenantStatusChanged";
|
||||||
public static final String IAM_PERMISSION_VERSION_CHANGED = "IamPermissionVersionChanged";
|
public static final String IAM_PERMISSION_VERSION_CHANGED = "IamPermissionVersionChanged";
|
||||||
|
|||||||
+23
-5
@@ -7,7 +7,14 @@ import java.util.List;
|
|||||||
public record FirstLegLoadingPlanCompletedV1(
|
public record FirstLegLoadingPlanCompletedV1(
|
||||||
String loadingPlanId, String planNo, String containerBookingId,
|
String loadingPlanId, String planNo, String containerBookingId,
|
||||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||||
|
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||||
List<Order> orders) {
|
List<Order> orders) {
|
||||||
|
public FirstLegLoadingPlanCompletedV1(String loadingPlanId, String planNo, String containerBookingId,
|
||||||
|
String containerNo, String cabinetNo, String warehouseId,
|
||||||
|
String warehouseName, List<Order> orders) {
|
||||||
|
this(loadingPlanId, planNo, containerBookingId, containerNo, cabinetNo, warehouseId,
|
||||||
|
warehouseName, null, null, null, orders);
|
||||||
|
}
|
||||||
|
|
||||||
public record Order(
|
public record Order(
|
||||||
String sourceInboundId, String rootInboundId,
|
String sourceInboundId, String rootInboundId,
|
||||||
@@ -16,10 +23,17 @@ public record FirstLegLoadingPlanCompletedV1(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items, String customerId, String orderType, List<PalletSnapshot> pallets,
|
BigDecimal volumeWeightKg,
|
||||||
|
List<Item> items, String customerId, String customerName, String orderType, List<PalletSnapshot> pallets,
|
||||||
String destinationType) {
|
String destinationType) {
|
||||||
public Order {
|
public Order {
|
||||||
items = items == null ? List.of() : items;
|
items = items == null ? List.of() : items;
|
||||||
|
if (customerId == null || customerId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("customerId is required");
|
||||||
|
}
|
||||||
|
if (customerName == null || customerName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("customerName is required");
|
||||||
|
}
|
||||||
orderType = orderType == null || orderType.isBlank() ? "FIRST_LEG" : orderType;
|
orderType = orderType == null || orderType.isBlank() ? "FIRST_LEG" : orderType;
|
||||||
pallets = pallets == null ? List.of() : pallets;
|
pallets = pallets == null ? List.of() : pallets;
|
||||||
}
|
}
|
||||||
@@ -29,11 +43,13 @@ public record FirstLegLoadingPlanCompletedV1(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items, String customerId, String orderType, List<PalletSnapshot> pallets) {
|
List<Item> items, String customerId, String customerName,
|
||||||
|
String orderType, List<PalletSnapshot> pallets) {
|
||||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||||
grossWeightKg, netWeightKg, volumeCbm, items, customerId, orderType, pallets, null);
|
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||||
|
orderType, pallets, null);
|
||||||
}
|
}
|
||||||
public Order(String sourceInboundId, String rootInboundId,
|
public Order(String sourceInboundId, String rootInboundId,
|
||||||
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
||||||
@@ -41,11 +57,13 @@ public record FirstLegLoadingPlanCompletedV1(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items) {
|
List<Item> items, String customerId, String customerName,
|
||||||
|
String orderType, List<PalletSnapshot> pallets, String destinationType) {
|
||||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||||
grossWeightKg, netWeightKg, volumeCbm, items, null, "FIRST_LEG", List.of(), null);
|
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||||
|
orderType, pallets, destinationType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-7
@@ -3,11 +3,19 @@ package com.baiyue.common.event;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 装柜完成交接快照,包含三类逐货物备注、推荐托数和可复用的头程托盘编号快照。 */
|
/** 装柜完成交接快照,包含三类逐货物备注及一键代发托盘快照。 */
|
||||||
public record FirstLegLoadingPlanCompletedV2(
|
public record FirstLegLoadingPlanCompletedV2(
|
||||||
String loadingPlanId, String planNo, String containerBookingId,
|
String loadingPlanId, String planNo, String containerBookingId,
|
||||||
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
String containerNo, String cabinetNo, String warehouseId, String warehouseName,
|
||||||
|
String estimatedPortArrivalAt, String actualPortArrivalAt, String estimatedWarehouseArrivalAt,
|
||||||
List<Order> orders) {
|
List<Order> orders) {
|
||||||
|
public FirstLegLoadingPlanCompletedV2(String loadingPlanId, String planNo, String containerBookingId,
|
||||||
|
String containerNo, String cabinetNo, String warehouseId,
|
||||||
|
String warehouseName, List<Order> orders) {
|
||||||
|
this(loadingPlanId, planNo, containerBookingId, containerNo, cabinetNo, warehouseId,
|
||||||
|
warehouseName, null, null, null, orders);
|
||||||
|
}
|
||||||
|
|
||||||
public record Order(
|
public record Order(
|
||||||
String sourceInboundId, String rootInboundId,
|
String sourceInboundId, String rootInboundId,
|
||||||
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
String sourceOrderId, String sourceOrderNo, String rootOrderId,
|
||||||
@@ -15,11 +23,18 @@ public record FirstLegLoadingPlanCompletedV2(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items, String customerId, String orderType, List<PalletSnapshot> pallets,
|
BigDecimal volumeWeightKg,
|
||||||
|
List<Item> items, String customerId, String customerName, String orderType, List<PalletSnapshot> pallets,
|
||||||
/** 目的类型:FBA 可按箱数收货,OVERSEAS 必须逐箱扫码。 */
|
/** 目的类型:FBA 可按箱数收货,OVERSEAS 必须逐箱扫码。 */
|
||||||
String destinationType) {
|
String destinationType) {
|
||||||
public Order {
|
public Order {
|
||||||
items = items == null ? List.of() : items;
|
items = items == null ? List.of() : items;
|
||||||
|
if (customerId == null || customerId.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("customerId is required");
|
||||||
|
}
|
||||||
|
if (customerName == null || customerName.isBlank()) {
|
||||||
|
throw new IllegalArgumentException("customerName is required");
|
||||||
|
}
|
||||||
orderType = orderType == null || orderType.isBlank() ? "FIRST_LEG" : orderType;
|
orderType = orderType == null || orderType.isBlank() ? "FIRST_LEG" : orderType;
|
||||||
pallets = pallets == null ? List.of() : pallets;
|
pallets = pallets == null ? List.of() : pallets;
|
||||||
}
|
}
|
||||||
@@ -30,11 +45,13 @@ public record FirstLegLoadingPlanCompletedV2(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items, String customerId, String orderType, List<PalletSnapshot> pallets) {
|
List<Item> items, String customerId, String customerName,
|
||||||
|
String orderType, List<PalletSnapshot> pallets) {
|
||||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||||
grossWeightKg, netWeightKg, volumeCbm, items, customerId, orderType, pallets, null);
|
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||||
|
orderType, pallets, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Order(String sourceInboundId, String rootInboundId,
|
public Order(String sourceInboundId, String rootInboundId,
|
||||||
@@ -43,11 +60,13 @@ public record FirstLegLoadingPlanCompletedV2(
|
|||||||
String destinationWarehouseCode, String destinationWarehouseName,
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
int packageCount, Integer productQuantity, Integer palletCount,
|
int packageCount, Integer productQuantity, Integer palletCount,
|
||||||
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
BigDecimal grossWeightKg, BigDecimal netWeightKg, BigDecimal volumeCbm,
|
||||||
List<Item> items) {
|
List<Item> items, String customerId, String customerName,
|
||||||
|
String orderType, List<PalletSnapshot> pallets, String destinationType) {
|
||||||
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
this(sourceInboundId, rootInboundId, sourceOrderId, sourceOrderNo, rootOrderId,
|
||||||
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
inboundCode, destinationWarehouseId, destinationWarehouseCode,
|
||||||
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
destinationWarehouseName, packageCount, productQuantity, palletCount,
|
||||||
grossWeightKg, netWeightKg, volumeCbm, items, null, "FIRST_LEG", List.of(), null);
|
grossWeightKg, netWeightKg, volumeCbm, null, items, customerId, customerName,
|
||||||
|
orderType, pallets, destinationType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +105,7 @@ public record FirstLegLoadingPlanCompletedV2(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 头程托盘冻结快照;普通入仓用于复用打印编号,一键代发还冻结 SKU/箱唛库存。 */
|
/** 一键代发入仓单的托盘冻结快照。 */
|
||||||
public record PalletSnapshot(String palletNo, List<SkuPackageSnapshot> skuPackages) {}
|
public record PalletSnapshot(String palletNo, List<SkuPackageSnapshot> skuPackages) {}
|
||||||
|
|
||||||
/** 托盘下 SKU、箱数、数量及箱号冻结快照;cartonMarks 中每个箱号都对应 skuCode。 */
|
/** 托盘下 SKU、箱数、数量及箱号冻结快照;cartonMarks 中每个箱号都对应 skuCode。 */
|
||||||
|
|||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
package com.baiyue.common.event;
|
|
||||||
|
|
||||||
import java.time.Instant;
|
|
||||||
|
|
||||||
/** 海外尾程按来源入仓单回传的暂扣状态事实。 */
|
|
||||||
public record OverseasInboundOrderDetainedChangedV1(
|
|
||||||
String sourceInboundId,
|
|
||||||
String inboundCode,
|
|
||||||
String lastlegOrderId,
|
|
||||||
String lastlegOrderNo,
|
|
||||||
boolean detained,
|
|
||||||
String reason,
|
|
||||||
String operatorId,
|
|
||||||
String operatorName,
|
|
||||||
Instant changedAt
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Baiyue Cross-region Event Contracts
|
||||||
|
version: 1.0.0
|
||||||
|
paths: {}
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
FirstLegLoadingPlanCompletedV2:
|
||||||
|
type: object
|
||||||
|
required: [loadingPlanId, containerBookingId, estimatedPortArrivalAt, actualPortArrivalAt, estimatedWarehouseArrivalAt, orders]
|
||||||
|
properties:
|
||||||
|
loadingPlanId: {type: string}
|
||||||
|
containerBookingId: {type: string}
|
||||||
|
containerNo: {type: string}
|
||||||
|
cabinetNo: {type: string}
|
||||||
|
warehouseId: {type: string}
|
||||||
|
warehouseName: {type: string}
|
||||||
|
estimatedPortArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local estimated port arrival; null when Firstleg has no fact}
|
||||||
|
actualPortArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local actual port arrival; never copied from an estimate}
|
||||||
|
estimatedWarehouseArrivalAt: {type: [string, 'null'], format: local-date-time, description: Frozen source-local estimated warehouse arrival; null when Firstleg has no fact}
|
||||||
|
orders:
|
||||||
|
type: array
|
||||||
|
items: {$ref: '#/components/schemas/FirstLegLoadingPlanCompletedOrderV2'}
|
||||||
|
FirstLegLoadingPlanCompletedOrderV2:
|
||||||
|
type: object
|
||||||
|
required: [sourceInboundId, customerId, customerName, palletCount, packageCount, productQuantity, volumeCbm, volumeWeightKg, items, orderType, pallets, destinationType]
|
||||||
|
properties:
|
||||||
|
sourceInboundId: {type: string}
|
||||||
|
sourceOrderId: {type: string}
|
||||||
|
sourceOrderNo: {type: string}
|
||||||
|
inboundCode: {type: string}
|
||||||
|
customerId: {type: string, minLength: 1, description: Trusted customer identity frozen by Firstleg}
|
||||||
|
customerName: {type: string, minLength: 1, maxLength: 128, description: Immutable trusted container/customer name snapshot; consumers must not query domestic IAM}
|
||||||
|
packageCount: {type: integer, minimum: 1}
|
||||||
|
productQuantity: {type: [integer, 'null'], minimum: 0, description: Final loaded product quantity; null when unavailable}
|
||||||
|
palletCount: {type: [integer, 'null'], minimum: 1, description: Distinct physical pallet count from Firstleg allocation pallet identity; never a planned zero placeholder}
|
||||||
|
volumeCbm: {type: [number, 'null'], minimum: 0, description: Final loaded volume in cubic metres}
|
||||||
|
volumeWeightKg: {type: [number, 'null'], minimum: 0, description: Sum of final loaded item dimensional weight facts; null when unavailable}
|
||||||
|
items: {type: array, items: {type: object}}
|
||||||
|
orderType: {type: string, enum: [FIRST_LEG, ONE_CLICK_DROPSHIP]}
|
||||||
|
pallets: {type: array, items: {type: object}}
|
||||||
|
destinationType: {type: string, enum: [FBA, OVERSEAS]}
|
||||||
+12
-4
@@ -19,7 +19,7 @@ class FirstLegLoadingPlanCompletedV1Test {
|
|||||||
var order = new FirstLegLoadingPlanCompletedV1.Order("2088000000000000003", "2088000000000000004",
|
var order = new FirstLegLoadingPlanCompletedV1.Order("2088000000000000003", "2088000000000000004",
|
||||||
"2088000000000000005", "FL-1", "2088000000000000006", "RK-1", "2088000000000000002",
|
"2088000000000000005", "FL-1", "2088000000000000006", "RK-1", "2088000000000000002",
|
||||||
"ONT8", "ONT8", 2, 20, 1, new BigDecimal("10.500"), new BigDecimal("9.500"),
|
"ONT8", "ONT8", 2, 20, 1, new BigDecimal("10.500"), new BigDecimal("9.500"),
|
||||||
new BigDecimal("0.200"), List.of(item));
|
new BigDecimal("0.200"), List.of(item), "9001", "客户甲", "FIRST_LEG", List.of());
|
||||||
var payload = new FirstLegLoadingPlanCompletedV1("2088000000000000007", "P-1", "2088000000000000008",
|
var payload = new FirstLegLoadingPlanCompletedV1("2088000000000000007", "P-1", "2088000000000000008",
|
||||||
"CONT-1", "CAB-1", "2088000000000000009", "中转仓", List.of(order));
|
"CONT-1", "CAB-1", "2088000000000000009", "中转仓", List.of(order));
|
||||||
String json = mapper.writeValueAsString(payload);
|
String json = mapper.writeValueAsString(payload);
|
||||||
@@ -35,7 +35,7 @@ class FirstLegLoadingPlanCompletedV1Test {
|
|||||||
"SKU-2", "产品2", 1, 5, List.of("BOX-003"));
|
"SKU-2", "产品2", 1, 5, List.of("BOX-003"));
|
||||||
var order = new FirstLegLoadingPlanCompletedV1.Order("100", "100", "200", "FL-200", "200",
|
var order = new FirstLegLoadingPlanCompletedV1.Order("100", "100", "200", "FL-200", "200",
|
||||||
"RK-100", "300", "ONT8", "ONT8", 3, 25, 2, BigDecimal.ONE,
|
"RK-100", "300", "ONT8", "ONT8", 3, 25, 2, BigDecimal.ONE,
|
||||||
BigDecimal.ONE, BigDecimal.ONE, List.of(), "9001", "ONE_CLICK_DROPSHIP",
|
BigDecimal.ONE, BigDecimal.ONE, List.of(), "9001", "客户甲", "ONE_CLICK_DROPSHIP",
|
||||||
List.of(new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-01", List.of(sku1)),
|
List.of(new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-01", List.of(sku1)),
|
||||||
new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-02", List.of(sku2))));
|
new FirstLegLoadingPlanCompletedV1.PalletSnapshot("PLT-02", List.of(sku2))));
|
||||||
var payload = new FirstLegLoadingPlanCompletedV1("1", "LP-1", "2", "CONT-1", null,
|
var payload = new FirstLegLoadingPlanCompletedV1("1", "LP-1", "2", "CONT-1", null,
|
||||||
@@ -52,12 +52,20 @@ class FirstLegLoadingPlanCompletedV1Test {
|
|||||||
void legacyParserRetainsDestinationTypeWhenV2OrderCarriesIt() throws Exception {
|
void legacyParserRetainsDestinationTypeWhenV2OrderCarriesIt() throws Exception {
|
||||||
String json = """
|
String json = """
|
||||||
{"loadingPlanId":"1","planNo":"LP-1","containerBookingId":"2","containerNo":"CONT-1",
|
{"loadingPlanId":"1","planNo":"LP-1","containerBookingId":"2","containerNo":"CONT-1",
|
||||||
"warehouseId":"3","warehouseName":"海外仓","orders":[{"sourceInboundId":"100",
|
"warehouseId":"3","warehouseName":"海外仓","estimatedPortArrivalAt":"2026-09-30T12:00",
|
||||||
"destinationWarehouseId":"3","packageCount":1,"items":[],"destinationType":"OVERSEAS"}]}
|
"actualPortArrivalAt":null,"estimatedWarehouseArrivalAt":"2026-10-03T09:00",
|
||||||
|
"orders":[{"sourceInboundId":"100","destinationWarehouseId":"3","packageCount":1,
|
||||||
|
"palletCount":2,"volumeWeightKg":12.500,"items":[],"customerId":"9001",
|
||||||
|
"customerName":"客户甲","destinationType":"OVERSEAS"}]}
|
||||||
""";
|
""";
|
||||||
|
|
||||||
FirstLegLoadingPlanCompletedV1 restored = mapper.readValue(json, FirstLegLoadingPlanCompletedV1.class);
|
FirstLegLoadingPlanCompletedV1 restored = mapper.readValue(json, FirstLegLoadingPlanCompletedV1.class);
|
||||||
|
|
||||||
assertThat(restored.orders().get(0).destinationType()).isEqualTo("OVERSEAS");
|
assertThat(restored.orders().get(0).destinationType()).isEqualTo("OVERSEAS");
|
||||||
|
assertThat(restored.estimatedPortArrivalAt()).isEqualTo("2026-09-30T12:00");
|
||||||
|
assertThat(restored.actualPortArrivalAt()).isNull();
|
||||||
|
assertThat(restored.estimatedWarehouseArrivalAt()).isEqualTo("2026-10-03T09:00");
|
||||||
|
assertThat(restored.orders().get(0).palletCount()).isEqualTo(2);
|
||||||
|
assertThat(restored.orders().get(0).volumeWeightKg()).isEqualByComparingTo("12.500");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-4
@@ -4,17 +4,43 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
class FirstLegLoadingPlanCompletedV2Test {
|
class FirstLegLoadingPlanCompletedV2Test {
|
||||||
|
@Test
|
||||||
|
void carriesOnlyFrozenContainerTimesAndActualOrderAggregates() {
|
||||||
|
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
||||||
|
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "ONT8",
|
||||||
|
12, 120, 3, null, null, new BigDecimal("2.500"), new BigDecimal("18.750"),
|
||||||
|
List.of(), "11", "客户甲", "FIRST_LEG", List.of(), "OVERSEAS");
|
||||||
|
var payload = new FirstLegLoadingPlanCompletedV2(
|
||||||
|
"4", "P", "5", "CONT", "CAB", "6", "WH",
|
||||||
|
"2026-09-30T12:00", null, "2026-10-03T09:00", List.of(order));
|
||||||
|
|
||||||
|
var json = new ObjectMapper().valueToTree(payload);
|
||||||
|
|
||||||
|
assertThat(json.at("/estimatedPortArrivalAt").asText()).isEqualTo("2026-09-30T12:00");
|
||||||
|
assertThat(json.at("/actualPortArrivalAt").isNull()).isTrue();
|
||||||
|
assertThat(json.at("/estimatedWarehouseArrivalAt").asText()).isEqualTo("2026-10-03T09:00");
|
||||||
|
assertThat(json.at("/orders/0/palletCount").asInt()).isEqualTo(3);
|
||||||
|
assertThat(json.at("/orders/0/packageCount").asInt()).isEqualTo(12);
|
||||||
|
assertThat(json.at("/orders/0/productQuantity").asInt()).isEqualTo(120);
|
||||||
|
assertThat(json.at("/orders/0/volumeCbm").decimalValue()).isEqualByComparingTo("2.500");
|
||||||
|
assertThat(json.at("/orders/0/volumeWeightKg").decimalValue()).isEqualByComparingTo("18.750");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void carriesTheThreeItemRemarks() throws Exception {
|
void carriesTheThreeItemRemarks() throws Exception {
|
||||||
var item = new FirstLegLoadingPlanCompletedV2.Item("1", "SKU", "中文", "EN", 1,
|
var item = new FirstLegLoadingPlanCompletedV2.Item("1", "SKU", "中文", "EN", 1,
|
||||||
2, null, null, null, null, null, null, null, null, null, null, null,
|
2, null, null, null, null, null, null, null, null, null, null, null,
|
||||||
"客户备注", "内部备注", "仓库备注");
|
"客户备注", "内部备注", "仓库备注");
|
||||||
var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK",
|
var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK",
|
||||||
null, null, null, 1, 2, null, null, null, null, List.of(item));
|
null, null, null, 1, 2, null, null, null, null, List.of(item),
|
||||||
|
"11", "客户甲", "FIRST_LEG", List.of());
|
||||||
var payload = new FirstLegLoadingPlanCompletedV2("4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order));
|
var payload = new FirstLegLoadingPlanCompletedV2("4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order));
|
||||||
String json = new ObjectMapper().writeValueAsString(payload);
|
String json = new ObjectMapper().writeValueAsString(payload);
|
||||||
assertThat(json).contains("customerRemark", "internalRemark", "warehouseRemark");
|
assertThat(json).contains("customerRemark", "internalRemark", "warehouseRemark");
|
||||||
@@ -29,13 +55,14 @@ class FirstLegLoadingPlanCompletedV2Test {
|
|||||||
var pallet = new FirstLegLoadingPlanCompletedV2.PalletSnapshot("PLT-01", List.of(sku));
|
var pallet = new FirstLegLoadingPlanCompletedV2.PalletSnapshot("PLT-01", List.of(sku));
|
||||||
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
||||||
"2", "2", "3", "ORD", "3", "RK", null, null, null,
|
"2", "2", "3", "ORD", "3", "RK", null, null, null,
|
||||||
2, 20, 1, null, null, null, List.of(), "11", "ONE_CLICK_DROPSHIP", List.of(pallet));
|
2, 20, 1, null, null, null, List.of(), "11", "客户甲", "ONE_CLICK_DROPSHIP", List.of(pallet));
|
||||||
var payload = new FirstLegLoadingPlanCompletedV2(
|
var payload = new FirstLegLoadingPlanCompletedV2(
|
||||||
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order));
|
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order));
|
||||||
|
|
||||||
var json = new ObjectMapper().valueToTree(payload);
|
var json = new ObjectMapper().valueToTree(payload);
|
||||||
|
|
||||||
assertThat(json.at("/orders/0/customerId").asText()).isEqualTo("11");
|
assertThat(json.at("/orders/0/customerId").asText()).isEqualTo("11");
|
||||||
|
assertThat(json.at("/orders/0/customerName").asText()).isEqualTo("客户甲");
|
||||||
assertThat(json.at("/orders/0/orderType").asText()).isEqualTo("ONE_CLICK_DROPSHIP");
|
assertThat(json.at("/orders/0/orderType").asText()).isEqualTo("ONE_CLICK_DROPSHIP");
|
||||||
assertThat(json.at("/orders/0/pallets/0/skuPackages/0/cartonMarks")).hasSize(2);
|
assertThat(json.at("/orders/0/pallets/0/skuPackages/0/cartonMarks")).hasSize(2);
|
||||||
}
|
}
|
||||||
@@ -44,7 +71,7 @@ class FirstLegLoadingPlanCompletedV2Test {
|
|||||||
void carriesFrozenDestinationType() {
|
void carriesFrozenDestinationType() {
|
||||||
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
var order = new FirstLegLoadingPlanCompletedV2.Order(
|
||||||
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "FBA ONT8",
|
"2", "2", "3", "ORD", "3", "RK", null, "ONT8", "FBA ONT8",
|
||||||
2, 20, 0, null, null, null, List.of(), "11", "FIRST_LEG", List.of(), "FBA");
|
2, 20, 0, null, null, null, List.of(), "11", "客户甲", "FIRST_LEG", List.of(), "FBA");
|
||||||
|
|
||||||
var json = new ObjectMapper().valueToTree(new FirstLegLoadingPlanCompletedV2(
|
var json = new ObjectMapper().valueToTree(new FirstLegLoadingPlanCompletedV2(
|
||||||
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)));
|
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)));
|
||||||
@@ -58,7 +85,8 @@ class FirstLegLoadingPlanCompletedV2Test {
|
|||||||
20, null, null, null, null, null, null, null, null, null, null, null,
|
20, null, null, null, null, null, null, null, null, null, null, null,
|
||||||
null, null, null, null, List.of("HDHK260711126-01", "HDHK260711126-02"));
|
null, null, null, null, List.of("HDHK260711126-01", "HDHK260711126-02"));
|
||||||
var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK",
|
var order = new FirstLegLoadingPlanCompletedV2.Order("2", "2", "3", "ORD", "3", "RK",
|
||||||
null, null, null, 2, 20, null, null, null, null, List.of(item));
|
null, null, null, 2, 20, null, null, null, null, List.of(item),
|
||||||
|
"11", "客户甲", "FIRST_LEG", List.of());
|
||||||
|
|
||||||
var json = new ObjectMapper().valueToTree(new FirstLegLoadingPlanCompletedV2(
|
var json = new ObjectMapper().valueToTree(new FirstLegLoadingPlanCompletedV2(
|
||||||
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)));
|
"4", "P", "5", "CONT", "CAB", "6", "WH", List.of(order)));
|
||||||
@@ -66,4 +94,29 @@ class FirstLegLoadingPlanCompletedV2Test {
|
|||||||
assertThat(json.at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU-PSC2");
|
assertThat(json.at("/orders/0/items/0/skuCode").asText()).isEqualTo("SKU-PSC2");
|
||||||
assertThat(json.at("/orders/0/items/0/cartonMarks")).hasSize(2);
|
assertThat(json.at("/orders/0/items/0/cartonMarks")).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsMissingImmutableCustomerName() {
|
||||||
|
assertThatThrownBy(() -> new FirstLegLoadingPlanCompletedV2.Order(
|
||||||
|
"2", "2", "3", "ORD", "3", "RK", null, null, null,
|
||||||
|
1, 1, 0, null, null, null, List.of(), "11", " ", "FIRST_LEG", List.of()))
|
||||||
|
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("customerName");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void openApiRequiresImmutableCustomerNameNextToCustomerId() throws Exception {
|
||||||
|
String yaml;
|
||||||
|
try (var input = FirstLegLoadingPlanCompletedV2Test.class.getResourceAsStream(
|
||||||
|
"/openapi/cross-region-events-v1.yaml")) {
|
||||||
|
assertThat(input).isNotNull();
|
||||||
|
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
assertThat(yaml)
|
||||||
|
.contains("required: [sourceInboundId, customerId, customerName")
|
||||||
|
.contains("customerId: {type: string, minLength: 1")
|
||||||
|
.contains("customerName: {type: string, minLength: 1, maxLength: 128")
|
||||||
|
.contains("consumers must not query domestic IAM")
|
||||||
|
.contains("estimatedPortArrivalAt", "actualPortArrivalAt", "estimatedWarehouseArrivalAt")
|
||||||
|
.contains("palletCount", "volumeWeightKg", "never a planned zero placeholder");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework</groupId>
|
||||||
|
<artifactId>spring-tx</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.security</groupId>
|
<groupId>org.springframework.security</groupId>
|
||||||
<artifactId>spring-security-core</artifactId>
|
<artifactId>spring-security-core</artifactId>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.security.core.AuthenticationException;
|
|||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
import org.springframework.web.bind.MissingRequestHeaderException;
|
import org.springframework.web.bind.MissingRequestHeaderException;
|
||||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
@@ -96,6 +97,14 @@ public class GlobalExceptionHandler {
|
|||||||
return failure(ResultCode.NOT_FOUND, ResultCode.NOT_FOUND.message(), request);
|
return failure(ResultCode.NOT_FOUND, ResultCode.NOT_FOUND.message(), request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(CannotAcquireLockException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleCannotAcquireLock(
|
||||||
|
CannotAcquireLockException exception, HttpServletRequest request) {
|
||||||
|
log.warn("Database lock acquisition conflict requestId={} traceId={}",
|
||||||
|
CorrelationContext.requestId(request), CorrelationContext.traceId(request));
|
||||||
|
return failure(ResultCode.CONFLICT, "资源正在处理中,请稍后重试", request);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception, HttpServletRequest request) {
|
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception, HttpServletRequest request) {
|
||||||
log.error("Unhandled request failure requestId={} traceId={}",
|
log.error("Unhandled request failure requestId={} traceId={}",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.springframework.web.servlet.resource.NoResourceFoundException;
|
|||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.dao.CannotAcquireLockException;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
@@ -116,6 +117,18 @@ class GlobalExceptionHandlerTest {
|
|||||||
assertThat(response.getBody().code()).isEqualTo(ResultCode.NOT_FOUND.code());
|
assertThat(response.getBody().code()).isEqualTo(ResultCode.NOT_FOUND.code());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void lockWaitTimeoutMapsToCanonicalConflict() {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
request.setAttribute(CorrelationContext.REQUEST_ID, "lock-timeout");
|
||||||
|
|
||||||
|
var response = handler.handleCannotAcquireLock(
|
||||||
|
new CannotAcquireLockException("Lock wait timeout exceeded"), request);
|
||||||
|
|
||||||
|
assertThat(response.getBody().code()).isEqualTo(ResultCode.CONFLICT.code());
|
||||||
|
assertThat(response.getBody().requestId()).isEqualTo("lock-timeout");
|
||||||
|
}
|
||||||
|
|
||||||
private void paging(long page) {
|
private void paging(long page) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,8 @@
|
|||||||
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-local-nacos-config.ps1
|
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-local-nacos-config.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
也可以使用统一入口;`local` 会被强制限制在 `public` namespace:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 -Environment local
|
|
||||||
```
|
|
||||||
|
|
||||||
配置发布成功后,各服务仍由自身的 Nacos Discovery 配置在启动时自动注册,不需要手工创建服务实例。
|
配置发布成功后,各服务仍由自身的 Nacos Discovery 配置在启动时自动注册,不需要手工创建服务实例。
|
||||||
|
|
||||||
本地海外链路也发布在同一 `public / BAIYUE` 作用域,包括
|
|
||||||
`gateway-overseas.yml`、`lastleg-service.yml` 和
|
|
||||||
`warehouse-overseas-service.yml`。这些配置默认通过 `127.0.0.1` 连接本机
|
|
||||||
Auth、国内仓库、文件和海外服务;需要跨主机运行时使用对应环境变量覆盖。
|
|
||||||
|
|
||||||
## MyBatis SQL 日志
|
## MyBatis SQL 日志
|
||||||
|
|
||||||
本地公共配置使用 MyBatis `Slf4jImpl`,数据库服务的 Mapper 日志级别默认为 `DEBUG`。执行数据库操作时,控制台会打印 SQL 模板、参数和影响行数。
|
本地公共配置使用 MyBatis `Slf4jImpl`,数据库服务的 Mapper 日志级别默认为 `DEBUG`。执行数据库操作时,控制台会打印 SQL 模板、参数和影响行数。
|
||||||
|
|||||||
@@ -5,7 +5,5 @@ spring.datasource:
|
|||||||
password: ${EVENT_BRIDGE_DB_PASSWORD:123456}
|
password: ${EVENT_BRIDGE_DB_PASSWORD:123456}
|
||||||
baiyue:
|
baiyue:
|
||||||
region: ${REGION_CODE:CN}
|
region: ${REGION_CODE:CN}
|
||||||
event-bridge:
|
|
||||||
accept-any-region: ${EVENT_BRIDGE_ACCEPT_ANY_REGION:true}
|
|
||||||
deployment-cell: ${DEPLOYMENT_CELL:local-cn}
|
deployment-cell: ${DEPLOYMENT_CELL:local-cn}
|
||||||
data-residency: ${DATA_RESIDENCY:CN}
|
data-residency: ${DATA_RESIDENCY:CN}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
springdoc:
|
|
||||||
api-docs.enabled: ${SWAGGER_ENABLED:true}
|
|
||||||
swagger-ui.enabled: ${SWAGGER_ENABLED:true}
|
|
||||||
baiyue:
|
|
||||||
security:
|
|
||||||
issuer: ${JWT_ISSUER:auth-service}
|
|
||||||
audience: ${JWT_AUDIENCE:lastleg-api}
|
|
||||||
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
|
|
||||||
pda-session-validation:
|
|
||||||
auth-url: ${AUTH_SESSION_VALIDATION_URL:http://127.0.0.1:9100}
|
|
||||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
|
|
||||||
timeout: ${PDA_SESSION_VALIDATION_TIMEOUT:2s}
|
|
||||||
overseas-projection:
|
|
||||||
service-url: ${LASTLEG_SECURITY_SERVICE_URL:http://127.0.0.1:9303}
|
|
||||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
|
|
||||||
timeout: ${LASTLEG_SECURITY_VALIDATION_TIMEOUT:2s}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
spring:
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: ${LASTLEG_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_lastleg?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
|
||||||
username: ${LASTLEG_DB_USERNAME:root}
|
|
||||||
password: ${LASTLEG_DB_PASSWORD:123456}
|
|
||||||
lastleg:
|
|
||||||
dispatch-query-api.enabled: ${LASTLEG_DISPATCH_QUERY_API_ENABLED:true}
|
|
||||||
smart-dispatch-api.enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}
|
|
||||||
baiyue:
|
|
||||||
clients:
|
|
||||||
domestic-warehouse:
|
|
||||||
url: ${DOMESTIC_WAREHOUSE_URL:http://127.0.0.1:9205}
|
|
||||||
cache-ttl: ${DOMESTIC_WAREHOUSE_CACHE_TTL:30m}
|
|
||||||
file:
|
|
||||||
url: ${FILE_SERVICE_URL:http://127.0.0.1:9206}
|
|
||||||
event-bridge:
|
|
||||||
url: ${CN_EVENT_BRIDGE_URL:${FIRSTLEG_EVENT_BRIDGE_URL:http://127.0.0.1:9312}}
|
|
||||||
warehouse:
|
|
||||||
base-url: ${WAREHOUSE_OVERSEAS_SERVICE_URL:http://127.0.0.1:9305}
|
|
||||||
firstleg:
|
|
||||||
event-bridge-url: ${CN_EVENT_BRIDGE_URL:${FIRSTLEG_EVENT_BRIDGE_URL:http://127.0.0.1:9312}}
|
|
||||||
security:
|
|
||||||
internal-service-token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
|
|
||||||
jwt-mode: jwks
|
|
||||||
session-mode: claims
|
|
||||||
issuer: ${JWT_ISSUER:auth-service}
|
|
||||||
audience: ${JWT_AUDIENCE:lastleg-api}
|
|
||||||
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
spring:
|
|
||||||
datasource:
|
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
|
||||||
url: ${WAREHOUSE_DB_URL:jdbc:mysql://127.0.0.1:3306/baiyue_lastlag_warehouse?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true}
|
|
||||||
username: ${WAREHOUSE_DB_USERNAME:root}
|
|
||||||
password: ${WAREHOUSE_DB_PASSWORD:123456}
|
|
||||||
baiyue:
|
|
||||||
clients:
|
|
||||||
domestic-warehouse:
|
|
||||||
url: ${DOMESTIC_WAREHOUSE_URL:http://127.0.0.1:9205}
|
|
||||||
cache-ttl: ${DOMESTIC_WAREHOUSE_CACHE_TTL:30m}
|
|
||||||
internal-service:
|
|
||||||
token: ${INTERNAL_SERVICE_TOKEN:phase1-local-internal-token-at-least-32-bytes}
|
|
||||||
security:
|
|
||||||
jwt-mode: jwks
|
|
||||||
session-mode: claims
|
|
||||||
issuer: ${JWT_ISSUER:auth-service}
|
|
||||||
audience: ${JWT_AUDIENCE:lastleg-api}
|
|
||||||
jwk-set-uri: ${JWT_JWK_SET_URI:http://127.0.0.1:9100/api/auth/.well-known/jwks.json}
|
|
||||||
@@ -1,19 +1,5 @@
|
|||||||
# 海外 Nacos 配置
|
# 海外 Nacos 配置
|
||||||
|
|
||||||
本目录是海外远程部署模板,由 dev/development、test 和 production 共用。配置只引用环境变量,不保存密码或访问密钥;三个环境必须发布到各自独立的 Nacos namespace,Gateway 必须同时启用 `overseas` Profile。
|
将本目录文件按原文件名作为 Data ID 发布到 `prod-overseas` 命名空间和 `BAIYUE` 分组。配置只引用环境变量,不保存密码或访问密钥。Gateway 必须同时启用 `overseas` Profile。
|
||||||
|
|
||||||
海外入口与仓储 Data ID 分别为 `gateway-overseas.yml`、`warehouse-overseas-service.yml`。`print-service.yml` 对应头程仓库保留的共用打印服务,不包含尾程旧 Print 模块。
|
海外入口与仓储 Data ID 分别为 `gateway-overseas.yml`、`warehouse-overseas-service.yml`。`print-service.yml` 对应头程仓库保留的共用打印服务,不包含尾程旧 Print 模块。
|
||||||
|
|
||||||
发布示例:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# 开发环境
|
|
||||||
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 `
|
|
||||||
-Environment development -Region overseas -ServerAddr <dev-nacos> -Namespace <dev-overseas-namespace>
|
|
||||||
|
|
||||||
# 测试环境
|
|
||||||
powershell -ExecutionPolicy Bypass -File D:\baiyue-firstleg\scripts\publish-nacos-config.ps1 `
|
|
||||||
-Environment test -Region overseas -ServerAddr <test-nacos> -Namespace <test-overseas-namespace>
|
|
||||||
```
|
|
||||||
|
|
||||||
远程环境禁止使用 `public` namespace。数据库、Redis、JWKS、内部服务令牌和跨区服务地址继续由对应 Jenkins Secret file 提供;不要把真实值写入本目录。
|
|
||||||
|
|||||||
@@ -4,36 +4,54 @@
|
|||||||
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
|
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.2.0 https://maven.apache.org/xsd/settings-1.2.0.xsd">
|
||||||
<mirrors>
|
<mirrors>
|
||||||
<mirror>
|
<mirror>
|
||||||
<id>aliyun-public</id>
|
<id>baiyue-nexus-public</id>
|
||||||
<name>阿里云 Maven 公共镜像</name>
|
<name>柏悦 Nexus Maven 聚合仓库</name>
|
||||||
<url>https://maven.aliyun.com/repository/public</url>
|
<url>http://8.163.30.128:18081/repository/maven-public/</url>
|
||||||
<mirrorOf>central</mirrorOf>
|
<mirrorOf>external:*</mirrorOf>
|
||||||
</mirror>
|
</mirror>
|
||||||
</mirrors>
|
</mirrors>
|
||||||
|
|
||||||
|
<servers>
|
||||||
|
<server>
|
||||||
|
<id>baiyue-nexus-public</id>
|
||||||
|
<username>${env.NEXUS_USERNAME}</username>
|
||||||
|
<password>${env.NEXUS_PASSWORD}</password>
|
||||||
|
</server>
|
||||||
|
<server>
|
||||||
|
<id>baiyue-nexus-releases</id>
|
||||||
|
<username>${env.NEXUS_USERNAME}</username>
|
||||||
|
<password>${env.NEXUS_PASSWORD}</password>
|
||||||
|
</server>
|
||||||
|
<server>
|
||||||
|
<id>baiyue-nexus-snapshots</id>
|
||||||
|
<username>${env.NEXUS_USERNAME}</username>
|
||||||
|
<password>${env.NEXUS_PASSWORD}</password>
|
||||||
|
</server>
|
||||||
|
</servers>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
<profile>
|
<profile>
|
||||||
<id>国内公共镜像</id>
|
<id>柏悦 Nexus</id>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>central</id>
|
<id>baiyue-nexus-public</id>
|
||||||
<url>https://repo.maven.apache.org/maven2</url>
|
<url>http://8.163.30.128:18081/repository/maven-public/</url>
|
||||||
<releases><enabled>true</enabled></releases>
|
<releases><enabled>true</enabled></releases>
|
||||||
<snapshots><enabled>false</enabled></snapshots>
|
<snapshots><enabled>true</enabled></snapshots>
|
||||||
</repository>
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
<pluginRepositories>
|
<pluginRepositories>
|
||||||
<pluginRepository>
|
<pluginRepository>
|
||||||
<id>central</id>
|
<id>baiyue-nexus-public</id>
|
||||||
<url>https://repo.maven.apache.org/maven2</url>
|
<url>http://8.163.30.128:18081/repository/maven-public/</url>
|
||||||
<releases><enabled>true</enabled></releases>
|
<releases><enabled>true</enabled></releases>
|
||||||
<snapshots><enabled>false</enabled></snapshots>
|
<snapshots><enabled>true</enabled></snapshots>
|
||||||
</pluginRepository>
|
</pluginRepository>
|
||||||
</pluginRepositories>
|
</pluginRepositories>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
||||||
<activeProfiles>
|
<activeProfiles>
|
||||||
<activeProfile>国内公共镜像</activeProfile>
|
<activeProfile>柏悦 Nexus</activeProfile>
|
||||||
</activeProfiles>
|
</activeProfiles>
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
+85
-259
@@ -2502,7 +2502,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
||||||
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
||||||
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
||||||
last_overseas_hold_version BIGINT UNSIGNED NULL COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖',
|
|
||||||
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
||||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '确认后的实际总毛重kg',
|
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '确认后的实际总毛重kg',
|
||||||
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
||||||
@@ -2792,7 +2791,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
|||||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
||||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
|
||||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||||
@@ -2830,7 +2828,6 @@ BEGIN
|
|||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
CALL unified_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
|
CALL unified_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
|
||||||
CALL unified_carton_label_add_column('sku_code', 'VARCHAR(128) NULL COMMENT ''箱唛绑定的SKU快照;二维码内容不变'' AFTER cargo_name');
|
|
||||||
CALL unified_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
|
CALL unified_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
|
||||||
CALL unified_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
|
CALL unified_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
|
||||||
CALL unified_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
|
CALL unified_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
|
||||||
@@ -5368,7 +5365,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
|||||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
||||||
cabinet_no VARCHAR(64) NOT NULL COMMENT '稳定内部柜次号快照',
|
cabinet_no VARCHAR(64) NOT NULL COMMENT '稳定内部柜次号快照',
|
||||||
container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份或随运输号变更重写',
|
container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份或随运输号变更重写',
|
||||||
task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成',
|
task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING、COMPLETING、COMPLETION_FAILED、COMPLETED',
|
||||||
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
||||||
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
||||||
started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
|
started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
|
||||||
@@ -5381,7 +5378,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
|||||||
UNIQUE KEY uk_loading_pick_task_booking (container_booking_id),
|
UNIQUE KEY uk_loading_pick_task_booking (container_booking_id),
|
||||||
KEY idx_loading_pick_container (container_no, task_status, started_at),
|
KEY idx_loading_pick_container (container_no, task_status, started_at),
|
||||||
CONSTRAINT fk_loading_pick_task_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
CONSTRAINT fk_loading_pick_task_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||||
CONSTRAINT chk_loading_pick_task_status CHECK (task_status IN ('LOADING', 'COMPLETED'))
|
CONSTRAINT chk_loading_pick_task_status CHECK (task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED','COMPLETED'))
|
||||||
) ENGINE=InnoDB COMMENT='PDA装柜拣货任务';
|
) ENGINE=InnoDB COMMENT='PDA装柜拣货任务';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_item (
|
CREATE TABLE IF NOT EXISTS fl_loading_pick_item (
|
||||||
@@ -5425,66 +5422,6 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history (
|
|||||||
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
||||||
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (
|
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场作业托盘ID',
|
|
||||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
|
||||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID快照',
|
|
||||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
|
||||||
warehouse_pallet_id BIGINT UNSIGNED NULL COMMENT '仓库实物托盘ID;未绑定仓库资源时为空',
|
|
||||||
pallet_code VARCHAR(64) NOT NULL COMMENT '现场托盘号',
|
|
||||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '开托幂等操作号',
|
|
||||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作号',
|
|
||||||
pallet_status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '托盘状态:OPEN打托中、SEALED已封托、CANCELLED已取消',
|
|
||||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
|
||||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人名称快照',
|
|
||||||
sealed_by BIGINT UNSIGNED NULL COMMENT '封托操作人ID',
|
|
||||||
sealed_by_name VARCHAR(64) NULL COMMENT '封托操作人名称快照',
|
|
||||||
sealed_at DATETIME(3) NULL COMMENT '封托时间',
|
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_code (pick_task_id, pallet_code),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_open_operation (open_operation_id),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_seal_operation (seal_operation_id),
|
|
||||||
KEY idx_loading_work_pallet_plan (loading_plan_id, pallet_status, id),
|
|
||||||
CONSTRAINT fk_loading_work_pallet_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
|
||||||
CONSTRAINT fk_loading_work_pallet_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
|
||||||
CONSTRAINT chk_loading_work_pallet_status CHECK (pallet_status IN ('OPEN','SEALED','CANCELLED'))
|
|
||||||
) ENGINE=InnoDB COMMENT='一件代发PDA现场实际托盘;允许同托盘承载多个客户订单';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (
|
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场托盘箱唛绑定ID',
|
|
||||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
|
||||||
work_pallet_id BIGINT UNSIGNED NOT NULL COMMENT '现场作业托盘ID',
|
|
||||||
loading_plan_item_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划货物明细ID',
|
|
||||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '箱唛所属客户ID',
|
|
||||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓单ID',
|
|
||||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓货物明细ID',
|
|
||||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
|
||||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱唛箱码快照',
|
|
||||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照',
|
|
||||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数量快照',
|
|
||||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '最后绑定操作人ID',
|
|
||||||
bound_by_name VARCHAR(64) NULL COMMENT '最后绑定操作人名称快照',
|
|
||||||
operation_id VARCHAR(128) NOT NULL COMMENT '最近一次绑定或移托幂等操作号',
|
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次上托时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后移托时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_loading_work_carton_task_label (pick_task_id, carton_label_id),
|
|
||||||
UNIQUE KEY uk_loading_work_carton_operation (operation_id),
|
|
||||||
KEY idx_loading_work_carton_pallet (work_pallet_id, sku_code, id),
|
|
||||||
KEY idx_loading_work_carton_plan_item (loading_plan_item_id, id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_pallet FOREIGN KEY (work_pallet_id) REFERENCES fl_loading_work_pallet (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_plan_item FOREIGN KEY (loading_plan_item_id) REFERENCES fl_loading_plan_item (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_pick_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label (id),
|
|
||||||
CONSTRAINT chk_loading_work_carton_units CHECK (units_per_carton IS NULL OR units_per_carton > 0)
|
|
||||||
) ENGINE=InnoDB COMMENT='一件代发PDA逐箱上托事实;客户订单不合并但实际托盘可混托';
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE repair_phase3_loading_pick_task_foreign_keys()
|
CREATE PROCEDURE repair_phase3_loading_pick_task_foreign_keys()
|
||||||
@@ -5681,7 +5618,6 @@ BEGIN
|
|||||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||||
WHERE pick_item.loading_plan_item_id = item.id
|
WHERE pick_item.loading_plan_item_id = item.id
|
||||||
AND pick_task.loading_plan_id = OLD.id
|
AND pick_task.loading_plan_id = OLD.id
|
||||||
AND pick_task.task_status = 'LOADING'
|
|
||||||
AND (
|
AND (
|
||||||
(pick_item.item_status = 'PICKED'
|
(pick_item.item_status = 'PICKED'
|
||||||
AND pick_item.picked_package_count = item.package_count)
|
AND pick_item.picked_package_count = item.package_count)
|
||||||
@@ -6536,9 +6472,7 @@ ON DUPLICATE KEY UPDATE
|
|||||||
location_type = VALUES(location_type),
|
location_type = VALUES(location_type),
|
||||||
max_weight_kg = VALUES(max_weight_kg),
|
max_weight_kg = VALUES(max_weight_kg),
|
||||||
shared_overflow = VALUES(shared_overflow),
|
shared_overflow = VALUES(shared_overflow),
|
||||||
status = 'AVAILABLE',
|
remark = VALUES(remark);
|
||||||
remark = VALUES(remark),
|
|
||||||
deleted = 0;
|
|
||||||
|
|
||||||
INSERT INTO wh_location (
|
INSERT INTO wh_location (
|
||||||
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
|
id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_name,
|
||||||
@@ -6557,9 +6491,7 @@ ON DUPLICATE KEY UPDATE
|
|||||||
location_type = VALUES(location_type),
|
location_type = VALUES(location_type),
|
||||||
max_weight_kg = VALUES(max_weight_kg),
|
max_weight_kg = VALUES(max_weight_kg),
|
||||||
shared_overflow = VALUES(shared_overflow),
|
shared_overflow = VALUES(shared_overflow),
|
||||||
status = 'AVAILABLE',
|
remark = VALUES(remark);
|
||||||
remark = VALUES(remark),
|
|
||||||
deleted = 0;
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
DROP PROCEDURE IF EXISTS assert_yiwu_location_seed;
|
||||||
DELIMITER //
|
DELIMITER //
|
||||||
@@ -13507,7 +13439,6 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_at', '
|
|||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by', '创建人ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by', '创建人ID');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by_name', '创建人名称快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'created_by_name', '创建人名称快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'current_hold_reason', '当前扣货理由快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'current_hold_reason', '当前扣货理由快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'last_overseas_hold_version', '海外暂扣事件最新聚合版本,用于阻止乱序覆盖');
|
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_id', '客户逻辑ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_id', '客户逻辑ID');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_name', '客户名称快照');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customer_name', '客户名称快照');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customs_inspection', '是否商检');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_inbound_order', 'customs_inspection', '是否商检');
|
||||||
@@ -13836,7 +13767,7 @@ CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'loading_pl
|
|||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_at', '开始时间');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_at', '开始时间');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by', '开始装柜人');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by', '开始装柜人');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by_name', '开始装柜人名称');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'started_by_name', '开始装柜人名称');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'task_status', 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'task_status', 'PDA任务状态:LOADING、COMPLETING、COMPLETION_FAILED、COMPLETED');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'version', '乐观锁版本号');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_pick_task', 'version', '乐观锁版本号');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_at', '确认时间');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_at', '确认时间');
|
||||||
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_by', '确认操作人ID');
|
CALL queue_column_comment('baiyue_firstleg', 'fl_loading_plan', 'confirmed_by', '确认操作人ID');
|
||||||
@@ -15697,9 +15628,11 @@ CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_container_transport_history (
|
|||||||
id BIGINT UNSIGNED NOT NULL COMMENT '运输字段变更历史ID',container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '运输字段变更历史ID',container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
||||||
before_container_no VARCHAR(64) NULL COMMENT '变更前真实集装箱号',after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
before_container_no VARCHAR(64) NULL COMMENT '变更前真实集装箱号',after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
||||||
before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
||||||
|
operation_id VARCHAR(64) NOT NULL COMMENT '运输字段操作幂等ID',request_hash CHAR(64) NOT NULL COMMENT '运输字段请求哈希',
|
||||||
reason VARCHAR(500) NULL COMMENT '运输字段变更原因',operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
reason VARCHAR(500) NULL COMMENT '运输字段变更原因',operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
||||||
operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '变更记录创建时间',
|
operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '变更记录创建时间',
|
||||||
PRIMARY KEY(id),KEY idx_container_transport_history(container_booking_id,created_at,id)
|
PRIMARY KEY(id),UNIQUE KEY uk_container_transport_history_operation(container_booking_id,operator_id,operation_id),
|
||||||
|
KEY idx_container_transport_history(container_booking_id,created_at,id)
|
||||||
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
|
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
|
||||||
|
|
||||||
ALTER TABLE baiyue_firstleg.fl_container_transport_history
|
ALTER TABLE baiyue_firstleg.fl_container_transport_history
|
||||||
@@ -15709,6 +15642,8 @@ ALTER TABLE baiyue_firstleg.fl_container_transport_history
|
|||||||
MODIFY COLUMN after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
MODIFY COLUMN after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
||||||
MODIFY COLUMN before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',
|
MODIFY COLUMN before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',
|
||||||
MODIFY COLUMN after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
MODIFY COLUMN after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
||||||
|
MODIFY COLUMN operation_id VARCHAR(64) NOT NULL COMMENT '运输字段操作幂等ID',
|
||||||
|
MODIFY COLUMN request_hash CHAR(64) NOT NULL COMMENT '运输字段请求哈希',
|
||||||
MODIFY COLUMN reason VARCHAR(500) NULL COMMENT '运输字段变更原因',
|
MODIFY COLUMN reason VARCHAR(500) NULL COMMENT '运输字段变更原因',
|
||||||
MODIFY COLUMN operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
MODIFY COLUMN operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
||||||
MODIFY COLUMN operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',
|
MODIFY COLUMN operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',
|
||||||
@@ -16024,6 +15959,14 @@ CREATE TABLE IF NOT EXISTS wh_supplier (
|
|||||||
CONSTRAINT chk_wh_supplier_status CHECK(status IN ('ACTIVE','INACTIVE')),
|
CONSTRAINT chk_wh_supplier_status CHECK(status IN ('ACTIVE','INACTIVE')),
|
||||||
CONSTRAINT chk_wh_supplier_settlement CHECK(settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
CONSTRAINT chk_wh_supplier_settlement CHECK(settlement_period IN ('MONTHLY_30','MONTHLY_45','MONTHLY_60','MONTHLY_90','PER_TRANSACTION','IMMEDIATE'))
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='平台公共供应商业务档案';
|
||||||
|
CREATE TABLE IF NOT EXISTS wh_supplier_carrier (
|
||||||
|
supplier_id BIGINT UNSIGNED NOT NULL COMMENT 'BOOKING供应商ID', tenant_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '平台公共作用域,固定为0',
|
||||||
|
carrier_code VARCHAR(16) NOT NULL COMMENT '船司标准编码', created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
PRIMARY KEY(supplier_id,carrier_code), KEY idx_wh_supplier_carrier_code(tenant_id,carrier_code,supplier_id),
|
||||||
|
CONSTRAINT chk_wh_supplier_carrier_tenant CHECK(tenant_id=0),
|
||||||
|
CONSTRAINT chk_wh_supplier_carrier_code CHECK(carrier_code IN('MATSON','SML','ZIM','COSCO','EMC','HEDE','OTHER'))
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BOOKING供应商与标准船司编码多对多关联';
|
||||||
DROP PROCEDURE IF EXISTS supplier_add_column_if_missing;
|
DROP PROCEDURE IF EXISTS supplier_add_column_if_missing;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE supplier_add_column_if_missing(IN p_name VARCHAR(64),IN p_definition TEXT)
|
CREATE PROCEDURE supplier_add_column_if_missing(IN p_name VARCHAR(64),IN p_definition TEXT)
|
||||||
@@ -16085,6 +16028,8 @@ CREATE PROCEDURE assert_platform_supplier_permissions()
|
|||||||
BEGIN
|
BEGIN
|
||||||
IF (SELECT COUNT(*) FROM baiyue_warehouse.wh_supplier WHERE tenant_id<>0)<>0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商存在非平台作用域数据'; END IF;
|
IF (SELECT COUNT(*) FROM baiyue_warehouse.wh_supplier WHERE tenant_id<>0)<>0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商存在非平台作用域数据'; END IF;
|
||||||
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND index_name IN('PRIMARY','uk_wh_supplier_name','idx_wh_supplier_filter'))<>3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商表索引不完整'; END IF;
|
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier' AND index_name IN('PRIMARY','uk_wh_supplier_name','idx_wh_supplier_filter'))<>3 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商表索引不完整'; END IF;
|
||||||
|
IF (SELECT COUNT(DISTINCT index_name) FROM information_schema.statistics WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier_carrier' AND index_name IN('PRIMARY','idx_wh_supplier_carrier_code'))<>2 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商船司关联表索引不完整'; END IF;
|
||||||
|
IF (SELECT COUNT(*) FROM information_schema.table_constraints WHERE table_schema='baiyue_warehouse' AND table_name='wh_supplier_carrier' AND constraint_type='CHECK' AND constraint_name IN('chk_wh_supplier_carrier_tenant','chk_wh_supplier_carrier_code'))<>2 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商船司关联表约束不完整'; END IF;
|
||||||
IF (SELECT COUNT(*) FROM iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND status='ACTIVE' AND deleted=0 AND permission_code IN('platform:supplier:list','platform:supplier:create','platform:supplier:edit','platform:supplier:status'))<>4 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商权限资源初始化不完整'; END IF;
|
IF (SELECT COUNT(*) FROM iam_permission WHERE tenant_id=0 AND app_code='PLATFORM' AND status='ACTIVE' AND deleted=0 AND permission_code IN('platform:supplier:list','platform:supplier:create','platform:supplier:edit','platform:supplier:status'))<>4 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='供应商权限资源初始化不完整'; END IF;
|
||||||
IF (SELECT COUNT(*) FROM iam_role_permission rp JOIN iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE r.role_code='PLATFORM_ADMIN' AND p.permission_code IN('platform:supplier:list','platform:supplier:create','platform:supplier:edit','platform:supplier:status'))<>4 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PLATFORM_ADMIN供应商权限授权不完整'; END IF;
|
IF (SELECT COUNT(*) FROM iam_role_permission rp JOIN iam_role r ON r.id=rp.role_id AND r.tenant_id=rp.tenant_id JOIN iam_permission p ON p.id=rp.permission_id AND p.tenant_id=rp.tenant_id WHERE r.role_code='PLATFORM_ADMIN' AND p.permission_code IN('platform:supplier:list','platform:supplier:create','platform:supplier:edit','platform:supplier:status'))<>4 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PLATFORM_ADMIN供应商权限授权不完整'; END IF;
|
||||||
END$$
|
END$$
|
||||||
@@ -16302,35 +16247,12 @@ DROP PROCEDURE IF EXISTS add_container_empty_report_fields;
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
||||||
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID', event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||||
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本', aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本', aggregate_id VARCHAR(64) NOT NULL COMMENT '货柜聚合ID',
|
||||||
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本', payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '货柜聚合版本', payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
||||||
payload_json JSON NOT NULL COMMENT '原始事件负载', status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
payload_json JSON NOT NULL COMMENT '原始事件负载', status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
||||||
received_at DATETIME(3) NOT NULL COMMENT '接收时间(UTC)', processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
received_at DATETIME(3) NOT NULL COMMENT '接收时间(UTC)', processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
||||||
PRIMARY KEY (event_id), KEY idx_container_event_aggregate (aggregate_id,aggregate_version)
|
PRIMARY KEY (event_id), KEY idx_container_event_aggregate (aggregate_id,aggregate_version)
|
||||||
) ENGINE=InnoDB COMMENT='海外到国内业务事件消费Inbox';
|
) ENGINE=InnoDB COMMENT='海外货柜事件消费Inbox';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS add_inbound_overseas_hold_version;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE add_inbound_overseas_hold_version()
|
|
||||||
BEGIN
|
|
||||||
IF NOT EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema=DATABASE() AND table_name='fl_inbound_order'
|
|
||||||
AND column_name='last_overseas_hold_version'
|
|
||||||
) THEN
|
|
||||||
ALTER TABLE fl_inbound_order
|
|
||||||
ADD COLUMN last_overseas_hold_version BIGINT UNSIGNED NULL
|
|
||||||
COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖'
|
|
||||||
AFTER current_hold_reason;
|
|
||||||
END IF;
|
|
||||||
ALTER TABLE fl_container_event_inbox
|
|
||||||
MODIFY COLUMN aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
|
||||||
MODIFY COLUMN aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本',
|
|
||||||
COMMENT='海外到国内业务事件消费Inbox';
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
CALL add_inbound_overseas_hold_version();
|
|
||||||
DROP PROCEDURE IF EXISTS add_inbound_overseas_hold_version;
|
|
||||||
|
|
||||||
-- 标准跨区事件摘要包含 sha256: 前缀,共 71 字符;兼容既有 Finance 64 位摘要。
|
-- 标准跨区事件摘要包含 sha256: 前缀,共 71 字符;兼容既有 Finance 64 位摘要。
|
||||||
ALTER TABLE fl_inbound_outbox
|
ALTER TABLE fl_inbound_outbox
|
||||||
@@ -16377,9 +16299,6 @@ VALUES
|
|||||||
(5107061,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:create','创建海外库位',NULL,'POST',761,'ACTIVE',1,0),
|
(5107061,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:create','创建海外库位',NULL,'POST',761,'ACTIVE',1,0),
|
||||||
(5107062,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:edit','编辑海外库位',NULL,'PUT',762,'ACTIVE',1,0),
|
(5107062,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:edit','编辑海外库位',NULL,'PUT',762,'ACTIVE',1,0),
|
||||||
(5107063,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:delete','删除海外库位',NULL,'DELETE',763,'ACTIVE',1,0),
|
(5107063,0,'PLATFORM','BUTTON',5107060,'platform:overseas-warehouse:location:delete','删除海外库位',NULL,'DELETE',763,'ACTIVE',1,0),
|
||||||
(5107090,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:outbound:list','海外出库单','/overseas-warehouse/outbounds','GET',765,'ACTIVE',1,0),
|
|
||||||
(5107091,0,'PLATFORM','BUTTON',5107090,'platform:overseas-warehouse:outbound:create','创建海外出库单',NULL,'POST',766,'ACTIVE',1,0),
|
|
||||||
(5107092,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:delivery:list','海外派送单','/overseas-warehouse/delivery-orders','GET',767,'ACTIVE',1,0),
|
|
||||||
(5107100,0,'PLATFORM','MENU',0,'platform:overseas-business:purchase:list','海外采购订单','/overseas-business/purchases','GET',800,'ACTIVE',1,0),
|
(5107100,0,'PLATFORM','MENU',0,'platform:overseas-business:purchase:list','海外采购订单','/overseas-business/purchases','GET',800,'ACTIVE',1,0),
|
||||||
(5107101,0,'PLATFORM','BUTTON',5107100,'platform:overseas-business:purchase:edit','编辑海外采购订单',NULL,'PATCH',801,'ACTIVE',1,0),
|
(5107101,0,'PLATFORM','BUTTON',5107100,'platform:overseas-business:purchase:edit','编辑海外采购订单',NULL,'PATCH',801,'ACTIVE',1,0),
|
||||||
(5107102,0,'PLATFORM','BUTTON',5107100,'platform:overseas-business:purchase:review','审核海外采购订单',NULL,'POST',802,'ACTIVE',1,0),
|
(5107102,0,'PLATFORM','BUTTON',5107100,'platform:overseas-business:purchase:review','审核海外采购订单',NULL,'POST',802,'ACTIVE',1,0),
|
||||||
@@ -16554,152 +16473,6 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
|||||||
AND UPPER(role.role_code) IN ('PLATFORM_ADMIN','PLATFORM_CUSTOM_ADMIN')
|
AND UPPER(role.role_code) IN ('PLATFORM_ADMIN','PLATFORM_CUSTOM_ADMIN')
|
||||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||||
|
|
||||||
-- PLATFORM_CUSTOM_ADMIN needs access to the overseas outbound and delivery lists shown in the admin navigation.
|
|
||||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id, role_id, permission_id, created_at)
|
|
||||||
SELECT 0, role.id, permission.id, NOW(3)
|
|
||||||
FROM baiyue_iam.iam_role role
|
|
||||||
JOIN baiyue_iam.iam_permission permission ON permission.tenant_id=0 AND permission.app_code='PLATFORM'
|
|
||||||
AND permission.permission_code IN (
|
|
||||||
'platform:overseas-warehouse:outbound:list',
|
|
||||||
'platform:overseas-warehouse:delivery:list'
|
|
||||||
)
|
|
||||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
|
||||||
AND UPPER(role.role_code)='PLATFORM_CUSTOM_ADMIN'
|
|
||||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
|
||||||
|
|
||||||
-- 海外智能排车草稿锁:拖入即锁定,移除或关闭草稿即释放。
|
|
||||||
CREATE DATABASE IF NOT EXISTS baiyue_lastleg
|
|
||||||
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
|
||||||
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='source_warehouse_id'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_id VARCHAR(128) NULL COMMENT ''来源国内装柜仓 ID 快照'' AFTER customer_name'));
|
|
||||||
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='source_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN source_warehouse_name VARCHAR(255) NULL COMMENT ''来源国内装柜仓名称快照'' AFTER source_warehouse_id'));
|
|
||||||
PREPARE lastleg_order_source_warehouse_stmt FROM @ddl; EXECUTE lastleg_order_source_warehouse_stmt; DEALLOCATE PREPARE lastleg_order_source_warehouse_stmt;
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
|
|
||||||
id BIGINT NOT NULL COMMENT '主键ID',
|
|
||||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
|
||||||
source_pallet_key VARCHAR(160) NOT NULL COMMENT '来源托盘业务键',
|
|
||||||
active_key VARCHAR(160) NULL COMMENT '生效中的资源排他键',
|
|
||||||
plan_id BIGINT NULL COMMENT '入仓计划ID',
|
|
||||||
plan_pallet_id BIGINT NULL COMMENT '计划托盘ID',
|
|
||||||
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID',
|
|
||||||
dispatch_order_id BIGINT NULL COMMENT '正式派车单ID,草稿锁阶段为空',
|
|
||||||
draft_token VARCHAR(64) NULL COMMENT '智能排车草稿随机标识',
|
|
||||||
operator_id VARCHAR(128) NULL COMMENT '草稿锁操作人ID',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态',
|
|
||||||
locked_at DATETIME(3) NOT NULL COMMENT '锁定时间',
|
|
||||||
expires_at DATETIME(3) NULL COMMENT '草稿锁失效时间,正式派车锁为空',
|
|
||||||
released_at DATETIME(3) NULL COMMENT '释放时间',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_ll_dispatch_active_pallet (customer_id,active_key),
|
|
||||||
KEY idx_ll_dispatch_pallet_lock_order (customer_id,dispatch_order_id,status),
|
|
||||||
KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status)
|
|
||||||
) ENGINE=InnoDB COMMENT='派车托盘排他锁';
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='draft_token'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN draft_token VARCHAR(64) NULL COMMENT ''智能排车草稿随机标识'' AFTER dispatch_order_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_dispatch_pallet_lock' AND column_name='operator_id'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN operator_id VARCHAR(128) NULL COMMENT ''草稿锁操作人ID'' AFTER draft_token'));
|
|
||||||
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_dispatch_pallet_lock' AND column_name='expires_at'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN expires_at DATETIME(3) NULL COMMENT ''草稿锁失效时间,正式派车锁为空'' AFTER locked_at'));
|
|
||||||
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_dispatch_pallet_lock' AND column_name='dispatch_order_id' AND is_nullable='YES'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock MODIFY COLUMN dispatch_order_id BIGINT NULL COMMENT ''正式派车单ID,草稿锁阶段为空'''));
|
|
||||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
|
||||||
|
|
||||||
-- 尾程库存余额按客户入仓单与海外目的仓汇总箱数,SKU仅保留为快照。
|
|
||||||
SET @ddl = (SELECT CASE
|
|
||||||
WHEN EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
|
||||||
AND table_name='ll_inventory_balance' AND index_name='uk_ll_inventory_dimension' AND column_name='sku_code')
|
|
||||||
THEN 'ALTER TABLE ll_inventory_balance DROP INDEX uk_ll_inventory_dimension, ADD UNIQUE KEY uk_ll_inventory_dimension (customer_id,overseas_warehouse_id,destination_warehouse_code,inbound_order_id,inbound_code)'
|
|
||||||
WHEN NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
|
||||||
AND table_name='ll_inventory_balance' AND index_name='uk_ll_inventory_dimension')
|
|
||||||
THEN 'ALTER TABLE ll_inventory_balance ADD UNIQUE KEY uk_ll_inventory_dimension (customer_id,overseas_warehouse_id,destination_warehouse_code,inbound_order_id,inbound_code)'
|
|
||||||
ELSE 'SELECT 1' END);
|
|
||||||
PREPARE lastleg_inventory_dimension_stmt FROM @ddl; EXECUTE lastleg_inventory_dimension_stmt; DEALLOCATE PREPARE lastleg_inventory_dimension_stmt;
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
|
||||||
AND table_name='ll_inventory_balance' AND column_name='sku_code'),
|
|
||||||
'ALTER TABLE ll_inventory_balance MODIFY COLUMN sku_code VARCHAR(128) NOT NULL DEFAULT ''UNSPECIFIED'' COMMENT ''SKU快照;多SKU汇总时为MIXED,不参与当前库存唯一维度''',
|
|
||||||
'SELECT 1'));
|
|
||||||
PREPARE lastleg_inventory_dimension_stmt FROM @ddl; EXECUTE lastleg_inventory_dimension_stmt; DEALLOCATE PREPARE lastleg_inventory_dimension_stmt;
|
|
||||||
ALTER TABLE ll_inventory_balance COMMENT='客户入仓单与海外目的仓维度实时库存余额';
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
|
||||||
AND table_name='ll_local_looseboard_order' AND column_name='lastleg_order_id'), 'SELECT 1',
|
|
||||||
'ALTER TABLE ll_local_looseboard_order ADD COLUMN lastleg_order_id BIGINT NULL COMMENT ''关联尾程主订单ID'' AFTER customer_id'));
|
|
||||||
PREPARE lastleg_looseboard_tail_stmt FROM @ddl; EXECUTE lastleg_looseboard_tail_stmt; DEALLOCATE PREPARE lastleg_looseboard_tail_stmt;
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
|
||||||
AND table_name='ll_local_looseboard_order' AND index_name='uk_ll_local_looseboard_lastleg'), 'SELECT 1',
|
|
||||||
'ALTER TABLE ll_local_looseboard_order ADD UNIQUE KEY uk_ll_local_looseboard_lastleg(lastleg_order_id)'));
|
|
||||||
PREPARE lastleg_looseboard_tail_stmt FROM @ddl; EXECUTE lastleg_looseboard_tail_stmt; DEALLOCATE PREPARE lastleg_looseboard_tail_stmt;
|
|
||||||
|
|
||||||
-- 派送单列表与详情快照、转派送费用及操作记录(当前上下文为尾程库)。
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_delivery_order' AND column_name='overseas_warehouse_name'), 'SELECT 1', 'ALTER TABLE ll_delivery_order ADD COLUMN overseas_warehouse_name VARCHAR(255) NULL COMMENT ''海外仓名称快照'', ADD COLUMN customer_name VARCHAR(255) NULL COMMENT ''客户名称快照'', ADD COLUMN waybill_no VARCHAR(128) NULL COMMENT ''承运运单号'', ADD COLUMN delivery_method VARCHAR(32) NULL COMMENT ''派送方式'', ADD COLUMN carrier_name VARCHAR(255) NULL COMMENT ''派送承运商'', ADD COLUMN pickup_method VARCHAR(64) NULL COMMENT ''提货方式'', ADD COLUMN loading_type VARCHAR(64) NULL COMMENT ''装车类型'', ADD COLUMN vehicle_type VARCHAR(64) NULL COMMENT ''车辆类型'', ADD COLUMN vehicle_size VARCHAR(64) NULL COMMENT ''车辆大小'', ADD COLUMN vehicle_plate VARCHAR(64) NULL COMMENT ''车牌号'', ADD COLUMN driver_name VARCHAR(128) NULL COMMENT ''司机或车队名称'', ADD COLUMN driver_phone VARCHAR(64) NULL COMMENT ''车队联系电话'', ADD COLUMN appointment_account VARCHAR(128) NULL COMMENT ''预约账号'', ADD COLUMN seller_name VARCHAR(128) NULL COMMENT ''卖家姓名'', ADD COLUMN total_weight_kg DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''总重量(千克)'', ADD COLUMN total_volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT ''总体积(立方米)'', ADD COLUMN planned_delivery_at DATETIME(3) NULL COMMENT ''预计派送时间'', ADD COLUMN dispatched_at DATETIME(3) NULL COMMENT ''实际派送时间'', ADD COLUMN expected_arrival_at DATETIME(3) NULL COMMENT ''预计到达时间'', ADD COLUMN signed_at DATETIME(3) NULL COMMENT ''签收时间'', ADD COLUMN remark VARCHAR(1000) NULL COMMENT ''派送备注'''));
|
|
||||||
PREPARE delivery_detail_stmt FROM @ddl; EXECUTE delivery_detail_stmt; DEALLOCATE PREPARE delivery_detail_stmt;
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_delivery_fee (id BIGINT NOT NULL COMMENT '派送费用主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', fee_direction VARCHAR(16) NOT NULL DEFAULT 'PAYABLE' COMMENT '应收应付方向', fee_type VARCHAR(64) NOT NULL COMMENT '费用类型', currency VARCHAR(8) NOT NULL COMMENT '币种', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额', remark VARCHAR(500) NULL COMMENT '费用备注', created_by VARCHAR(128) NULL COMMENT '创建人ID', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id), KEY idx_ll_delivery_fee_order(customer_id,delivery_order_id,id)) ENGINE=InnoDB COMMENT='派送单费用明细';
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_delivery_operation (id BIGINT NOT NULL COMMENT '派送操作主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键', delivery_order_id BIGINT NOT NULL COMMENT '派送单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点', operator_id VARCHAR(128) NULL COMMENT '操作人ID', operator_name VARCHAR(128) NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间', PRIMARY KEY(id), KEY idx_ll_delivery_operation_order(customer_id,delivery_order_id,created_at,id)) ENGINE=InnoDB COMMENT='派送单操作记录';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_exception_order (
|
|
||||||
id BIGINT NOT NULL, customer_id VARCHAR(128) NOT NULL, exception_no VARCHAR(64) NOT NULL,
|
|
||||||
exception_type VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'PENDING', reason VARCHAR(1000) NOT NULL,
|
|
||||||
last_mile_order_id BIGINT NULL, last_mile_order_no VARCHAR(64) NULL, inbound_order_id BIGINT NULL, inbound_code VARCHAR(128) NULL,
|
|
||||||
delivery_order_id BIGINT NULL, delivery_no VARCHAR(64) NULL, outbound_no VARCHAR(64) NULL,
|
|
||||||
customer_name VARCHAR(255) NULL, overseas_warehouse_id VARCHAR(128) NULL, overseas_warehouse_name VARCHAR(255) NULL,
|
|
||||||
sku_code VARCHAR(128) NULL, package_count INT NULL, return_direction VARCHAR(32) NULL, return_logistics_no VARCHAR(128) NULL,
|
|
||||||
handling_action VARCHAR(500) NULL, handler_id VARCHAR(128) NULL, handler_name VARCHAR(128) NULL,
|
|
||||||
source_type VARCHAR(32) NOT NULL DEFAULT 'MANUAL', created_by VARCHAR(128) NULL,
|
|
||||||
exception_at DATETIME(3) NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
|
||||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_exception_no(customer_id,exception_no),
|
|
||||||
UNIQUE KEY uk_ll_exception_delivery_return(customer_id,delivery_order_id,exception_type),
|
|
||||||
KEY idx_ll_exception_list(customer_id,status,exception_at), KEY idx_ll_exception_tail(customer_id,last_mile_order_no),
|
|
||||||
KEY idx_ll_exception_inbound(customer_id,inbound_code)
|
|
||||||
) ENGINE=InnoDB COMMENT='海外仓异常订单';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_customer_visit (
|
|
||||||
id BIGINT NOT NULL COMMENT '客户回访记录ID',
|
|
||||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
|
|
||||||
delivery_order_id BIGINT NOT NULL COMMENT '关联派送单ID',
|
|
||||||
delivery_no VARCHAR(64) NOT NULL COMMENT '关联派送单号',
|
|
||||||
customer_name VARCHAR(255) NULL COMMENT '客户名称快照',
|
|
||||||
contact_name VARCHAR(128) NULL COMMENT '客户联系人姓名快照',
|
|
||||||
contact_phone VARCHAR(64) NULL COMMENT '客户联系人电话快照',
|
|
||||||
destination VARCHAR(500) NULL COMMENT '派送目的仓编码汇总快照',
|
|
||||||
delivery_completed_at DATETIME(3) NOT NULL COMMENT '派送完成时间',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '回访状态:PENDING待回访或COMPLETED已回访',
|
|
||||||
selected_last_mile_order_id BIGINT NULL COMMENT '本次回访选择的尾程订单ID',
|
|
||||||
selected_last_mile_order_no VARCHAR(64) NULL COMMENT '本次回访选择的尾程订单号',
|
|
||||||
selected_inbound_order_id BIGINT NULL COMMENT '本次回访选择的入仓单ID',
|
|
||||||
selected_inbound_code VARCHAR(128) NULL COMMENT '本次回访选择的入仓码',
|
|
||||||
visit_result VARCHAR(32) NULL COMMENT '最近一次回访结果',
|
|
||||||
customer_feedback VARCHAR(2000) NULL COMMENT '最近一次客户反馈',
|
|
||||||
remark VARCHAR(1000) NULL COMMENT '最近一次回访备注',
|
|
||||||
attachment_json JSON NULL COMMENT '当前回访资料文件元数据列表',
|
|
||||||
visited_by VARCHAR(128) NULL COMMENT '最近一次回访操作人ID',
|
|
||||||
visited_at DATETIME(3) NULL COMMENT '最近一次回访时间',
|
|
||||||
visit_count INT NOT NULL DEFAULT 0 COMMENT '累计保存回访次数',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
PRIMARY KEY(id),
|
|
||||||
UNIQUE KEY uk_ll_customer_visit_delivery(customer_id,delivery_order_id),
|
|
||||||
KEY idx_ll_customer_visit_list(customer_id,status,delivery_completed_at),
|
|
||||||
KEY idx_ll_customer_visit_delivery_no(customer_id,delivery_no)
|
|
||||||
) ENGINE=InnoDB COMMENT='派送单维度客户回访记录';
|
|
||||||
|
|
||||||
|
|
||||||
INSERT INTO baiyue_iam.iam_permission
|
|
||||||
(id,tenant_id,app_code,permission_type,parent_id,permission_code,permission_name,route_path,http_method,sort_no,status,built_in,deleted)
|
|
||||||
VALUES
|
|
||||||
(5107070,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:exception-order:list','海外异常订单','/overseas-warehouse/exception-orders','GET',770,'ACTIVE',1,0),
|
|
||||||
(5107071,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:exception-order:create','新增海外异常订单',NULL,'POST',771,'ACTIVE',1,0),
|
|
||||||
(5107072,0,'PLATFORM','BUTTON',5107070,'platform:overseas-warehouse:delivery:return','派送单标记退货',NULL,'POST',772,'ACTIVE',1,0)
|
|
||||||
,(5107080,0,'PLATFORM','MENU',0,'platform:overseas-warehouse:customer-visit:list','客户回访','/overseas-warehouse/customer-visits','GET',780,'ACTIVE',1,0)
|
|
||||||
,(5107081,0,'PLATFORM','BUTTON',5107080,'platform:overseas-warehouse:customer-visit:edit','保存客户回访',NULL,'PUT',781,'ACTIVE',1,0)
|
|
||||||
,(5107082,0,'PLATFORM','BUTTON',5107080,'platform:overseas-warehouse:delivery:status','编辑派送单状态',NULL,'PUT',782,'ACTIVE',1,0)
|
|
||||||
ON DUPLICATE KEY UPDATE permission_name=VALUES(permission_name),route_path=VALUES(route_path),http_method=VALUES(http_method),sort_no=VALUES(sort_no),status='ACTIVE',built_in=1,deleted=0;
|
|
||||||
INSERT IGNORE INTO baiyue_iam.iam_role_permission (tenant_id,role_id,permission_id,created_at)
|
|
||||||
SELECT 0,role.id,permission.id,NOW(3) FROM baiyue_iam.iam_role role
|
|
||||||
JOIN baiyue_iam.iam_permission permission ON permission.id BETWEEN 5107070 AND 5107082 AND permission.tenant_id=0
|
|
||||||
WHERE role.tenant_id=0 AND role.app_code='PLATFORM' AND role.role_code='PLATFORM_ADMIN' AND role.status='ACTIVE' AND role.deleted=0;
|
|
||||||
|
|
||||||
-- IAM 客户提交即成功,默认尺寸通过持久化任务后台初始化;已有客户按历史事实标记 READY。
|
-- IAM 客户提交即成功,默认尺寸通过持久化任务后台初始化;已有客户按历史事实标记 READY。
|
||||||
USE baiyue_iam;
|
USE baiyue_iam;
|
||||||
SET @customer_dimension_status_ddl = (SELECT IF(
|
SET @customer_dimension_status_ddl = (SELECT IF(
|
||||||
@@ -16732,15 +16505,68 @@ CREATE TABLE IF NOT EXISTS iam_customer_dimension_initialization_task (
|
|||||||
KEY idx_customer_dimension_task_diagnosis (status,updated_at)
|
KEY idx_customer_dimension_task_diagnosis (status,updated_at)
|
||||||
) ENGINE=InnoDB COMMENT='IAM客户默认尺寸可靠后台初始化任务及诊断状态';
|
) ENGINE=InnoDB COMMENT='IAM客户默认尺寸可靠后台初始化任务及诊断状态';
|
||||||
|
|
||||||
-- 兼容已存在的旧表:CREATE TABLE IF NOT EXISTS 不会为旧表补充新字段。
|
USE baiyue_firstleg;
|
||||||
SET @customer_dimension_lease_ddl = (SELECT IF(
|
ALTER TABLE fl_loading_pick_task MODIFY COLUMN task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING'
|
||||||
EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
COMMENT 'PDA任务状态:LOADING、COMPLETING、COMPLETION_FAILED、COMPLETED';
|
||||||
AND table_name='iam_customer_dimension_initialization_task' AND column_name='lease_until'),
|
ALTER TABLE fl_loading_pick_task DROP CHECK chk_loading_pick_task_status;
|
||||||
'SELECT 1',
|
ALTER TABLE fl_loading_pick_task ADD CONSTRAINT chk_loading_pick_task_status
|
||||||
'ALTER TABLE iam_customer_dimension_initialization_task ADD COLUMN lease_until DATETIME(3) NULL COMMENT ''当前任务租约截止时间'' AFTER lease_owner'));
|
CHECK (task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED','COMPLETED'));
|
||||||
PREPARE customer_dimension_lease_stmt FROM @customer_dimension_lease_ddl;
|
|
||||||
EXECUTE customer_dimension_lease_stmt;
|
CREATE TABLE IF NOT EXISTS fl_loading_completion_command (
|
||||||
DEALLOCATE PREPARE customer_dimension_lease_stmt;
|
id BIGINT UNSIGNED NOT NULL COMMENT '可靠装柜完成命令ID',
|
||||||
|
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT '关联PDA任务ID',
|
||||||
|
operation_id VARCHAR(128) NOT NULL COMMENT '客户端全局幂等操作ID',
|
||||||
|
request_hash CHAR(64) NOT NULL COMMENT '规范化请求摘要',
|
||||||
|
command_status VARCHAR(16) NOT NULL COMMENT '命令状态',
|
||||||
|
expected_task_version INT UNSIGNED NOT NULL COMMENT '受理时客户端任务版本',
|
||||||
|
remark VARCHAR(500) NULL COMMENT '装柜完成备注',
|
||||||
|
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||||
|
operator_name VARCHAR(64) NULL COMMENT '操作人名称快照',
|
||||||
|
attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '后台认领次数',
|
||||||
|
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次允许处理时间',
|
||||||
|
lease_owner VARCHAR(128) NULL COMMENT '后台租约持有者',
|
||||||
|
lease_until DATETIME(3) NULL COMMENT '后台租约截止时间',
|
||||||
|
failure_stage VARCHAR(40) NULL COMMENT '脱敏失败阶段',
|
||||||
|
failure_code VARCHAR(32) NULL COMMENT '业务或下游错误码',
|
||||||
|
failure_request_id VARCHAR(64) NULL COMMENT '下游请求ID',
|
||||||
|
failure_trace_id VARCHAR(64) NULL COMMENT '下游链路ID',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '命令创建时间',
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '命令更新时间',
|
||||||
|
completed_at DATETIME(3) NULL COMMENT '命令完成时间',
|
||||||
|
PRIMARY KEY(id),UNIQUE KEY uk_loading_completion_operation(operation_id),
|
||||||
|
KEY idx_loading_completion_dispatch(command_status,next_retry_at,lease_until),
|
||||||
|
CONSTRAINT fk_loading_completion_task FOREIGN KEY(pick_task_id) REFERENCES fl_loading_pick_task(id),
|
||||||
|
CONSTRAINT chk_loading_completion_status CHECK(command_status IN ('PROCESSING','RETRYABLE','FAILED','COMPLETED'))
|
||||||
|
) ENGINE=InnoDB COMMENT='PDA装柜完成可靠异步命令';
|
||||||
|
CREATE TABLE IF NOT EXISTS fl_loading_completion_checkpoint (
|
||||||
|
command_id BIGINT UNSIGNED NOT NULL COMMENT '可靠装柜完成命令ID',
|
||||||
|
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '入库单客户ID',
|
||||||
|
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '入库单ID',
|
||||||
|
owner_sequence INT UNSIGNED NOT NULL COMMENT '稳定处理顺序',
|
||||||
|
checkpoint_stage VARCHAR(24) NOT NULL COMMENT '入库单处理检查点',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '检查点创建时间',
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '检查点更新时间',
|
||||||
|
PRIMARY KEY(command_id,tenant_id,inbound_id),KEY idx_loading_completion_owner(command_id,owner_sequence),
|
||||||
|
CONSTRAINT fk_loading_completion_checkpoint_command FOREIGN KEY(command_id) REFERENCES fl_loading_completion_command(id),
|
||||||
|
CONSTRAINT chk_loading_completion_checkpoint CHECK(checkpoint_stage IN ('READY','SETTLED','INBOUND_COMPLETED'))
|
||||||
|
) ENGINE=InnoDB COMMENT='PDA装柜完成按入库单恢复检查点';
|
||||||
|
|
||||||
|
DROP TRIGGER IF EXISTS trg_loading_pick_task_before_update;
|
||||||
|
DELIMITER $$
|
||||||
|
CREATE TRIGGER trg_loading_pick_task_before_update BEFORE UPDATE ON fl_loading_pick_task FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF OLD.task_status='COMPLETED' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='已完成PDA拣货任务不可修改'; END IF;
|
||||||
|
IF NEW.loading_plan_id<>OLD.loading_plan_id OR NOT(NEW.container_booking_id<=>OLD.container_booking_id)
|
||||||
|
OR NOT(NEW.cabinet_no<=>OLD.cabinet_no) THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PDA拣货任务稳定归属不可修改';
|
||||||
|
END IF;
|
||||||
|
IF NOT ((OLD.task_status='LOADING' AND NEW.task_status IN ('LOADING','COMPLETING'))
|
||||||
|
OR (OLD.task_status='COMPLETING' AND NEW.task_status IN ('COMPLETING','COMPLETION_FAILED','COMPLETED'))
|
||||||
|
OR (OLD.task_status='COMPLETION_FAILED' AND NEW.task_status='COMPLETING')) THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PDA拣货任务状态流转非法';
|
||||||
|
END IF;
|
||||||
|
END$$
|
||||||
|
DELIMITER ;
|
||||||
|
|
||||||
CALL baiyue_firstleg.assert_unified_final_schema();
|
CALL baiyue_firstleg.assert_unified_final_schema();
|
||||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
|
|||||||
purchase_quantity BIGINT NOT NULL COMMENT '采购数量', budget_unit_price DECIMAL(18,2) NULL COMMENT '预算单价',
|
purchase_quantity BIGINT NOT NULL COMMENT '采购数量', budget_unit_price DECIMAL(18,2) NULL COMMENT '预算单价',
|
||||||
currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', receiving_address VARCHAR(1000) NULL COMMENT '收货地址',
|
currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种', receiving_address VARCHAR(1000) NULL COMMENT '收货地址',
|
||||||
contact_name VARCHAR(128) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(64) NOT NULL COMMENT '联系人电话',
|
contact_name VARCHAR(128) NOT NULL COMMENT '联系人姓名', contact_phone VARCHAR(64) NOT NULL COMMENT '联系人电话',
|
||||||
purchase_warehouse_id VARCHAR(128) NULL COMMENT '采购入仓仓库ID', purchase_warehouse_name VARCHAR(255) NULL COMMENT '采购入仓仓库名称', outbound_details JSON NULL COMMENT '出库明细',
|
purchase_warehouse_id VARCHAR(128) NULL COMMENT '采购入仓仓库ID', purchase_warehouse_name VARCHAR(255) NULL COMMENT '采购入仓仓库名称',
|
||||||
inbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '采购单据累计入库数量', domestic_carrier VARCHAR(128) NULL COMMENT '境内承运商',
|
inbound_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '采购单据累计入库数量', domestic_carrier VARCHAR(128) NULL COMMENT '境内承运商',
|
||||||
domestic_tracking_no VARCHAR(128) NULL COMMENT '境内物流单号', overseas_carrier VARCHAR(128) NULL COMMENT '海外承运商',
|
domestic_tracking_no VARCHAR(128) NULL COMMENT '境内物流单号', overseas_carrier VARCHAR(128) NULL COMMENT '海外承运商',
|
||||||
overseas_tracking_no VARCHAR(128) NULL COMMENT '海外物流单号', estimated_arrival_at DATETIME(3) NULL COMMENT '预计到达时间',
|
overseas_tracking_no VARCHAR(128) NULL COMMENT '海外物流单号', estimated_arrival_at DATETIME(3) NULL COMMENT '预计到达时间',
|
||||||
@@ -211,8 +211,6 @@ CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
|
|||||||
KEY idx_ll_overseas_procurement_list(customer_id,status,created_at),
|
KEY idx_ll_overseas_procurement_list(customer_id,status,created_at),
|
||||||
KEY idx_ll_overseas_procurement_warehouse(customer_id,purchase_warehouse_id,created_at)
|
KEY idx_ll_overseas_procurement_warehouse(customer_id,purchase_warehouse_id,created_at)
|
||||||
) ENGINE=InnoDB COMMENT='海外采购订单';
|
) ENGINE=InnoDB COMMENT='海外采购订单';
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_overseas_procurement_order' AND column_name='outbound_details'), 'SELECT 1', 'ALTER TABLE ll_overseas_procurement_order ADD COLUMN outbound_details JSON NULL COMMENT ''出库明细'' AFTER inbound_quantity'));
|
|
||||||
PREPARE procurement_outbound_stmt FROM @ddl; EXECUTE procurement_outbound_stmt; DEALLOCATE PREPARE procurement_outbound_stmt;
|
|
||||||
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (
|
SET @ddl = (SELECT IF(EXISTS (
|
||||||
SELECT 1 FROM information_schema.columns
|
SELECT 1 FROM information_schema.columns
|
||||||
@@ -500,6 +498,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
|||||||
'ALTER TABLE ll_order ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码'' AFTER order_type'));
|
'ALTER TABLE ll_order ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA按箱数接收、OVERSEAS逐箱扫码'' AFTER order_type'));
|
||||||
PREPARE phase4_destination_type_stmt FROM @ddl; EXECUTE phase4_destination_type_stmt; DEALLOCATE PREPARE phase4_destination_type_stmt;
|
PREPARE phase4_destination_type_stmt FROM @ddl; EXECUTE phase4_destination_type_stmt; DEALLOCATE PREPARE phase4_destination_type_stmt;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS ll_order_no_daily_sequence (
|
||||||
|
business_date DATE NOT NULL COMMENT '业务日期(Asia/Shanghai)',
|
||||||
|
last_sequence INT NOT NULL COMMENT '当日已分配尾程单流水号',
|
||||||
|
PRIMARY KEY(business_date)
|
||||||
|
) ENGINE=InnoDB COMMENT='尾程单号按日原子流水';
|
||||||
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_line' AND column_name='customer_id'),
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan_line' AND column_name='customer_id'),
|
||||||
'SELECT 1',
|
'SELECT 1',
|
||||||
'ALTER TABLE ll_inbound_plan_line ADD COLUMN customer_id VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''客户账号标识'' AFTER id'));
|
'ALTER TABLE ll_inbound_plan_line ADD COLUMN customer_id VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''客户账号标识'' AFTER id'));
|
||||||
@@ -1035,42 +1039,7 @@ DELIMITER ;
|
|||||||
CALL assert_phase4_pda_operations_ready();
|
CALL assert_phase4_pda_operations_ready();
|
||||||
DROP PROCEDURE assert_phase4_pda_operations_ready;
|
DROP PROCEDURE assert_phase4_pda_operations_ready;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_order_fee (
|
|
||||||
id BIGINT NOT NULL COMMENT '费用明细主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
|
|
||||||
order_id BIGINT NOT NULL COMMENT '尾程订单ID', fee_item VARCHAR(128) NOT NULL COMMENT '费用项目',
|
|
||||||
currency VARCHAR(3) NOT NULL COMMENT '币种代码', amount DECIMAL(18,2) NOT NULL COMMENT '费用金额',
|
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'PENDING_REVIEW' COMMENT '费用状态:PENDING_REVIEW待审核、APPROVED审核通过、REJECTED已驳回',
|
|
||||||
supplier_description VARCHAR(500) NULL COMMENT '供应商或费用说明', created_by VARCHAR(128) NOT NULL COMMENT '创建操作人',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
PRIMARY KEY(id), KEY idx_ll_order_fee_customer_order(customer_id,order_id,created_at)
|
|
||||||
) ENGINE=InnoDB COMMENT='尾程订单费用明细';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_order_operation_log (
|
|
||||||
id BIGINT NOT NULL COMMENT '操作记录主键', customer_id VARCHAR(128) NOT NULL COMMENT '客户数据隔离键',
|
|
||||||
order_id BIGINT NOT NULL COMMENT '尾程订单ID', operation_node VARCHAR(64) NOT NULL COMMENT '操作节点',
|
|
||||||
operator_name VARCHAR(128) NOT NULL COMMENT '操作人名称', result VARCHAR(500) NOT NULL COMMENT '处理结果',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '操作时间',
|
|
||||||
PRIMARY KEY(id), KEY idx_ll_order_operation_customer_order(customer_id,order_id,created_at)
|
|
||||||
) ENGINE=InnoDB COMMENT='尾程订单操作记录';
|
|
||||||
|
|
||||||
USE baiyue_lastleg;
|
USE baiyue_lastleg;
|
||||||
SET @ddl = (SELECT IF(EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema=DATABASE() AND table_name='ll_inbound_plan' AND column_name='version'
|
|
||||||
), 'SELECT 1', 'ALTER TABLE ll_inbound_plan ADD COLUMN version INT NOT NULL DEFAULT 0 COMMENT ''入库计划乐观锁版本'' AFTER updated_at'));
|
|
||||||
PREPARE ll_inbound_plan_version_ddl FROM @ddl;
|
|
||||||
EXECUTE ll_inbound_plan_version_ddl;
|
|
||||||
DEALLOCATE PREPARE ll_inbound_plan_version_ddl;
|
|
||||||
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (
|
|
||||||
SELECT 1 FROM information_schema.columns
|
|
||||||
WHERE table_schema=DATABASE() AND table_name='ll_order' AND column_name='pallet_merge_batch_id'
|
|
||||||
), 'SELECT 1', 'ALTER TABLE ll_order ADD COLUMN pallet_merge_batch_id VARCHAR(64) NULL COMMENT ''平台预标记合托分组号'' AFTER pallet_merge_flag'));
|
|
||||||
PREPARE ll_order_merge_batch_ddl FROM @ddl;
|
|
||||||
EXECUTE ll_order_merge_batch_ddl;
|
|
||||||
DEALLOCATE PREPARE ll_order_merge_batch_ddl;
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS ll_security_event_inbox (
|
CREATE TABLE IF NOT EXISTS ll_security_event_inbox (
|
||||||
event_id VARCHAR(64) NOT NULL COMMENT '跨区安全事件ID', event_type VARCHAR(96) NOT NULL COMMENT 'IAM安全事件类型',
|
event_id VARCHAR(64) NOT NULL COMMENT '跨区安全事件ID', event_type VARCHAR(96) NOT NULL COMMENT 'IAM安全事件类型',
|
||||||
payload_hash CHAR(64) NOT NULL COMMENT '规范化载荷SHA-256哈希', payload_json JSON NOT NULL COMMENT '冻结事件载荷JSON',
|
payload_hash CHAR(64) NOT NULL COMMENT '规范化载荷SHA-256哈希', payload_json JSON NOT NULL COMMENT '冻结事件载荷JSON',
|
||||||
|
|||||||
+53
-49
@@ -1,62 +1,66 @@
|
|||||||
# Nacos 环境切换说明
|
# 数据库环境切换说明
|
||||||
|
|
||||||
数据库、Redis 和服务地址均由所选环境的 Nacos 配置决定。本地启动时只切换
|
## 环境定义
|
||||||
Nacos 地址与 namespace,不再通过 `application-test.yml` 单独切换数据库。
|
|
||||||
|
|
||||||
## 环境映射
|
- `dev`:本地开发环境,连接 `127.0.0.1:3306`,默认账号为 `root/123456`。
|
||||||
|
- `test`:联调测试环境,连接 `8.163.30.128:3306`,各微服务仍使用独立数据库。
|
||||||
|
|
||||||
| Maven Profile | Nacos 地址 | Namespace |
|
所有服务默认使用 `dev`:
|
||||||
| --- | --- | --- |
|
|
||||||
| `local` | `127.0.0.1:8848` | `public` |
|
|
||||||
| `dev` | `dev-nacos.buggoglobal.com:8848` | `DEV_NACOS_NAMESPACE` |
|
|
||||||
| `test` | `8.163.30.128:8848` | `d99d7998-5bb0-4ffe-a2a0-1a564ce14463` |
|
|
||||||
|
|
||||||
本地 Nacos 使用 `nacos/nacos`。开发和测试密码不写入 Git,需在启动 IDEA
|
```yaml
|
||||||
之前设置一次用户环境变量:
|
spring:
|
||||||
|
profiles:
|
||||||
```powershell
|
active: ${SPRING_PROFILES_ACTIVE:dev}
|
||||||
[Environment]::SetEnvironmentVariable('DEV_NACOS_PASSWORD', '<开发 Nacos 密码>', 'User')
|
|
||||||
[Environment]::SetEnvironmentVariable('TEST_NACOS_PASSWORD', '<测试 Nacos 密码>', 'User')
|
|
||||||
[Environment]::SetEnvironmentVariable('DEV_NACOS_NAMESPACE', '<开发海外 namespace>', 'User')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
设置后完全退出并重新打开 IDEA,使 Maven 能读取新环境变量。
|
建议不要直接修改配置文件。通过 IDEA 的启动配置设置 `Active profiles` 即可切换:
|
||||||
|
|
||||||
## IDEA 中切换
|
- 本地数据库:`dev`
|
||||||
|
- 测试数据库:`test`
|
||||||
|
|
||||||
1. 在 Maven 面板的 `Profiles` 中,`local`、`dev`、`test` 只勾选一个。
|
也可以使用启动参数:
|
||||||
2. 重新加载 Maven 项目。
|
|
||||||
3. 在目标服务的 Maven `Plugins > spring-boot > spring-boot:run` 启动。
|
|
||||||
|
|
||||||
例如海外查询链路需要分别启动 `gateway-overseas`、`lastleg-service` 和
|
```text
|
||||||
`warehouse-overseas-service`。已经运行的进程不会自动切换环境,切换 Profile
|
--spring.profiles.active=test
|
||||||
后必须停止并重新启动。
|
|
||||||
|
|
||||||
IDEA 普通绿色 Run 按钮不执行 Maven,因此不会读取 Maven Profile。需要继续使用
|
|
||||||
绿色 Run 时,请在 Run Configuration 的 Environment variables 中设置
|
|
||||||
`NACOS_SERVER_ADDR`、`NACOS_NAMESPACE`、`NACOS_USERNAME` 和 `NACOS_PASSWORD`。
|
|
||||||
`gateway-overseas` 已默认启用 `overseas` Spring Profile。
|
|
||||||
|
|
||||||
## PowerShell 中切换
|
|
||||||
|
|
||||||
也可以在当前终端加载同一套变量,然后从同一终端启动服务:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
. .\scripts\use-nacos-environment.ps1 -Environment local -Region overseas
|
|
||||||
. .\scripts\use-nacos-environment.ps1 -Environment dev -Region overseas
|
|
||||||
. .\scripts\use-nacos-environment.ps1 -Environment test -Region overseas
|
|
||||||
```
|
```
|
||||||
|
|
||||||
脚本读取 `DEV_NACOS_PASSWORD`、`TEST_NACOS_PASSWORD`,namespace 优先读取
|
## 测试数据库密码
|
||||||
`*_OVERSEAS_NACOS_NAMESPACE`,未设置时读取 `*_NACOS_NAMESPACE`。命令前面的点和
|
|
||||||
空格不能省略,否则变量只存在于脚本子进程中。
|
|
||||||
|
|
||||||
## 当前限制
|
测试数据库密码不写入 Git。启动 `test` 环境前,在 IDEA 启动配置的环境变量中设置:
|
||||||
|
|
||||||
- 测试 Nacos 的 `8848` 客户端端口已确认可连接。
|
```text
|
||||||
- 开发 Nacos 网页控制台可访问,但 `dev-nacos.buggoglobal.com:8848` 当前从本机
|
TEST_DB_PASSWORD=测试数据库密码
|
||||||
无法连接。需要开放 Nacos 客户端端口以及 Nacos 2.x/3.x 所需的 gRPC 端口,
|
```
|
||||||
或提供一个客户端可访问的内部地址。
|
|
||||||
- 国内和海外存在同名 Data ID,远程环境必须使用各自独立 namespace。开发海外
|
公共测试连接参数支持覆盖:
|
||||||
namespace 尚未确认,因此通过 `DEV_NACOS_NAMESPACE` 在本机配置。
|
|
||||||
- 不要同时勾选多个环境 Profile;Maven 对同名属性的最终取值不直观,容易连错环境。
|
```text
|
||||||
|
TEST_DB_HOST=8.163.30.128
|
||||||
|
TEST_DB_PORT=3306
|
||||||
|
TEST_DB_USERNAME=root
|
||||||
|
TEST_DB_PASSWORD=测试数据库密码
|
||||||
|
```
|
||||||
|
|
||||||
|
每个服务还支持使用自己的 `*_TEST_DB_URL`、`*_TEST_DB_USERNAME` 和 `*_TEST_DB_PASSWORD` 覆盖公共参数。
|
||||||
|
|
||||||
|
## 分库映射
|
||||||
|
|
||||||
|
| 服务 | 测试数据库 |
|
||||||
|
| --- | --- |
|
||||||
|
| 认证服务 | `baiyue_auth` |
|
||||||
|
| 权限服务 | `baiyue_iam` |
|
||||||
|
| 订单服务 | `baiyue_order` |
|
||||||
|
| 仓库服务 | `baiyue_warehouse` |
|
||||||
|
| 头程履约服务 | `baiyue_firstleg` |
|
||||||
|
| 文件服务 | `baiyue_file` |
|
||||||
|
| 审计服务 | `baiyue_audit` |
|
||||||
|
| 事件桥接服务 | `baiyue_event_bridge` |
|
||||||
|
|
||||||
|
网关、打印和集成服务没有业务数据库,但同样支持 `dev/test` Profile。网关在两种环境下都会自动加载 `cn` 路由配置。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- `test` 只切换数据库,Nacos 和 Redis 默认仍连接本机。
|
||||||
|
- 环境变量优先级高于配置文件,可用于临时覆盖单个服务。
|
||||||
|
- 修改 IDEA 环境变量后需要重启对应服务。
|
||||||
|
- 不要把测试数据库密码提交到配置文件、启动脚本或 Git。
|
||||||
|
|||||||
-1
@@ -30,7 +30,6 @@ public class EventBridgeMetrics {
|
|||||||
EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,
|
EventTypes.FIRST_LEG_LOADING_PLAN_COMPLETED,
|
||||||
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED,
|
EventTypes.FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED,
|
||||||
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
EventTypes.OVERSEAS_CONTAINER_UNLOADED,
|
||||||
EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED,
|
|
||||||
EventTypes.IAM_USER_STATUS_CHANGED,
|
EventTypes.IAM_USER_STATUS_CHANGED,
|
||||||
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
EventTypes.IAM_TENANT_STATUS_CHANGED,
|
||||||
EventTypes.IAM_PERMISSION_VERSION_CHANGED,
|
EventTypes.IAM_PERMISSION_VERSION_CHANGED,
|
||||||
|
|||||||
+1
-5
@@ -31,14 +31,10 @@ public class EventInboxService {
|
|||||||
@Value("${baiyue.region:CN}")
|
@Value("${baiyue.region:CN}")
|
||||||
private String localRegion;
|
private String localRegion;
|
||||||
|
|
||||||
/** Local single-bridge mode may carry both regional envelopes; production keeps strict routing. */
|
|
||||||
@Value("${baiyue.event-bridge.accept-any-region:false}")
|
|
||||||
private boolean acceptAnyRegion;
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public EventReceiveResult receive(EventEnvelope envelope) {
|
public EventReceiveResult receive(EventEnvelope envelope) {
|
||||||
envelope.requireValid();
|
envelope.requireValid();
|
||||||
if (!acceptAnyRegion && !envelope.targetRegion().name().equalsIgnoreCase(localRegion)) {
|
if (!envelope.targetRegion().name().equalsIgnoreCase(localRegion)) {
|
||||||
throw new IllegalArgumentException("事件目标区域与当前事件桥区域不一致");
|
throw new IllegalArgumentException("事件目标区域与当前事件桥区域不一致");
|
||||||
}
|
}
|
||||||
if (!EventPayloadHash.matches(
|
if (!EventPayloadHash.matches(
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ spring:
|
|||||||
namespace: ${NACOS_NAMESPACE:}
|
namespace: ${NACOS_NAMESPACE:}
|
||||||
|
|
||||||
baiyue:
|
baiyue:
|
||||||
region: ${REGION_CODE:CN}
|
region: ${REGION_CODE:OVERSEAS}
|
||||||
event-dispatcher:
|
event-dispatcher:
|
||||||
consumer-url: ${EVENT_CONSUMER_URL:${LASTLEG_CONSUMER_URL:http://127.0.0.1:9303/internal/lastleg/events}}
|
consumer-url: ${EVENT_CONSUMER_URL:${LASTLEG_CONSUMER_URL:http://127.0.0.1:9303/internal/lastleg/events}}
|
||||||
firstleg-consumer-url: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:http://127.0.0.1:9202/internal/firstleg/v1/container-events}
|
firstleg-consumer-url: ${FIRSTLEG_CONTAINER_EVENT_CONSUMER_URL:http://127.0.0.1:9202/internal/firstleg/v1/container-events}
|
||||||
|
|||||||
-14
@@ -125,15 +125,6 @@ class EventInboxServiceTest {
|
|||||||
.hasMessageContaining("目标区域");
|
.hasMessageContaining("目标区域");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void 本地单实例模式允许接收不同目标区域事件() {
|
|
||||||
EventInboxService localService = service("CN", true);
|
|
||||||
EventEnvelope envelope = withHash(envelope("sha256:placeholder"));
|
|
||||||
|
|
||||||
assertThat(localService.receive(envelope).result()).isEqualTo("已接收");
|
|
||||||
verify(store).insert(any(BridgeInbox.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventEnvelope envelope(String hash) {
|
private EventEnvelope envelope(String hash) {
|
||||||
return new EventEnvelope("550e8400-e29b-41d4-a716-446655440000", "OrderApproved", 1,
|
return new EventEnvelope("550e8400-e29b-41d4-a716-446655440000", "OrderApproved", 1,
|
||||||
Instant.parse("2026-07-29T08:30:00Z"), RegionCode.CN, RegionCode.OVERSEAS,
|
Instant.parse("2026-07-29T08:30:00Z"), RegionCode.CN, RegionCode.OVERSEAS,
|
||||||
@@ -142,16 +133,11 @@ class EventInboxServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private EventInboxService service(String region) {
|
private EventInboxService service(String region) {
|
||||||
return service(region, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventInboxService service(String region, boolean acceptAnyRegion) {
|
|
||||||
EventInboxService service = new EventInboxService();
|
EventInboxService service = new EventInboxService();
|
||||||
ReflectionTestUtils.setField(service, "inboxStore", store);
|
ReflectionTestUtils.setField(service, "inboxStore", store);
|
||||||
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
ReflectionTestUtils.setField(service, "objectMapper", objectMapper);
|
||||||
ReflectionTestUtils.setField(service, "metrics", metrics);
|
ReflectionTestUtils.setField(service, "metrics", metrics);
|
||||||
ReflectionTestUtils.setField(service, "localRegion", region);
|
ReflectionTestUtils.setField(service, "localRegion", region);
|
||||||
ReflectionTestUtils.setField(service, "acceptAnyRegion", acceptAnyRegion);
|
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ public class InternalBusinessFileService {
|
|||||||
"CONTAINER_DELIVERY_ORDER",
|
"CONTAINER_DELIVERY_ORDER",
|
||||||
"CONTAINER_PICKUP_ORDER",
|
"CONTAINER_PICKUP_ORDER",
|
||||||
"OVERSEAS_UNLOAD_PHOTO",
|
"OVERSEAS_UNLOAD_PHOTO",
|
||||||
"OVERSEAS_RELABEL_PHOTO",
|
"OVERSEAS_RELABEL_PHOTO");
|
||||||
"OVERSEAS_CUSTOMER_VISIT");
|
|
||||||
|
|
||||||
@Resource private FileObjectMapper files;
|
@Resource private FileObjectMapper files;
|
||||||
@Resource private BusinessFileStorage storage;
|
@Resource private BusinessFileStorage storage;
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ public class PlatformFinanceController {
|
|||||||
@GetMapping("/fee-pool")
|
@GetMapping("/fee-pool")
|
||||||
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||||
@Operation(summary = "客户应收应付费用池")
|
@Operation(summary = "客户应收应付费用池")
|
||||||
|
public ApiResponse<?> feePool(
|
||||||
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
|
@RequestParam(required = false) Long customerId,
|
||||||
|
@RequestParam(required = false) String direction,
|
||||||
|
@RequestParam(required = false) Long supplierId) {
|
||||||
|
return FinanceResponses.success(queryService.feePool(customerId, direction, supplierId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
|
||||||
public ApiResponse<?> feePool(
|
public ApiResponse<?> feePool(
|
||||||
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
@Parameter(description = "可选客户筛选;省略时查询全部客户")
|
||||||
@RequestParam(required = false) Long customerId,
|
@RequestParam(required = false) Long customerId,
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ public class FinancePhase1OperationService {
|
|||||||
throw error("FIN-REQ-400", "开始日期不能晚于结束日期");
|
throw error("FIN-REQ-400", "开始日期不能晚于结束日期");
|
||||||
}
|
}
|
||||||
String normalizedDirection = normalized(direction, List.of("RECEIVABLE", "PAYABLE"), "费用方向无效");
|
String normalizedDirection = normalized(direction, List.of("RECEIVABLE", "PAYABLE"), "费用方向无效");
|
||||||
String normalizedReview = normalized(reviewStatus, List.of("PENDING", "APPROVED", "REJECTED"), "审核状态无效");
|
String normalizedReview = normalized(reviewStatus,
|
||||||
|
List.of("DRAFT", "PENDING", "APPROVED", "REJECTED"), "审核状态无效");
|
||||||
String normalizedSourceStatus = normalized(sourceStatus,
|
String normalizedSourceStatus = normalized(sourceStatus,
|
||||||
List.of("AVAILABLE", "LOCKED", "BILLED", "VOIDED", "SUPERSEDED"), "费用来源状态无效");
|
List.of("AVAILABLE", "LOCKED", "BILLED", "VOIDED", "SUPERSEDED"), "费用来源状态无效");
|
||||||
String generatedSql = "EXISTS(SELECT 1 FROM fin_bill_line bl WHERE bl.source_id=f.id AND bl.source_version=f.source_fee_version)";
|
String generatedSql = "EXISTS(SELECT 1 FROM fin_bill_line bl WHERE bl.source_id=f.id AND bl.source_version=f.source_fee_version)";
|
||||||
|
|||||||
@@ -99,7 +99,12 @@ public class FinanceQueryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<FeePoolItem> feePool(Long customerId, String direction) {
|
public List<FeePoolItem> feePool(Long customerId, String direction) {
|
||||||
|
return feePool(customerId, direction, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<FeePoolItem> feePool(Long customerId, String direction, Long supplierId) {
|
||||||
validateOptionalCustomer(customerId);
|
validateOptionalCustomer(customerId);
|
||||||
|
if (supplierId != null && supplierId <= 0) throw bad("供应商范围无效");
|
||||||
String normalized = direction == null || direction.isBlank() ? "ALL" : direction.trim().toUpperCase(java.util.Locale.ROOT);
|
String normalized = direction == null || direction.isBlank() ? "ALL" : direction.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
if (!List.of("ALL", "RECEIVABLE", "PAYABLE").contains(normalized)) throw bad("费用方向只能是RECEIVABLE或PAYABLE");
|
if (!List.of("ALL", "RECEIVABLE", "PAYABLE").contains(normalized)) throw bad("费用方向只能是RECEIVABLE或PAYABLE");
|
||||||
List<FeePoolItem> result = new ArrayList<>();
|
List<FeePoolItem> result = new ArrayList<>();
|
||||||
@@ -107,6 +112,14 @@ public class FinanceQueryService {
|
|||||||
List<Object> sourceArgs = new ArrayList<>();
|
List<Object> sourceArgs = new ArrayList<>();
|
||||||
if (customerId != null) { sourceSql += " AND customer_id=?"; sourceArgs.add(customerId); }
|
if (customerId != null) { sourceSql += " AND customer_id=?"; sourceArgs.add(customerId); }
|
||||||
if (!"ALL".equals(normalized)) { sourceSql += " AND direction=?"; sourceArgs.add(normalized); }
|
if (!"ALL".equals(normalized)) { sourceSql += " AND direction=?"; sourceArgs.add(normalized); }
|
||||||
|
if ("PAYABLE".equals(normalized)) {
|
||||||
|
sourceSql += " AND review_status='APPROVED' AND settlement_mode='ON_ACCOUNT' AND currency_code='CNY' AND amount>0";
|
||||||
|
sourceSql += " AND NOT EXISTS (SELECT 1 FROM fin_settlement_line sl JOIN fin_settlement s ON s.id=sl.settlement_id WHERE sl.fee_source_id=fin_fee_source.id AND s.status<>'VOIDED')";
|
||||||
|
if (supplierId != null) {
|
||||||
|
sourceSql += " AND CAST(JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.supplierId')) AS UNSIGNED)=?";
|
||||||
|
sourceArgs.add(supplierId);
|
||||||
|
}
|
||||||
|
}
|
||||||
sourceSql += " ORDER BY created_at DESC,id DESC";
|
sourceSql += " ORDER BY created_at DESC,id DESC";
|
||||||
result.addAll(jdbc.query(sourceSql, (rs, rowNum) -> {
|
result.addAll(jdbc.query(sourceSql, (rs, rowNum) -> {
|
||||||
String rowDirection = rs.getString("direction");
|
String rowDirection = rs.getString("direction");
|
||||||
|
|||||||
@@ -39,14 +39,35 @@ class FinanceFeePoolDirectionBehaviorTest {
|
|||||||
assertThat(jdbc.costQueryCount).isZero();
|
assertThat(jdbc.costQueryCount).isZero();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void payablePoolOnlyQueriesApprovedUnsettledSourcesForTheSelectedSupplier() {
|
||||||
|
DirectionJdbc jdbc = new DirectionJdbc();
|
||||||
|
FinanceQueryService service = new FinanceQueryService(jdbc, new ObjectMapper());
|
||||||
|
|
||||||
|
service.feePool(7L, "PAYABLE", 81L);
|
||||||
|
|
||||||
|
assertThat(jdbc.sourceSql).contains(
|
||||||
|
"review_status='APPROVED'",
|
||||||
|
"settlement_mode='ON_ACCOUNT'",
|
||||||
|
"currency_code='CNY'",
|
||||||
|
"NOT EXISTS (SELECT 1 FROM fin_settlement_line",
|
||||||
|
"s.status<>'VOIDED'",
|
||||||
|
"CAST(JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.supplierId')) AS UNSIGNED)=?");
|
||||||
|
assertThat(jdbc.sourceArgs).containsExactly(7L, "PAYABLE", 81L);
|
||||||
|
}
|
||||||
|
|
||||||
private static final class DirectionJdbc extends JdbcTemplate {
|
private static final class DirectionJdbc extends JdbcTemplate {
|
||||||
private final List<String> sourceDirections = new ArrayList<>();
|
private final List<String> sourceDirections = new ArrayList<>();
|
||||||
private int costQueryCount;
|
private int costQueryCount;
|
||||||
|
private String sourceSql;
|
||||||
|
private List<Object> sourceArgs = List.of();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T> List<T> query(String sql, RowMapper<T> mapper, Object... args) {
|
public <T> List<T> query(String sql, RowMapper<T> mapper, Object... args) {
|
||||||
if (sql.contains("FROM fin_fee_source")) {
|
if (sql.contains("FROM fin_fee_source")) {
|
||||||
|
sourceSql = sql;
|
||||||
|
sourceArgs = List.of(args);
|
||||||
String direction = String.valueOf(args[1]);
|
String direction = String.valueOf(args[1]);
|
||||||
sourceDirections.add(direction);
|
sourceDirections.add(direction);
|
||||||
return (List<T>) List.of(item(direction, "FIRSTLEG_FEE", 1L));
|
return (List<T>) List.of(item(direction, "FIRSTLEG_FEE", 1L));
|
||||||
|
|||||||
+1
-3
@@ -14,7 +14,7 @@
|
|||||||
| 履约任务/分配 | `controller/PlatformFulfillmentAssignmentController.java`、`service/FulfillmentTaskService.java` |
|
| 履约任务/分配 | `controller/PlatformFulfillmentAssignmentController.java`、`service/FulfillmentTaskService.java` |
|
||||||
| 入库单查询/聚合 | `controller/PlatformInboundOrderController.java`、`service/InboundOrderQueryService.java` |
|
| 入库单查询/聚合 | `controller/PlatformInboundOrderController.java`、`service/InboundOrderQueryService.java` |
|
||||||
| 平台测量/OCR | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`;手工单箱测量也从该 Controller 与 `CartonMeasurementService` 进入 |
|
| 平台测量/OCR | `controller/PlatformInboundMeasurementController.java`、`service/InboundMeasurementService.java`、`service/CartonMeasurementService.java`;手工单箱测量也从该 Controller 与 `CartonMeasurementService` 进入 |
|
||||||
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`。`fl_inbound_carton_label.sku_code` 是箱唛的 SKU 冻结快照,生成时经 `client/OrderCargoClient.java` 获取;既有记录按 `inbound_detail_id` 回填。`ONE_CLICK_DROPSHIP` 缺 SKU 时禁止生成箱唛。`box_code`、二维码内容和现有打印版式不携带 SKU,不能因该绑定改变。基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql`、`../deploy/sql/头程数据库统一增量-MySQL8.sql` |
|
| 箱唛箱码生成、打印与测量绑定 | 审核建单生成钩子 `controller/InternalInboundOrderController.java`;生成/解析 `service/InboundCartonLabelService.java`、`repository/InboundCartonLabelRepository.java`;平台与客户打印 `controller/PlatformInboundOrderController.java`、`MerchantInboundCartonLabelController.java`、`service/InboundCartonLabelPrintService.java`;基线/升级 SQL 为 `src/main/resources/db/phase2-inbound-carton-label.sql`、`phase2-carton-label-versioning-migration.sql` |
|
||||||
| 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` |
|
| 墨家设备测量上传 | `controller/OpenApiInboundMeasurementController.java`、`dto/MojiaMeasurementDtos.java`、`service/MojiaMeasurementOpenApiService.java`、`security/MeasurementRequestAuthenticator.java`;接口契约见 `src/main/resources/openapi/firstleg-v1.yaml` 与 `../../docs/墨家设备测量上传接口说明-V3.md` |
|
||||||
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
|
| 审核与初始计价 | `controller/PlatformInboundMeasurementReviewController.java`、`service/MeasurementReviewService.java`、`service/InitialInboundPricingService.java`、`dto/MeasurementApprovalResponse.java` |
|
||||||
| FBA 主数据/计费 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java` |
|
| FBA 主数据/计费 | `controller/PlatformFbaMasterDataController.java`、`PlatformFbaPricingController.java`、`service/FbaPricingService.java` |
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
|
| PDA 拣货与装柜 | `controller/PlatformLoadingPdaController.java`、`loading/controller/PlatformLoadingDeliveryController.java` |
|
||||||
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
| 装柜完成跨区事件 | `service/LoadingCompletedOutboxService.java`、`LoadingCompletedOutboxRelay.java`、`repository/LoadingCompletedOutboxRepository.java`、`mapper/LoadingCartonManifestMapper.java`、`client/EventBridgeClient.java`;从 `fl_inbound_order.destination_type` 冻结 V2 `orders[].destinationType=FBA|OVERSEAS`,OVERSEAS 按最终实装与稳定箱码冻结,FBA 可仅冻结箱数;真实生产验收 `src/test/java/com/baiyue/firstleg/service/TaillegRealProducerAcceptanceTest.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||||
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
| 海外卸柜完成事件回写 | 内部入口 `controller/InternalOverseasContainerEventController.java`、消费者 `service/OverseasContainerEventConsumer.java`、Inbox `entity/ContainerEventInboxEntity.java` 与 `mapper/ContainerEventInboxMapper.java`;事件模型见 `../common-event-contract/AGENTS.md` |
|
||||||
| 海外暂扣状态回写 | 同一内部事件入口、消费者 `service/OverseasInboundOrderDetainedConsumer.java`;按来源入仓单 ID 更新 `fl_inbound_order.hold_flag/current_hold_reason`,复用跨区 Inbox 并写 `fl_inbound_hold_record` |
|
|
||||||
| 三期海运订舱、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;关联候选入口 `/api/platform/v1/ocean-bookings/link-candidates`,订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
| 三期海运订舱、关联候选与出仓前船期资料 | `oceanbooking/controller/PlatformOceanBookingController.java`、`oceanbooking/service/OceanBookingService.java`、`oceanbooking/mapper/OceanBookingMapper.java`、`portschedule/controller/PlatformPortScheduleController.java`;关联候选入口 `/api/platform/v1/ocean-bookings/link-candidates`,订舱契约见 `src/main/resources/openapi/ocean-bookings-v1.yaml`,表结构见 `src/main/resources/db/phase3-ocean-booking-schema.sql` |
|
||||||
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
| 四期国内出仓后船节点定时查询 | `outbound/dispatch/service/OutboundDispatchService.java` 在确认出仓事务内幂等创建现有 `CarrierScheduleQuery`;编排与租约 `portschedule/service/CarrierScheduleQueryScheduler.java`,任务/十节点事实/异常事实/不可变 attempt `CarrierScheduleQueryService.java`,平台查询与人工补录入口 `PlatformCarrierScheduleQueryController.java` 的 `/{queryId}/manual-nodes`;停止只由实际 `EMPTY_RETURNED` 节点触发,不提供无节点停止入口;外部船司/轨迹调用经 `integration-service/`;标准见 `../../docs/四期/船节点标准化契约.md` |
|
||||||
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
| 船节点跨区交接事件 | `portschedule/service/CarrierScheduleNodeOutboxService.java`;实际节点落库后发布 `FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED` V1,复用 `LoadingCompletedOutboxRepository`/Relay 投 Event Bridge;海外消费见 `../lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java` |
|
||||||
@@ -53,7 +52,6 @@
|
|||||||
- 价格簿查询消费者依赖 `PriceBookQueryUseCase` 或稳定查询端口,不得反向依赖持久化 Mapper;移动旧查询类时同步更新生产调用、测试、Mock 与 import,避免同一职责在根 `repository/mapper` 和 `pricebookquery/` 双写。
|
- 价格簿查询消费者依赖 `PriceBookQueryUseCase` 或稳定查询端口,不得反向依赖持久化 Mapper;移动旧查询类时同步更新生产调用、测试、Mock 与 import,避免同一职责在根 `repository/mapper` 和 `pricebookquery/` 双写。
|
||||||
- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。
|
- 第三方回传必须签名、时间窗、防重放、请求哈希,并绑定订单货物和 File 证据;不得信任外部客户字段。
|
||||||
- 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。
|
- 稳定箱码映射以入库快照和 Order 货物明细为事实;订单审核创建入库单时必须幂等生成,打印接口只复用或补齐同一映射,用户是否下载箱唛不得决定箱码有效性。手工测量、墨家设备和 PDA 扫码统一通过 `InboundCartonLabelService` 校验客户、入库单、货物明细、总箱数/下标和有效批次,不得在测量时临时伪造箱码或接受跨客户、跨入库箱码。
|
||||||
- 一件代发以 `fl_inbound_carton_label.sku_code` 作为不可变 SKU 快照;PDA 仍只扫描稳定 `box_code`,不得将 SKU 塞入二维码或以 SKU 替代箱码。实际打托后的查询必须能按“现场托盘 -> 客户 -> SKU -> 箱数”聚合;历史箱唛仅能在同客户、同入库单范围内按 `inbound_detail_id` 回填,禁止按货名、箱规或箱号猜测 SKU。修改本链路时同步检查 `OrderCargoClient`、Order 内部货物接口及 PDA 打托实现。
|
|
||||||
- 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。
|
- 测量审核通过后直接进入 `WAIT_SPLIT`,费用仅作为独立初算和人工维护结果,不得阻塞或回退订单主流程;计费量、单价和附加费调整必须版本化并由服务端重算金额。状态或 DTO 变更时逐个检查 Order/Warehouse/File/Integration 消费者。
|
||||||
- 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。
|
- 报关、订舱和拖车引用的供应商必须通过 `WarehouseSupplierSnapshotService` 按 ID 向 Warehouse 校验:报关使用 `CUSTOMS`,订舱使用 `BOOKING`(不区分国内/国外 scope),拖车使用 `TRUCKING`;供应商必须为 `ACTIVE`。请求中的名称只能作为一致性提示,持久化名称必须来自 Warehouse 快照,禁止用自由文本绕过 ID、类别或启用状态校验。修改任一供应商引用 DTO、OpenAPI 或内部客户端时,必须同步检查三条业务链、Warehouse `supplier-v1.yaml`、数据库快照字段和对应契约测试。
|
||||||
- 订舱状态固定为 `DRAFT → RELEASED`:草稿允许资料不完整并可替换或删除,放舱时校验完整持久化聚合,放舱后永久只读。创建草稿和放舱都只维护订舱聚合、三类参与方及柜型需求,不创建真实 `containerNo`、货柜行或分配关系;真实货柜只在后续提柜关联时占用已放舱柜型需求。
|
- 订舱状态固定为 `DRAFT → RELEASED`:草稿允许资料不完整并可替换或删除,放舱时校验完整持久化聚合,放舱后永久只读。创建草稿和放舱都只维护订舱聚合、三类参与方及柜型需求,不创建真实 `containerNo`、货柜行或分配关系;真实货柜只在后续提柜关联时占用已放舱柜型需求。
|
||||||
|
|||||||
@@ -63,12 +63,7 @@ public interface OrderCargoClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
record CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
||||||
String skuNo, Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
||||||
BigDecimal heightCm) {
|
BigDecimal heightCm) {
|
||||||
public CargoLabelBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
|
|
||||||
Integer packageCount, BigDecimal lengthCm, BigDecimal widthCm,
|
|
||||||
BigDecimal heightCm) {
|
|
||||||
this(cargoId, orderId, itemNo, cargoName, null, packageCount, lengthCm, widthCm, heightCm);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-1
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.firstleg.client;
|
package com.baiyue.firstleg.client;
|
||||||
|
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -16,8 +17,17 @@ import java.util.List;
|
|||||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1")
|
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1")
|
||||||
public interface WarehousePalletLocationClient {
|
public interface WarehousePalletLocationClient {
|
||||||
@GetMapping("/pallets/snapshots")
|
@GetMapping("/pallets/snapshots")
|
||||||
ApiResponse<List<PalletSnapshot>> palletSnapshots(
|
ApiResponse<PageResult<PalletSnapshot>> palletSnapshots(
|
||||||
@RequestHeader("X-Internal-Service-Token") String token,
|
@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
@RequestParam("warehouseId") long warehouseId,
|
||||||
|
@RequestParam("page") long page,
|
||||||
|
@RequestParam("size") long size,
|
||||||
|
@RequestParam(value = "keyword", required = false) String keyword);
|
||||||
|
|
||||||
|
@GetMapping("/pallets/{palletId}/snapshot")
|
||||||
|
ApiResponse<PalletSnapshot> palletSnapshot(
|
||||||
|
@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
@org.springframework.web.bind.annotation.PathVariable("palletId") long palletId,
|
||||||
@RequestParam("warehouseId") long warehouseId);
|
@RequestParam("warehouseId") long warehouseId);
|
||||||
|
|
||||||
@PostMapping("/pallet-locations/reservations")
|
@PostMapping("/pallet-locations/reservations")
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.baiyue.firstleg.client;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehousePartnerClient",
|
||||||
|
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse-partners")
|
||||||
|
public interface WarehousePartnerClient {
|
||||||
|
@GetMapping("/{tenantId}/{partnerId}/active")
|
||||||
|
ApiResponse<Snapshot> active(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
@PathVariable long tenantId, @PathVariable long partnerId);
|
||||||
|
|
||||||
|
record Snapshot(long id, long tenantId, String partnerName, String partnerType,
|
||||||
|
String carrierCode, List<String> types, String status) {}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.RequestHeader;
|
import org.springframework.web.bind.annotation.RequestHeader;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseSupplierClient",
|
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseSupplierClient",
|
||||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/suppliers")
|
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/suppliers")
|
||||||
@@ -16,5 +17,11 @@ public interface WarehouseSupplierClient {
|
|||||||
@RequestParam(defaultValue = "true") boolean requireActive,
|
@RequestParam(defaultValue = "true") boolean requireActive,
|
||||||
@RequestParam(required = false) String category);
|
@RequestParam(required = false) String category);
|
||||||
|
|
||||||
record Snapshot(long supplierId, String name, String category, String status) {}
|
@GetMapping
|
||||||
|
ApiResponse<List<Snapshot>> candidates(@RequestHeader("X-Internal-Service-Token") String token,
|
||||||
|
@RequestParam String category,
|
||||||
|
@RequestParam String carrierCode);
|
||||||
|
|
||||||
|
record Snapshot(long supplierId, String name, String category, String status,
|
||||||
|
List<String> carrierCodes) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-7
@@ -4,9 +4,8 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.common.event.EventEnvelope;
|
import com.baiyue.common.event.EventEnvelope;
|
||||||
import com.baiyue.common.event.EventTypes;
|
import com.baiyue.firstleg.entity.ContainerBookingEntity;
|
||||||
import com.baiyue.firstleg.service.OverseasContainerEventConsumer;
|
import com.baiyue.firstleg.service.OverseasContainerEventConsumer;
|
||||||
import com.baiyue.firstleg.service.OverseasInboundOrderDetainedConsumer;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -21,17 +20,14 @@ import java.security.MessageDigest;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class InternalOverseasContainerEventController {
|
public class InternalOverseasContainerEventController {
|
||||||
private final OverseasContainerEventConsumer consumer;
|
private final OverseasContainerEventConsumer consumer;
|
||||||
private final OverseasInboundOrderDetainedConsumer detainedConsumer;
|
|
||||||
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
|
||||||
|
|
||||||
@PostMapping("/internal/firstleg/v1/container-events")
|
@PostMapping("/internal/firstleg/v1/container-events")
|
||||||
public ApiResponse<Object> receive(
|
public ApiResponse<ContainerBookingEntity> receive(
|
||||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||||
@RequestBody EventEnvelope event) {
|
@RequestBody EventEnvelope event) {
|
||||||
authenticate(token);
|
authenticate(token);
|
||||||
Object result = EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED.equals(event.eventType())
|
return ApiResponse.success(consumer.consume(event));
|
||||||
? detainedConsumer.consume(event) : consumer.consume(event);
|
|
||||||
return ApiResponse.success(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void authenticate(String token) {
|
private void authenticate(String token) {
|
||||||
|
|||||||
+2
-1
@@ -114,9 +114,10 @@ public class PlatformContainerController {
|
|||||||
@Operation(summary="修改集装箱号和航次", description="PDA拣货中仅开放此白名单;装柜完成后全部只读")
|
@Operation(summary="修改集装箱号和航次", description="PDA拣货中仅开放此白名单;装柜完成后全部只读")
|
||||||
public ApiResponse<ContainerBookingEntity> updateTransport(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<ContainerBookingEntity> updateTransport(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable @Positive long id,
|
@PathVariable @Positive long id,
|
||||||
|
@RequestHeader("Idempotency-Key") String key,
|
||||||
@Valid @RequestBody TransportUpdateRequest request) {
|
@Valid @RequestBody TransportUpdateRequest request) {
|
||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
return ApiResponse.success(service.updateTransport(id, user.userId(), user.username(), request));
|
return ApiResponse.success(service.updateTransport(id, user.userId(), user.username(), key, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}/clearance")
|
@GetMapping("/{id}/clearance")
|
||||||
|
|||||||
+13
-5
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
|||||||
|
|
||||||
import com.baiyue.common.audit.BusinessAudit;
|
import com.baiyue.common.audit.BusinessAudit;
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
@@ -18,8 +19,12 @@ import com.baiyue.firstleg.service.MeasurementSubmissionService;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -33,10 +38,10 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
|
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
|
||||||
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
|
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
|
||||||
|
@Validated
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/platform/v1/first-leg/inbounds/{inboundId}")
|
@RequestMapping("/api/platform/v1/first-leg/inbounds/{inboundId}")
|
||||||
public class PlatformInboundMeasurementController {
|
public class PlatformInboundMeasurementController {
|
||||||
@@ -56,16 +61,19 @@ public class PlatformInboundMeasurementController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "查询称重可选托盘快照",
|
@Operation(summary = "查询称重可选托盘快照",
|
||||||
description = "按入库单所属仓库返回未删除的托盘只读快照,不校验托盘状态或占用资格。")
|
description = "按入库单所属仓库分页返回当前有效托盘候选,支持编码或类型关键词;托盘可复用,不按预占或占用过滤。")
|
||||||
@GetMapping("/weight-pallet-options")
|
@GetMapping("/weight-pallet-options")
|
||||||
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:measure:view',"
|
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:measure:view',"
|
||||||
+ " 'platform:firstleg:inbound:weight:edit')")
|
+ " 'platform:firstleg:inbound:weight:edit')")
|
||||||
public ApiResponse<List<InboundMeasurementResponses.PalletSnapshot>> weightPalletOptions(
|
public ApiResponse<PageResult<InboundMeasurementResponses.PalletSnapshot>> weightPalletOptions(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam long tenantId,
|
@RequestParam long tenantId,
|
||||||
@PathVariable long inboundId) {
|
@PathVariable long inboundId,
|
||||||
|
@RequestParam(defaultValue = "1") @Min(1) long page,
|
||||||
|
@RequestParam(defaultValue = "20") @Min(1) @Max(100) long size,
|
||||||
|
@RequestParam(required = false) @Size(max = 50) String keyword) {
|
||||||
requirePlatformTenant(user, tenantId);
|
requirePlatformTenant(user, tenantId);
|
||||||
return ApiResponse.success(service.weightPalletOptions(tenantId, inboundId));
|
return ApiResponse.success(service.weightPalletOptions(tenantId, inboundId, page, size, keyword));
|
||||||
}
|
}
|
||||||
|
|
||||||
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
|
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
|
||||||
|
|||||||
-48
@@ -17,11 +17,6 @@ import java.util.List;
|
|||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.Task;
|
||||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||||
import com.baiyue.firstleg.service.LoadingPickService;
|
import com.baiyue.firstleg.service.LoadingPickService;
|
||||||
import com.baiyue.firstleg.service.LoadingWorkPalletService;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
@@ -46,8 +41,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class PlatformLoadingPdaController {
|
public class PlatformLoadingPdaController {
|
||||||
@Resource
|
@Resource
|
||||||
private LoadingPickService service;
|
private LoadingPickService service;
|
||||||
@Resource
|
|
||||||
private LoadingWorkPalletService palletService;
|
|
||||||
|
|
||||||
@GetMapping("/tasks")
|
@GetMapping("/tasks")
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
||||||
@@ -171,45 +164,4 @@ public class PlatformLoadingPdaController {
|
|||||||
FirstlegScopeGuard.platform(user);
|
FirstlegScopeGuard.platform(user);
|
||||||
return ApiResponse.success(service.operations(taskId));
|
return ApiResponse.success(service.operations(taskId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/tasks/{taskId}/pallets")
|
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
|
|
||||||
public ApiResponse<List<Pallet>> pallets(@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@PathVariable @Positive long taskId,
|
|
||||||
@RequestParam @Positive long warehouseId) {
|
|
||||||
FirstlegScopeGuard.platform(user);
|
|
||||||
return ApiResponse.success(palletService.list(taskId, warehouseId));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/tasks/{taskId}/pallets/open")
|
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
|
||||||
public ApiResponse<Pallet> openPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@PathVariable @Positive long taskId,
|
|
||||||
@Valid @RequestBody OpenRequest request) {
|
|
||||||
FirstlegScopeGuard.platform(user);
|
|
||||||
return ApiResponse.success(palletService.open(taskId, request, user.userId(), user.username()));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 扫到其他未封托托盘上的箱唛时,同时完成按箱移托。 */
|
|
||||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/cartons/scan")
|
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
|
||||||
public ApiResponse<Pallet> scanCarton(@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@PathVariable @Positive long taskId,
|
|
||||||
@PathVariable @Positive long palletId,
|
|
||||||
@Valid @RequestBody ScanRequest request) {
|
|
||||||
FirstlegScopeGuard.platform(user);
|
|
||||||
return ApiResponse.success(palletService.scan(taskId, palletId, request,
|
|
||||||
user.userId(), user.username()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/tasks/{taskId}/pallets/{palletId}/seal")
|
|
||||||
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
|
|
||||||
public ApiResponse<Pallet> sealPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@PathVariable @Positive long taskId,
|
|
||||||
@PathVariable @Positive long palletId,
|
|
||||||
@Valid @RequestBody SealRequest request) {
|
|
||||||
FirstlegScopeGuard.platform(user);
|
|
||||||
return ApiResponse.success(palletService.seal(taskId, palletId, request,
|
|
||||||
user.userId(), user.username()));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ public final class LoadingListDtos {
|
|||||||
public record History(
|
public record History(
|
||||||
Long historyId,
|
Long historyId,
|
||||||
Long itemId,
|
Long itemId,
|
||||||
|
String operationId,
|
||||||
String actionType,
|
String actionType,
|
||||||
String fromStatus,
|
String fromStatus,
|
||||||
String toStatus,
|
String toStatus,
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
package com.baiyue.firstleg.dto;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.Min;
|
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
import jakarta.validation.constraints.Size;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/** 一件代发PDA现场打托接口模型。 */
|
|
||||||
public final class LoadingPalletDtos {
|
|
||||||
private LoadingPalletDtos() {}
|
|
||||||
|
|
||||||
public record OpenRequest(
|
|
||||||
@NotNull @Min(1) Long warehouseId,
|
|
||||||
@Min(1) Long warehousePalletId,
|
|
||||||
@NotBlank @Size(max = 64) String palletCode,
|
|
||||||
@NotBlank @Size(max = 128) String operationId
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public record ScanRequest(
|
|
||||||
@NotNull @Min(1) Long warehouseId,
|
|
||||||
@NotBlank @Size(max = 80) String boxCode,
|
|
||||||
@NotBlank @Size(max = 128) String operationId
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public record SealRequest(
|
|
||||||
@NotNull @Min(1) Long warehouseId,
|
|
||||||
@NotNull @Min(0) Integer expectedVersion,
|
|
||||||
@NotBlank @Size(max = 128) String operationId
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public record Carton(Long id, String boxCode, String skuCode, Long tenantId,
|
|
||||||
Long inboundId, Long inboundDetailId, Integer unitsPerCarton) {}
|
|
||||||
|
|
||||||
public record SkuSummary(String skuCode, Integer cartonCount, Long productQuantity) {}
|
|
||||||
|
|
||||||
public record Pallet(Long id, String palletCode, Long warehousePalletId, String status,
|
|
||||||
Integer version, Integer cartonCount, Integer skuCount,
|
|
||||||
Integer customerCount, List<SkuSummary> skus, List<Carton> cartons) {}
|
|
||||||
}
|
|
||||||
@@ -125,12 +125,18 @@ public final class LoadingPickDtos {
|
|||||||
Long operatorId, String operatorName, LocalDateTime createdAt) {
|
Long operatorId, String operatorName, LocalDateTime createdAt) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record CompletionCommand(Long id, String operationId, String status, Integer expectedTaskVersion, String remark,
|
||||||
|
Integer attemptCount, LocalDateTime nextRetryAt, String failureStage,
|
||||||
|
String failureCode, String requestId, String traceId,
|
||||||
|
LocalDateTime completedAt) {
|
||||||
|
}
|
||||||
|
|
||||||
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
|
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
|
||||||
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
|
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
|
||||||
String currentContainerNo, String sourceType, String recordStatus,
|
String currentContainerNo, String sourceType, String recordStatus,
|
||||||
Long mergedIntoContainerBookingId, String taskStatus,
|
Long mergedIntoContainerBookingId, String taskStatus,
|
||||||
String workflowStatus, Integer version, Assignment assignment,
|
String workflowStatus, Integer version, Assignment assignment,
|
||||||
Review review, List<PickItem> items) {
|
Review review, CompletionCommand completionCommand, List<PickItem> items) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
|
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.baiyue.firstleg.entity;
|
package com.baiyue.firstleg.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
@@ -69,6 +70,8 @@ public class ContainerBookingEntity {
|
|||||||
@TableField(exist = false) private Long planId;
|
@TableField(exist = false) private Long planId;
|
||||||
@TableField(exist = false) private String planNo;
|
@TableField(exist = false) private String planNo;
|
||||||
@TableField(exist = false) private String planStatus;
|
@TableField(exist = false) private String planStatus;
|
||||||
|
/** Actual completion timestamp projected from the associated loading plan. */
|
||||||
|
@JsonIgnore @TableField(exist = false) private LocalDateTime completedLoadingAt;
|
||||||
@TableField(exist = false) private Boolean exceptionActive;
|
@TableField(exist = false) private Boolean exceptionActive;
|
||||||
@TableField(exist = false) private String destinationClearanceDocumentStatus;
|
@TableField(exist = false) private String destinationClearanceDocumentStatus;
|
||||||
@TableField(exist = false) private String customsDeclarationDocumentStatus;
|
@TableField(exist = false) private String customsDeclarationDocumentStatus;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public class ContainerTransportHistoryEntity {
|
|||||||
private String afterContainerNo;
|
private String afterContainerNo;
|
||||||
private String beforeVoyageNo;
|
private String beforeVoyageNo;
|
||||||
private String afterVoyageNo;
|
private String afterVoyageNo;
|
||||||
|
private String operationId;
|
||||||
|
private String requestHash;
|
||||||
private String reason;
|
private String reason;
|
||||||
private Long operatorId;
|
private Long operatorId;
|
||||||
private String operatorName;
|
private String operatorName;
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public class InboundCartonLabelEntity {
|
|||||||
private Long inboundDetailId;
|
private Long inboundDetailId;
|
||||||
private Integer cargoItemNo;
|
private Integer cargoItemNo;
|
||||||
private String cargoName;
|
private String cargoName;
|
||||||
private String skuCode;
|
|
||||||
private Integer detailPackageCount;
|
private Integer detailPackageCount;
|
||||||
private String inboundCode;
|
private String inboundCode;
|
||||||
private String boxCode;
|
private String boxCode;
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
package com.baiyue.firstleg.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@TableName("fl_inbound_hold_record")
|
|
||||||
public class InboundHoldRecordEntity {
|
|
||||||
@TableId(type = IdType.INPUT) private Long id;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long inboundId;
|
|
||||||
private String holdAction;
|
|
||||||
private String reason;
|
|
||||||
private Long evidenceFileId;
|
|
||||||
private Long operatorId;
|
|
||||||
private String operatorName;
|
|
||||||
private LocalDateTime operatedAt;
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,6 @@ public class InboundOrderEntity extends BaseEntity {
|
|||||||
private String destinationName;
|
private String destinationName;
|
||||||
private Boolean holdFlag;
|
private Boolean holdFlag;
|
||||||
private String currentHoldReason;
|
private String currentHoldReason;
|
||||||
private Long lastOverseasHoldVersion;
|
|
||||||
private Integer totalPackageCount;
|
private Integer totalPackageCount;
|
||||||
private BigDecimal actualGrossWeightKg;
|
private BigDecimal actualGrossWeightKg;
|
||||||
private BigDecimal chargeableWeightKg;
|
private BigDecimal chargeableWeightKg;
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.baiyue.firstleg.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/** Durable PDA completion command. Cross-service work is never executed in the acceptance transaction. */
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@TableName("fl_loading_completion_command")
|
||||||
|
public class LoadingCompletionCommandEntity {
|
||||||
|
@TableId(type = IdType.INPUT) private Long id;
|
||||||
|
private Long pickTaskId;
|
||||||
|
private String operationId;
|
||||||
|
private String requestHash;
|
||||||
|
private String commandStatus;
|
||||||
|
private Integer expectedTaskVersion;
|
||||||
|
private String remark;
|
||||||
|
private Long operatorId;
|
||||||
|
private String operatorName;
|
||||||
|
private Integer attemptCount;
|
||||||
|
private LocalDateTime nextRetryAt;
|
||||||
|
private String leaseOwner;
|
||||||
|
private LocalDateTime leaseUntil;
|
||||||
|
private String failureStage;
|
||||||
|
private String failureCode;
|
||||||
|
private String failureRequestId;
|
||||||
|
private String failureTraceId;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
private LocalDateTime completedAt;
|
||||||
|
}
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package com.baiyue.firstleg.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.baomidou.mybatisplus.annotation.Version;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/** 一件代发箱唛与现场实际托盘的当前绑定事实。 */
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@TableName("fl_loading_work_pallet_carton")
|
|
||||||
public class LoadingWorkPalletCartonEntity {
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
|
||||||
private Long id;
|
|
||||||
private Long pickTaskId;
|
|
||||||
private Long workPalletId;
|
|
||||||
private Long loadingPlanItemId;
|
|
||||||
private Long pickItemId;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long inboundId;
|
|
||||||
private Long inboundDetailId;
|
|
||||||
private Long cartonLabelId;
|
|
||||||
private String boxCode;
|
|
||||||
private String skuCode;
|
|
||||||
private Integer unitsPerCarton;
|
|
||||||
private Long boundBy;
|
|
||||||
private String boundByName;
|
|
||||||
private String operationId;
|
|
||||||
@Version
|
|
||||||
private Integer version;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
package com.baiyue.firstleg.entity;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
|
||||||
import com.baomidou.mybatisplus.annotation.Version;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/** 一件代发装柜现场的实际托盘。 */
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
@TableName("fl_loading_work_pallet")
|
|
||||||
public class LoadingWorkPalletEntity {
|
|
||||||
@TableId(type = IdType.ASSIGN_ID)
|
|
||||||
private Long id;
|
|
||||||
private Long pickTaskId;
|
|
||||||
private Long loadingPlanId;
|
|
||||||
private Long warehouseId;
|
|
||||||
private Long warehousePalletId;
|
|
||||||
private String palletCode;
|
|
||||||
private String openOperationId;
|
|
||||||
private String sealOperationId;
|
|
||||||
private String palletStatus;
|
|
||||||
private Long createdBy;
|
|
||||||
private String createdByName;
|
|
||||||
private Long sealedBy;
|
|
||||||
private String sealedByName;
|
|
||||||
private LocalDateTime sealedAt;
|
|
||||||
@Version
|
|
||||||
private Integer version;
|
|
||||||
private LocalDateTime createdAt;
|
|
||||||
private LocalDateTime updatedAt;
|
|
||||||
}
|
|
||||||
+1
-1
@@ -42,7 +42,7 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
|
|||||||
@Update("UPDATE fl_preload_plan SET status='CONFIRMED',confirmed_by=#{operatorId},"
|
@Update("UPDATE fl_preload_plan SET status='CONFIRMED',confirmed_by=#{operatorId},"
|
||||||
+ "confirmed_by_name=#{operatorName},confirmed_at=NOW(),updated_by=#{operatorId},"
|
+ "confirmed_by_name=#{operatorName},confirmed_at=NOW(),updated_by=#{operatorId},"
|
||||||
+ "updated_by_name=#{operatorName},version=version+1 WHERE id=#{preloadPlanId} "
|
+ "updated_by_name=#{operatorName},version=version+1 WHERE id=#{preloadPlanId} "
|
||||||
+ "AND tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND status='PENDING' "
|
+ "AND tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND status IN ('PENDING','CONFIRMING') "
|
||||||
+ "AND version=#{expectedVersion}")
|
+ "AND version=#{expectedVersion}")
|
||||||
int confirmIncrementalPreload(@Param("preloadPlanId") long preloadPlanId,
|
int confirmIncrementalPreload(@Param("preloadPlanId") long preloadPlanId,
|
||||||
@Param("tenantId") long tenantId,
|
@Param("tenantId") long tenantId,
|
||||||
|
|||||||
+4
-2
@@ -125,7 +125,9 @@ public class LoadingModelBridgeService {
|
|||||||
}
|
}
|
||||||
var preload = repository.preload(tenantId, inboundId);
|
var preload = repository.preload(tenantId, inboundId);
|
||||||
if (preload == null) throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
|
if (preload == null) throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
|
||||||
if (!Set.of("PENDING", "CONFIRMED").contains(preload.status())) {
|
boolean running = "LOADING".equals(plan.status());
|
||||||
|
if (!Set.of("PENDING", "CONFIRMED").contains(preload.status())
|
||||||
|
&& !(running && "CONFIRMING".equals(preload.status()))) {
|
||||||
throw conflict("新增入库单预装状态不允许进入当前柜次");
|
throw conflict("新增入库单预装状态不允许进入当前柜次");
|
||||||
}
|
}
|
||||||
PreloadLoadingPlanLinkEntity link = new PreloadLoadingPlanLinkEntity();
|
PreloadLoadingPlanLinkEntity link = new PreloadLoadingPlanLinkEntity();
|
||||||
@@ -145,7 +147,7 @@ public class LoadingModelBridgeService {
|
|||||||
// A running PDA task has already passed plan-level confirmation. Adding an inbound is the
|
// A running PDA task has already passed plan-level confirmation. Adding an inbound is the
|
||||||
// explicit Admin approval for that new allocation, so its fresh PENDING preload joins the
|
// explicit Admin approval for that new allocation, so its fresh PENDING preload joins the
|
||||||
// same confirmed plan atomically instead of leaving an unreachable pending aggregate.
|
// same confirmed plan atomically instead of leaving an unreachable pending aggregate.
|
||||||
if ("LOADING".equals(plan.status()) && "PENDING".equals(preload.status())
|
if (running && Set.of("PENDING", "CONFIRMING").contains(preload.status())
|
||||||
&& repository.confirmIncrementalPreload(preload.id(), tenantId, inboundId,
|
&& repository.confirmIncrementalPreload(preload.id(), tenantId, inboundId,
|
||||||
preload.version(), operatorId, operatorName) != 1) {
|
preload.version(), operatorId, operatorName) != 1) {
|
||||||
throw conflict("新增入库单预装确认并发冲突,请刷新后重试");
|
throw conflict("新增入库单预装确认并发冲突,请刷新后重试");
|
||||||
|
|||||||
@@ -61,6 +61,17 @@ public class LoadingRepository {
|
|||||||
.eq(LoadingEvidenceEntity::getSealNo, sealNo));
|
.eq(LoadingEvidenceEntity::getSealNo, sealNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LoadingEvidenceEntity findActiveEvidenceByFile(long tenantId, long inboundId,
|
||||||
|
String evidenceType, long fileId) {
|
||||||
|
return evidences.selectOne(new LambdaQueryWrapper<LoadingEvidenceEntity>()
|
||||||
|
.eq(LoadingEvidenceEntity::getTenantId, tenantId)
|
||||||
|
.eq(LoadingEvidenceEntity::getInboundId, inboundId)
|
||||||
|
.eq(LoadingEvidenceEntity::getEvidenceType, evidenceType)
|
||||||
|
.eq(LoadingEvidenceEntity::getFileId, fileId)
|
||||||
|
.eq(LoadingEvidenceEntity::getStatus, "ACTIVE")
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
public void createEvidence(LoadingEvidenceEntity entity) { evidences.insert(entity); }
|
public void createEvidence(LoadingEvidenceEntity entity) { evidences.insert(entity); }
|
||||||
|
|
||||||
public int voidEvidence(long tenantId, long inboundId, long evidenceId, String reason,
|
public int voidEvidence(long tenantId, long inboundId, long evidenceId, String reason,
|
||||||
|
|||||||
+62
-15
@@ -34,6 +34,7 @@ import java.security.MessageDigest;
|
|||||||
import java.util.HexFormat;
|
import java.util.HexFormat;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -74,6 +75,18 @@ public class LoadingDeliveryService {
|
|||||||
throw new BizException(ResultCode.CONFLICT, "锁码已用于其他装柜任务");
|
throw new BizException(ResultCode.CONFLICT, "锁码已用于其他装柜任务");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LoadingEvidenceEntity existingFile = repository.findActiveEvidenceByFile(
|
||||||
|
tenantId, inboundId, request.evidenceType(), request.fileId());
|
||||||
|
if (existingFile != null) {
|
||||||
|
String requestedSealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType())
|
||||||
|
? request.sealNo().trim() : null;
|
||||||
|
if (!Objects.equals(existingFile.getSealNo(), requestedSealNo)) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "该带锁码照已绑定其他锁码,请先作废后重新拍摄");
|
||||||
|
}
|
||||||
|
LoadingActionResponse result = new LoadingActionResponse(
|
||||||
|
requireInbound(tenantId, inboundId).processStatus(), evidenceResponse(tenantId, inboundId));
|
||||||
|
return complete(tenantId, inboundId, operatorId, UPSERT_EVIDENCE, key, result);
|
||||||
|
}
|
||||||
var file = validateFile(tenantId, inboundId, request.fileId(), request.evidenceType());
|
var file = validateFile(tenantId, inboundId, request.fileId(), request.evidenceType());
|
||||||
String fileName = file.fileName();
|
String fileName = file.fileName();
|
||||||
String sealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType()) ? request.sealNo().trim() : null;
|
String sealNo = LoadingEvidenceType.SEAL.equals(request.evidenceType()) ? request.sealNo().trim() : null;
|
||||||
@@ -103,23 +116,33 @@ public class LoadingDeliveryService {
|
|||||||
private LoadingActionResponse completeLoading(long tenantId, long inboundId, long evidenceInboundId,
|
private LoadingActionResponse completeLoading(long tenantId, long inboundId, long evidenceInboundId,
|
||||||
long operatorId, String operatorName, String key,
|
long operatorId, String operatorName, String key,
|
||||||
CompleteLoadingRequest request) {
|
CompleteLoadingRequest request) {
|
||||||
|
return completeLoading(tenantId, inboundId, evidenceInboundId, operatorId, operatorName,
|
||||||
|
key, request, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadingActionResponse completeLoading(long tenantId, long inboundId, long evidenceInboundId,
|
||||||
|
long operatorId, String operatorName, String key,
|
||||||
|
CompleteLoadingRequest request,
|
||||||
|
boolean acceptedPdaCompletion) {
|
||||||
String hash = hash(request);
|
String hash = hash(request);
|
||||||
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, COMPLETE_LOADING, key, hash);
|
LoadingActionResponse replay = claimOrReplay(tenantId, inboundId, operatorId, COMPLETE_LOADING,
|
||||||
|
key, hash, acceptedPdaCompletion);
|
||||||
if (replay != null) return replay;
|
if (replay != null) return replay;
|
||||||
Snapshot inbound = requireInbound(tenantId, inboundId);
|
Snapshot inbound = requireInbound(tenantId, inboundId);
|
||||||
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
|
||||||
if (target != null) {
|
if (target != null) {
|
||||||
return completeFulfillmentSlice(tenantId, evidenceInboundId, operatorId, operatorName, key, request, target);
|
return completeFulfillmentSlice(tenantId, evidenceInboundId, operatorId, operatorName,
|
||||||
|
key, request, target, acceptedPdaCompletion);
|
||||||
}
|
}
|
||||||
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
if ("LOADED".equals(inbound.processStatus()) || "DELIVERED".equals(inbound.processStatus())) {
|
||||||
loadingModelBridge.requireCompleted(tenantId, inboundId);
|
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||||
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
return complete(tenantId, inboundId, operatorId, COMPLETE_LOADING, key,
|
||||||
new LoadingActionResponse(inbound.processStatus(), evidenceResponse(tenantId, inboundId)));
|
new LoadingActionResponse(inbound.processStatus(), evidenceResponse(tenantId, inboundId)));
|
||||||
}
|
}
|
||||||
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
|
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
|
||||||
loadingModelBridge.requireCompleted(tenantId, inboundId);
|
requireLoadingPlanGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||||
completionGate.requireReleasedBooking(tenantId, inboundId);
|
completionGate.requireReleasedBooking(tenantId, inboundId);
|
||||||
requireCompletedPicking(tenantId, inboundId);
|
requirePickingGate(tenantId, inboundId, acceptedPdaCompletion);
|
||||||
requireCompletionEvidence(tenantId, evidenceInboundId);
|
requireCompletionEvidence(tenantId, evidenceInboundId);
|
||||||
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
|
||||||
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
addHistory(tenantId, inboundId, "LOADING", "LOADED", COMPLETE_LOADING, request.remark(), operatorId, operatorName);
|
||||||
@@ -132,12 +155,14 @@ public class LoadingDeliveryService {
|
|||||||
* Resolve and lock the canonical completion row after settlement, then retain optimistic locking
|
* Resolve and lock the canonical completion row after settlement, then retain optimistic locking
|
||||||
* with that current server version instead of reusing the pre-settlement task snapshot.
|
* with that current server version instead of reusing the pre-settlement task snapshot.
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
||||||
long operatorId, String operatorName,
|
long operatorId, String operatorName,
|
||||||
String key, String remark) {
|
String key, String remark) {
|
||||||
return completeAfterPdaSettlement(tenantId, inboundId, inboundId, operatorId, operatorName, key, remark);
|
return completeAfterPdaSettlement(tenantId, inboundId, inboundId, operatorId, operatorName, key, remark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
|
||||||
long evidenceInboundId, long operatorId,
|
long evidenceInboundId, long operatorId,
|
||||||
String operatorName, String key, String remark) {
|
String operatorName, String key, String remark) {
|
||||||
@@ -148,7 +173,7 @@ public class LoadingDeliveryService {
|
|||||||
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
|
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
|
||||||
}
|
}
|
||||||
return completeLoading(tenantId, inboundId, evidenceInboundId, operatorId, operatorName, key,
|
return completeLoading(tenantId, inboundId, evidenceInboundId, operatorId, operatorName, key,
|
||||||
new CompleteLoadingRequest(current.version(), remark));
|
new CompleteLoadingRequest(current.version(), remark), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,7 +197,8 @@ public class LoadingDeliveryService {
|
|||||||
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long evidenceInboundId,
|
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long evidenceInboundId,
|
||||||
long operatorId, String operatorName, String key,
|
long operatorId, String operatorName, String key,
|
||||||
CompleteLoadingRequest request,
|
CompleteLoadingRequest request,
|
||||||
CompletionTarget target) {
|
CompletionTarget target,
|
||||||
|
boolean acceptedPdaCompletion) {
|
||||||
if ("LOADED".equals(target.loadedChildStatus())) {
|
if ("LOADED".equals(target.loadedChildStatus())) {
|
||||||
return complete(tenantId, evidenceInboundId, operatorId, COMPLETE_LOADING, key,
|
return complete(tenantId, evidenceInboundId, operatorId, COMPLETE_LOADING, key,
|
||||||
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, evidenceInboundId)));
|
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, evidenceInboundId)));
|
||||||
@@ -180,10 +206,9 @@ public class LoadingDeliveryService {
|
|||||||
if (!"LOADING".equals(target.loadedChildStatus())) {
|
if (!"LOADING".equals(target.loadedChildStatus())) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
|
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
|
||||||
}
|
}
|
||||||
loadingModelBridge.requireCompleted(tenantId, target.sourceInboundId());
|
requireLoadingPlanGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
|
||||||
completionGate.requireReleasedBooking(tenantId, target.sourceInboundId());
|
completionGate.requireReleasedBooking(tenantId, target.sourceInboundId());
|
||||||
// The cabinet PDA task is the source of truth while settling. The inbound-level
|
requirePickingGate(tenantId, target.sourceInboundId(), acceptedPdaCompletion);
|
||||||
// picking review is completed after this transition and must not gate it here.
|
|
||||||
requireCompletionEvidence(tenantId, evidenceInboundId);
|
requireCompletionEvidence(tenantId, evidenceInboundId);
|
||||||
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
|
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
|
||||||
request.expectedVersion()) != 1) throw concurrent();
|
request.expectedVersion()) != 1) throw concurrent();
|
||||||
@@ -296,9 +321,21 @@ public class LoadingDeliveryService {
|
|||||||
task.getExpectedCount(), task.getPickedCount(), task.getRemark());
|
task.getExpectedCount(), task.getPickedCount(), task.getRemark());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requireCompletedPicking(long tenantId, long inboundId) {
|
/** worker 先完成各入库单,之后才完成柜次计划;这里不能循环要求计划已经 COMPLETED。 */
|
||||||
if (!canonicalPicking(tenantId, inboundId).completed()) {
|
private void requireLoadingPlanGate(long tenantId, long inboundId, boolean acceptedPdaCompletion) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "PDA拣货复核尚未完成");
|
if (acceptedPdaCompletion) {
|
||||||
|
loadingModelBridge.requireStarted(tenantId, inboundId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
loadingModelBridge.requireCompleted(tenantId, inboundId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** completion worker 已持有 durable lease;此时 task 尚为 COMPLETING,不能循环要求最终 COMPLETED。 */
|
||||||
|
private void requirePickingGate(long tenantId, long inboundId, boolean acceptedPdaCompletion) {
|
||||||
|
CanonicalPicking picking = canonicalPicking(tenantId, inboundId);
|
||||||
|
if (acceptedPdaCompletion ? !picking.resolved() : !picking.completed()) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, acceptedPdaCompletion
|
||||||
|
? "PDA实装明细尚未全部解决" : "PDA拣货复核尚未完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,9 +362,12 @@ public class LoadingDeliveryService {
|
|||||||
|
|
||||||
private record CanonicalPicking(int expectedCount, int pickedCount, int taskCount,
|
private record CanonicalPicking(int expectedCount, int pickedCount, int taskCount,
|
||||||
int completedTaskCount, int unresolvedCount) {
|
int completedTaskCount, int unresolvedCount) {
|
||||||
boolean completed() {
|
boolean resolved() {
|
||||||
return expectedCount > 0 && expectedCount == pickedCount && taskCount > 0
|
return expectedCount > 0 && expectedCount == pickedCount && taskCount > 0
|
||||||
&& completedTaskCount == taskCount && unresolvedCount == 0;
|
&& unresolvedCount == 0;
|
||||||
|
}
|
||||||
|
boolean completed() {
|
||||||
|
return resolved() && completedTaskCount == taskCount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,10 +459,17 @@ public class LoadingDeliveryService {
|
|||||||
|
|
||||||
private LoadingActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
private LoadingActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
||||||
String operation, String key, String hash) {
|
String operation, String key, String hash) {
|
||||||
|
return claimOrReplay(tenantId, inboundId, operatorId, operation, key, hash, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoadingActionResponse claimOrReplay(long tenantId, long inboundId, long operatorId,
|
||||||
|
String operation, String key, String hash,
|
||||||
|
boolean resumeProcessing) {
|
||||||
validateKey(key);
|
validateKey(key);
|
||||||
if (repository.claimOperation(tenantId, inboundId, operatorId, operation, key, hash)) return null;
|
if (repository.claimOperation(tenantId, inboundId, operatorId, operation, key, hash)) return null;
|
||||||
LoadingOperationEntity existing = repository.findOperation(tenantId, inboundId, operatorId, operation, key);
|
LoadingOperationEntity existing = repository.findOperation(tenantId, inboundId, operatorId, operation, key);
|
||||||
if (existing == null || !hash.equals(existing.getRequestHash())) throw new BizException(ResultCode.CONFLICT, "幂等键已用于其他请求");
|
if (existing == null || !hash.equals(existing.getRequestHash())) throw new BizException(ResultCode.CONFLICT, "幂等键已用于其他请求");
|
||||||
|
if (resumeProcessing && "PROCESSING".equals(existing.getStatus())) return null;
|
||||||
if (!"SUCCESS".equals(existing.getStatus()) || !StringUtils.hasText(existing.getResultJson())) throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中,请稍后重试");
|
if (!"SUCCESS".equals(existing.getStatus()) || !StringUtils.hasText(existing.getResultJson())) throw new BizException(ResultCode.CONFLICT, "相同请求正在处理中,请稍后重试");
|
||||||
try { return objectMapper.readValue(existing.getResultJson(), LoadingActionResponse.class); }
|
try { return objectMapper.readValue(existing.getResultJson(), LoadingActionResponse.class); }
|
||||||
catch (JsonProcessingException e) { throw new IllegalStateException("无法读取装柜幂等结果", e); }
|
catch (JsonProcessingException e) { throw new IllegalStateException("无法读取装柜幂等结果", e); }
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
|||||||
@Select("""
|
@Select("""
|
||||||
<script>
|
<script>
|
||||||
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
|
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
|
||||||
|
CASE WHEN p.plan_status = 'COMPLETED' THEN COALESCE(p.confirmed_at, p.updated_at) END AS completed_loading_at,
|
||||||
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
|
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
|
||||||
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
|
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
|
||||||
THEN TRUE ELSE FALSE END AS exception_active,
|
THEN TRUE ELSE FALSE END AS exception_active,
|
||||||
@@ -77,6 +78,7 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
|
|||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
|
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
|
||||||
|
CASE WHEN p.plan_status = 'COMPLETED' THEN COALESCE(p.confirmed_at, p.updated_at) END AS completed_loading_at,
|
||||||
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
|
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
|
||||||
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
|
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
|
||||||
THEN TRUE ELSE FALSE END AS exception_active,
|
THEN TRUE ELSE FALSE END AS exception_active,
|
||||||
|
|||||||
+8
-1
@@ -3,6 +3,13 @@ package com.baiyue.firstleg.mapper;
|
|||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
|
import com.baiyue.firstleg.entity.ContainerTransportHistoryEntity;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface ContainerTransportHistoryMapper extends BaseMapper<ContainerTransportHistoryEntity> {}
|
public interface ContainerTransportHistoryMapper extends BaseMapper<ContainerTransportHistoryEntity> {
|
||||||
|
@Select("SELECT * FROM fl_container_transport_history WHERE container_booking_id=#{bookingId} "
|
||||||
|
+ "AND operator_id=#{operatorId} AND operation_id=#{operationId} LIMIT 1")
|
||||||
|
ContainerTransportHistoryEntity selectOperation(@Param("bookingId") long bookingId,
|
||||||
|
@Param("operatorId") long operatorId, @Param("operationId") String operationId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
package com.baiyue.firstleg.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface InboundHoldRecordMapper extends BaseMapper<InboundHoldRecordEntity> {
|
|
||||||
}
|
|
||||||
@@ -5,25 +5,10 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Update;
|
import org.apache.ibatis.annotations.Update;
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
|
|
||||||
/** 入库聚合根条件更新 Mapper,所有推进均同时校验租户、前置状态和版本。 */
|
/** 入库聚合根条件更新 Mapper,所有推进均同时校验租户、前置状态和版本。 */
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
|
||||||
@Select("SELECT * FROM fl_inbound_order WHERE id=#{inboundId} AND deleted=0 FOR UPDATE")
|
|
||||||
InboundOrderEntity selectActiveByIdForUpdate(@Param("inboundId") long inboundId);
|
|
||||||
|
|
||||||
@Update("""
|
|
||||||
UPDATE fl_inbound_order
|
|
||||||
SET hold_flag=#{detained}, current_hold_reason=#{reason},
|
|
||||||
last_overseas_hold_version=#{sourceVersion}, updated_by=#{operatorId}, version=version+1
|
|
||||||
WHERE id=#{inboundId} AND tenant_id=#{tenantId} AND deleted=0
|
|
||||||
AND (last_overseas_hold_version IS NULL OR last_overseas_hold_version < #{sourceVersion})
|
|
||||||
""")
|
|
||||||
int applyOverseasHold(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
|
|
||||||
@Param("detained") boolean detained, @Param("reason") String reason,
|
|
||||||
@Param("sourceVersion") long sourceVersion, @Param("operatorId") long operatorId);
|
|
||||||
|
|
||||||
@Update("""
|
@Update("""
|
||||||
UPDATE fl_inbound_order
|
UPDATE fl_inbound_order
|
||||||
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
|
||||||
|
|||||||
+14
-29
@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Param;
|
|||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** 读取 PDA 结算后的最终实装入仓单和冻结货物快照。 */
|
/** 读取 PDA 结算后的最终实装入仓单和冻结货物快照。 */
|
||||||
@@ -15,6 +16,9 @@ public interface LoadingCompletedEventMapper {
|
|||||||
@Select("""
|
@Select("""
|
||||||
SELECT p.id AS loading_plan_id, p.plan_no, p.container_booking_id, p.version AS aggregate_version,
|
SELECT p.id AS loading_plan_id, p.plan_no, p.container_booking_id, p.version AS aggregate_version,
|
||||||
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
|
||||||
|
b.customer_id AS booking_customer_id, b.customer_name,
|
||||||
|
b.estimated_port_arrival_at, b.actual_port_arrival_at,
|
||||||
|
b.estimated_warehouse_arrival_at,
|
||||||
i.account_id, i.inbound_id AS source_inbound_id,
|
i.account_id, i.inbound_id AS source_inbound_id,
|
||||||
COALESCE(o.root_inbound_id, o.id) AS root_inbound_id,
|
COALESCE(o.root_inbound_id, o.id) AS root_inbound_id,
|
||||||
o.order_id AS source_order_id, o.order_no AS source_order_no,
|
o.order_id AS source_order_id, o.order_no AS source_order_no,
|
||||||
@@ -46,10 +50,11 @@ public interface LoadingCompletedEventMapper {
|
|||||||
""")
|
""")
|
||||||
List<Row> rows(@Param("planId") long planId);
|
List<Row> rows(@Param("planId") long planId);
|
||||||
|
|
||||||
/** 读取头程托盘及箱唛快照;普通订单向尾程交接打印编号,一键代发同时冻结托盘库存。 */
|
/** 读取一键代发托盘及其箱唛快照;普通订单不会使用该结果。 */
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT i.account_id, i.inbound_id AS source_inbound_id,0 AS actual_binding,
|
SELECT i.account_id, i.inbound_id AS source_inbound_id,
|
||||||
a.pallet_code, a.allocation_no, a.picked_package_count AS allocated_package_count,
|
a.pallet_id, a.reservation_id, a.pallet_code, a.allocation_no,
|
||||||
|
a.picked_package_count AS allocated_package_count,
|
||||||
i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
|
i.inbound_detail_id AS source_inbound_detail_id, i.sku_code,
|
||||||
i.product_name_zh, i.package_count, i.product_quantity,
|
i.product_name_zh, i.package_count, i.product_quantity,
|
||||||
label.box_code
|
label.box_code
|
||||||
@@ -75,34 +80,14 @@ public interface LoadingCompletedEventMapper {
|
|||||||
""")
|
""")
|
||||||
List<PalletRow> palletRows(@Param("planId") long planId);
|
List<PalletRow> palletRows(@Param("planId") long planId);
|
||||||
|
|
||||||
/** 一件代发现场逐箱扫描结果;同一实物托盘可以包含多个客户,但行仍保留客户与入仓单归属。 */
|
|
||||||
@Select("""
|
|
||||||
SELECT carton.tenant_id AS account_id,carton.inbound_id AS source_inbound_id,1 AS actual_binding,
|
|
||||||
pallet.pallet_code,NULL AS allocation_no,
|
|
||||||
COUNT(*) OVER (PARTITION BY pallet.id,carton.tenant_id,carton.inbound_id) AS allocated_package_count,
|
|
||||||
carton.inbound_detail_id AS source_inbound_detail_id,carton.sku_code,
|
|
||||||
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code
|
|
||||||
FROM fl_loading_work_pallet pallet
|
|
||||||
JOIN fl_loading_work_pallet_carton carton ON carton.work_pallet_id=pallet.id
|
|
||||||
JOIN fl_loading_plan_item item ON item.id=carton.loading_plan_item_id AND item.deleted=0
|
|
||||||
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
|
|
||||||
AND task.loading_plan_id=pallet.loading_plan_id
|
|
||||||
WHERE pallet.loading_plan_id=#{planId} AND pallet.pallet_status='SEALED'
|
|
||||||
ORDER BY carton.tenant_id,carton.inbound_id,pallet.id,item.inbound_detail_id,carton.id
|
|
||||||
""")
|
|
||||||
List<PalletRow> actualPalletRows(@Param("planId") long planId);
|
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet WHERE loading_plan_id=#{planId} AND pallet_status<>'CANCELLED'")
|
|
||||||
int workPalletCount(@Param("planId") long planId);
|
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet WHERE loading_plan_id=#{planId} AND pallet_status='OPEN'")
|
|
||||||
int openWorkPalletCount(@Param("planId") long planId);
|
|
||||||
|
|
||||||
@Getter @Setter
|
@Getter @Setter
|
||||||
class Row {
|
class Row {
|
||||||
private Long loadingPlanId; private String planNo; private Long containerBookingId;
|
private Long loadingPlanId; private String planNo; private Long containerBookingId;
|
||||||
private Integer aggregateVersion; private String containerNo; private String cabinetNo;
|
private Integer aggregateVersion; private String containerNo; private String cabinetNo;
|
||||||
private Long warehouseId; private String warehouseName; private Long accountId;
|
private Long warehouseId; private String warehouseName; private Long bookingCustomerId;
|
||||||
|
private String customerName; private Long accountId;
|
||||||
|
private LocalDateTime estimatedPortArrivalAt; private LocalDateTime actualPortArrivalAt;
|
||||||
|
private LocalDateTime estimatedWarehouseArrivalAt;
|
||||||
private Long sourceInboundId; private Long rootInboundId; private Long sourceOrderId;
|
private Long sourceInboundId; private Long rootInboundId; private Long sourceOrderId;
|
||||||
private Long loadingPlanItemId;
|
private Long loadingPlanItemId;
|
||||||
private String sourceOrderNo; private Long rootOrderId; private String inboundCode;
|
private String sourceOrderNo; private Long rootOrderId; private String inboundCode;
|
||||||
@@ -118,8 +103,8 @@ public interface LoadingCompletedEventMapper {
|
|||||||
|
|
||||||
@Getter @Setter
|
@Getter @Setter
|
||||||
class PalletRow {
|
class PalletRow {
|
||||||
private Long accountId; private Long sourceInboundId; private String palletCode;
|
private Long accountId; private Long sourceInboundId; private Long palletId;
|
||||||
private Boolean actualBinding;
|
private Long reservationId; private String palletCode;
|
||||||
private Integer allocationNo; private Integer allocatedPackageCount;
|
private Integer allocationNo; private Integer allocatedPackageCount;
|
||||||
private Long sourceInboundDetailId; private String skuCode; private String productNameZh;
|
private Long sourceInboundDetailId; private String skuCode; private String productNameZh;
|
||||||
private Integer packageCount; private Integer productQuantity; private String boxCode;
|
private Integer packageCount; private Integer productQuantity; private String boxCode;
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
package com.baiyue.firstleg.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity;
|
||||||
|
import org.apache.ibatis.annotations.Insert;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Mapper
|
||||||
|
public interface LoadingCompletionCommandMapper extends BaseMapper<LoadingCompletionCommandEntity> {
|
||||||
|
@Insert("""
|
||||||
|
INSERT IGNORE INTO fl_loading_completion_command
|
||||||
|
(id,pick_task_id,operation_id,request_hash,command_status,expected_task_version,remark,
|
||||||
|
operator_id,operator_name,attempt_count,next_retry_at)
|
||||||
|
VALUES
|
||||||
|
(#{id},#{pickTaskId},#{operationId},#{requestHash},'PROCESSING',#{expectedTaskVersion},#{remark},
|
||||||
|
#{operatorId},#{operatorName},0,CURRENT_TIMESTAMP(3))
|
||||||
|
""")
|
||||||
|
int insertCommand(LoadingCompletionCommandEntity command);
|
||||||
|
|
||||||
|
@Select("SELECT * FROM fl_loading_completion_command WHERE operation_id=#{operationId}")
|
||||||
|
LoadingCompletionCommandEntity findByOperation(@Param("operationId") String operationId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT * FROM fl_loading_completion_command
|
||||||
|
WHERE pick_task_id=#{taskId} AND command_status IN ('PROCESSING','RETRYABLE','FAILED')
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
""")
|
||||||
|
LoadingCompletionCommandEntity findActiveByTask(@Param("taskId") long taskId);
|
||||||
|
|
||||||
|
@Select("SELECT * FROM fl_loading_completion_command WHERE pick_task_id=#{taskId} ORDER BY created_at DESC LIMIT 1")
|
||||||
|
LoadingCompletionCommandEntity findLatestByTask(@Param("taskId") long taskId);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT * FROM fl_loading_completion_command
|
||||||
|
WHERE command_status IN ('PROCESSING','RETRYABLE') AND next_retry_at<=CURRENT_TIMESTAMP(3)
|
||||||
|
AND (lease_until IS NULL OR lease_until<CURRENT_TIMESTAMP(3))
|
||||||
|
ORDER BY next_retry_at,id LIMIT #{limit}
|
||||||
|
""")
|
||||||
|
List<LoadingCompletionCommandEntity> findDue(@Param("limit") int limit);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_completion_command
|
||||||
|
SET command_status='PROCESSING',lease_owner=#{worker},lease_until=#{leaseUntil},
|
||||||
|
attempt_count=attempt_count+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||||
|
WHERE id=#{id} AND command_status IN ('PROCESSING','RETRYABLE')
|
||||||
|
AND next_retry_at<=CURRENT_TIMESTAMP(3)
|
||||||
|
AND (lease_until IS NULL OR lease_until<CURRENT_TIMESTAMP(3))
|
||||||
|
""")
|
||||||
|
int claim(@Param("id") long id, @Param("worker") String worker,
|
||||||
|
@Param("leaseUntil") LocalDateTime leaseUntil);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_completion_command SET lease_until=#{leaseUntil},updated_at=CURRENT_TIMESTAMP(3)
|
||||||
|
WHERE id=#{id} AND command_status='PROCESSING' AND lease_owner=#{worker}
|
||||||
|
""")
|
||||||
|
int renew(@Param("id") long id, @Param("worker") String worker,
|
||||||
|
@Param("leaseUntil") LocalDateTime leaseUntil);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_completion_command
|
||||||
|
SET command_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),updated_at=CURRENT_TIMESTAMP(3),
|
||||||
|
lease_owner=NULL,lease_until=NULL,failure_stage=NULL,failure_code=NULL,
|
||||||
|
failure_request_id=NULL,failure_trace_id=NULL
|
||||||
|
WHERE id=#{id} AND command_status='PROCESSING' AND lease_owner=#{worker}
|
||||||
|
""")
|
||||||
|
int complete(@Param("id") long id, @Param("worker") String worker);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_completion_command
|
||||||
|
SET command_status=#{status},next_retry_at=#{nextRetryAt},updated_at=CURRENT_TIMESTAMP(3),
|
||||||
|
lease_owner=NULL,lease_until=NULL,failure_stage=#{stage},failure_code=#{code},
|
||||||
|
failure_request_id=#{requestId},failure_trace_id=#{traceId}
|
||||||
|
WHERE id=#{id} AND command_status='PROCESSING' AND lease_owner=#{worker}
|
||||||
|
""")
|
||||||
|
int fail(@Param("id") long id, @Param("worker") String worker, @Param("status") String status,
|
||||||
|
@Param("nextRetryAt") LocalDateTime nextRetryAt, @Param("stage") String stage,
|
||||||
|
@Param("code") String code, @Param("requestId") String requestId, @Param("traceId") String traceId);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_completion_command
|
||||||
|
SET command_status='RETRYABLE',next_retry_at=CURRENT_TIMESTAMP(3),updated_at=CURRENT_TIMESTAMP(3),
|
||||||
|
lease_owner=NULL,lease_until=NULL
|
||||||
|
WHERE id=#{id} AND command_status='FAILED'
|
||||||
|
""")
|
||||||
|
int resume(@Param("id") long id);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT checkpoint_stage FROM fl_loading_completion_checkpoint
|
||||||
|
WHERE command_id=#{commandId} AND tenant_id=#{tenantId} AND inbound_id=#{inboundId}
|
||||||
|
""")
|
||||||
|
String checkpoint(@Param("commandId") long commandId, @Param("tenantId") long tenantId,
|
||||||
|
@Param("inboundId") long inboundId);
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO fl_loading_completion_checkpoint
|
||||||
|
(command_id,tenant_id,inbound_id,owner_sequence,checkpoint_stage)
|
||||||
|
VALUES (#{commandId},#{tenantId},#{inboundId},#{sequence},#{stage})
|
||||||
|
ON DUPLICATE KEY UPDATE checkpoint_stage=VALUES(checkpoint_stage),updated_at=CURRENT_TIMESTAMP(3)
|
||||||
|
""")
|
||||||
|
int checkpointCompleted(@Param("commandId") long commandId, @Param("tenantId") long tenantId,
|
||||||
|
@Param("inboundId") long inboundId, @Param("sequence") int sequence,
|
||||||
|
@Param("stage") String stage);
|
||||||
|
}
|
||||||
@@ -232,6 +232,7 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
|||||||
/**
|
/**
|
||||||
* 以结算后的装柜计划事实为完成口径,不能只检查当前 PDA 任务中的明细。
|
* 以结算后的装柜计划事实为完成口径,不能只检查当前 PDA 任务中的明细。
|
||||||
* PARTIAL 在本查询前已把计划箱数收敛为实际拣货箱数;收敛前的严格小于关系由结算计划校验。
|
* PARTIAL 在本查询前已把计划箱数收敛为实际拣货箱数;收敛前的严格小于关系由结算计划校验。
|
||||||
|
* 异步完成期间任务会在 COMPLETING/COMPLETION_FAILED 间切换,不能把任务状态当成明细未完成事实。
|
||||||
*/
|
*/
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
@@ -244,7 +245,6 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
|||||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||||
WHERE pick_item.loading_plan_item_id = plan_item.id
|
WHERE pick_item.loading_plan_item_id = plan_item.id
|
||||||
AND pick_task.loading_plan_id = #{planId}
|
AND pick_task.loading_plan_id = #{planId}
|
||||||
AND pick_task.task_status = 'LOADING'
|
|
||||||
AND (
|
AND (
|
||||||
(pick_item.item_status = 'PICKED'
|
(pick_item.item_status = 'PICKED'
|
||||||
AND pick_item.picked_package_count = plan_item.package_count)
|
AND pick_item.picked_package_count = plan_item.package_count)
|
||||||
|
|||||||
@@ -19,6 +19,29 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
+ "AND task_status='LOADING' AND version=#{expectedVersion}")
|
+ "AND task_status='LOADING' AND version=#{expectedVersion}")
|
||||||
int touch(@Param("taskId") long taskId, @Param("expectedVersion") int expectedVersion);
|
int touch(@Param("taskId") long taskId, @Param("expectedVersion") int expectedVersion);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_pick_task SET task_status='COMPLETING',version=version+1
|
||||||
|
WHERE id=#{taskId} AND task_status='LOADING' AND version=#{expectedVersion}
|
||||||
|
""")
|
||||||
|
int acceptCompletion(@Param("taskId") long taskId, @Param("expectedVersion") int expectedVersion);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE fl_loading_pick_task
|
||||||
|
SET task_status='COMPLETED',completed_by=#{operatorId},completed_by_name=#{operatorName},
|
||||||
|
completed_at=CURRENT_TIMESTAMP(3),version=version+1
|
||||||
|
WHERE id=#{taskId} AND task_status='COMPLETING'
|
||||||
|
""")
|
||||||
|
int completeAccepted(@Param("taskId") long taskId, @Param("operatorId") long operatorId,
|
||||||
|
@Param("operatorName") String operatorName);
|
||||||
|
|
||||||
|
@Update("UPDATE fl_loading_pick_task SET task_status='COMPLETION_FAILED',version=version+1 "
|
||||||
|
+ "WHERE id=#{taskId} AND task_status='COMPLETING'")
|
||||||
|
int failCompletion(@Param("taskId") long taskId);
|
||||||
|
|
||||||
|
@Update("UPDATE fl_loading_pick_task SET task_status='COMPLETING',version=version+1 "
|
||||||
|
+ "WHERE id=#{taskId} AND task_status='COMPLETION_FAILED'")
|
||||||
|
int resumeCompletion(@Param("taskId") long taskId);
|
||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT DISTINCT task.*
|
SELECT DISTINCT task.*
|
||||||
FROM fl_loading_pick_task task
|
FROM fl_loading_pick_task task
|
||||||
@@ -60,6 +83,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
booking.merged_into_container_booking_id AS mergedIntoContainerBookingId,
|
booking.merged_into_container_booking_id AS mergedIntoContainerBookingId,
|
||||||
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
|
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
|
||||||
CASE WHEN task.task_status = 'COMPLETED' THEN 'COMPLETED'
|
CASE WHEN task.task_status = 'COMPLETED' THEN 'COMPLETED'
|
||||||
|
WHEN task.task_status = 'COMPLETING' THEN 'PROCESSING'
|
||||||
|
WHEN task.task_status = 'COMPLETION_FAILED' THEN 'COMPLETION_FAILED'
|
||||||
WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED'
|
WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED'
|
||||||
WHEN SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) = 0
|
WHEN SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) = 0
|
||||||
THEN 'AWAITING_REVIEW'
|
THEN 'AWAITING_REVIEW'
|
||||||
@@ -72,7 +97,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
|
|||||||
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
|
||||||
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
|
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
|
||||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
|
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
|
||||||
WHERE task.task_status = 'LOADING' AND booking.record_status = 'ACTIVE'
|
WHERE task.task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED') AND booking.record_status = 'ACTIVE'
|
||||||
AND (assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId})
|
AND (assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId})
|
||||||
<if test="keyword != null and keyword != ''">
|
<if test="keyword != null and keyword != ''">
|
||||||
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
package com.baiyue.firstleg.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Update;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface LoadingWorkPalletCartonMapper extends BaseMapper<LoadingWorkPalletCartonEntity> {
|
|
||||||
@Select("""
|
|
||||||
SELECT label.id carton_label_id,label.tenant_id,label.inbound_id,label.inbound_detail_id,
|
|
||||||
label.box_code,plan_item.id loading_plan_item_id,pick_item.id pick_item_id,
|
|
||||||
plan_item.sku_code,plan_item.units_per_carton
|
|
||||||
FROM fl_inbound_carton_label label
|
|
||||||
JOIN fl_loading_plan_item plan_item
|
|
||||||
ON plan_item.account_id=label.tenant_id AND plan_item.inbound_id=label.inbound_id
|
|
||||||
AND plan_item.inbound_detail_id=label.inbound_detail_id AND plan_item.deleted=0
|
|
||||||
JOIN fl_loading_pick_item pick_item ON pick_item.loading_plan_item_id=plan_item.id
|
|
||||||
JOIN fl_loading_pick_task task ON task.id=pick_item.pick_task_id
|
|
||||||
AND task.loading_plan_id=plan_item.loading_plan_id
|
|
||||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
|
||||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
|
||||||
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
|
|
||||||
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
|
|
||||||
WHERE task.id=#{taskId} AND task.task_status='LOADING' AND booking.warehouse_id=#{warehouseId}
|
|
||||||
AND label.box_code=#{boxCode} AND label.batch_status='ACTIVE'
|
|
||||||
AND inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP'
|
|
||||||
AND pick_item.item_status<>'SKIPPED'
|
|
||||||
ORDER BY label.tenant_id,label.id
|
|
||||||
""")
|
|
||||||
List<CartonCandidate> selectCandidates(@Param("taskId") long taskId,
|
|
||||||
@Param("warehouseId") long warehouseId,
|
|
||||||
@Param("boxCode") String boxCode);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE pick_task_id=#{taskId} AND carton_label_id=#{labelId}")
|
|
||||||
LoadingWorkPalletCartonEntity selectBinding(@Param("taskId") long taskId,
|
|
||||||
@Param("labelId") long labelId);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE operation_id=#{operationId}")
|
|
||||||
LoadingWorkPalletCartonEntity selectByOperation(@Param("operationId") String operationId);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId} ORDER BY id")
|
|
||||||
List<LoadingWorkPalletCartonEntity> selectPalletCartons(@Param("palletId") long palletId);
|
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM fl_loading_work_pallet_carton WHERE work_pallet_id=#{palletId}")
|
|
||||||
int countPalletCartons(@Param("palletId") long palletId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT COUNT(*) FROM (
|
|
||||||
SELECT carton.pick_item_id
|
|
||||||
FROM fl_loading_work_pallet_carton carton
|
|
||||||
JOIN fl_loading_pick_item pick_item ON pick_item.id=carton.pick_item_id
|
|
||||||
WHERE carton.pick_task_id=#{taskId}
|
|
||||||
GROUP BY carton.pick_item_id,pick_item.picked_package_count
|
|
||||||
HAVING COUNT(*)>pick_item.picked_package_count
|
|
||||||
) unreconciled
|
|
||||||
""")
|
|
||||||
int countUnreconciled(@Param("taskId") long taskId);
|
|
||||||
|
|
||||||
@Update("""
|
|
||||||
UPDATE fl_loading_work_pallet_carton
|
|
||||||
SET work_pallet_id=#{targetPalletId},bound_by=#{operatorId},bound_by_name=#{operatorName},
|
|
||||||
operation_id=#{operationId},version=version+1
|
|
||||||
WHERE id=#{id} AND version=#{version}
|
|
||||||
""")
|
|
||||||
int move(@Param("id") long id, @Param("version") int version,
|
|
||||||
@Param("targetPalletId") long targetPalletId,
|
|
||||||
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName,
|
|
||||||
@Param("operationId") String operationId);
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
@Setter
|
|
||||||
class CartonCandidate {
|
|
||||||
private Long cartonLabelId;
|
|
||||||
private Long tenantId;
|
|
||||||
private Long inboundId;
|
|
||||||
private Long inboundDetailId;
|
|
||||||
private String boxCode;
|
|
||||||
private Long loadingPlanItemId;
|
|
||||||
private Long pickItemId;
|
|
||||||
private String skuCode;
|
|
||||||
private Integer unitsPerCarton;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package com.baiyue.firstleg.mapper;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
||||||
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
import org.apache.ibatis.annotations.Select;
|
|
||||||
import org.apache.ibatis.annotations.Update;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@Mapper
|
|
||||||
public interface LoadingWorkPalletMapper extends BaseMapper<LoadingWorkPalletEntity> {
|
|
||||||
@Select("""
|
|
||||||
SELECT task.id task_id,task.loading_plan_id,task.task_status,booking.warehouse_id,
|
|
||||||
assignment.assignee_id,
|
|
||||||
SUM(CASE WHEN inbound_order.fulfillment_type='ONE_CLICK_DROPSHIP' THEN 1 ELSE 0 END) dropship_item_count
|
|
||||||
FROM fl_loading_pick_task task
|
|
||||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
|
||||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
|
||||||
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id=task.id
|
|
||||||
JOIN fl_loading_plan_item plan_item ON plan_item.id=pick_item.loading_plan_item_id AND plan_item.deleted=0
|
|
||||||
JOIN fl_inbound_order inbound_order ON inbound_order.id=plan_item.inbound_id
|
|
||||||
AND inbound_order.tenant_id=plan_item.account_id AND inbound_order.deleted=0
|
|
||||||
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id=task.id
|
|
||||||
WHERE task.id=#{taskId} AND booking.warehouse_id=#{warehouseId}
|
|
||||||
GROUP BY task.id,task.loading_plan_id,task.task_status,booking.warehouse_id,assignment.assignee_id
|
|
||||||
""")
|
|
||||||
TaskContext selectTaskContext(@Param("taskId") long taskId, @Param("warehouseId") long warehouseId);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_work_pallet WHERE open_operation_id=#{operationId}")
|
|
||||||
LoadingWorkPalletEntity selectByOpenOperation(@Param("operationId") String operationId);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_work_pallet WHERE seal_operation_id=#{operationId}")
|
|
||||||
LoadingWorkPalletEntity selectBySealOperation(@Param("operationId") String operationId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT pallet.*
|
|
||||||
FROM fl_loading_work_pallet pallet
|
|
||||||
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
|
|
||||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
|
||||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
|
||||||
WHERE pallet.pick_task_id=#{taskId} AND pallet.id=#{palletId}
|
|
||||||
AND booking.warehouse_id=#{warehouseId}
|
|
||||||
""")
|
|
||||||
LoadingWorkPalletEntity selectScoped(@Param("taskId") long taskId,
|
|
||||||
@Param("palletId") long palletId,
|
|
||||||
@Param("warehouseId") long warehouseId);
|
|
||||||
|
|
||||||
@Select("""
|
|
||||||
SELECT pallet.*
|
|
||||||
FROM fl_loading_work_pallet pallet
|
|
||||||
JOIN fl_loading_pick_task task ON task.id=pallet.pick_task_id
|
|
||||||
JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id
|
|
||||||
JOIN fl_container_booking booking ON booking.id=plan.container_booking_id
|
|
||||||
WHERE pallet.pick_task_id=#{taskId} AND booking.warehouse_id=#{warehouseId}
|
|
||||||
AND pallet.pallet_status<>'CANCELLED'
|
|
||||||
ORDER BY pallet.id
|
|
||||||
""")
|
|
||||||
List<LoadingWorkPalletEntity> selectTaskPallets(@Param("taskId") long taskId,
|
|
||||||
@Param("warehouseId") long warehouseId);
|
|
||||||
|
|
||||||
@Update("""
|
|
||||||
UPDATE fl_loading_work_pallet
|
|
||||||
SET pallet_status='SEALED',seal_operation_id=#{operationId},sealed_by=#{operatorId},
|
|
||||||
sealed_by_name=#{operatorName},sealed_at=CURRENT_TIMESTAMP(3),version=version+1
|
|
||||||
WHERE id=#{palletId} AND pick_task_id=#{taskId} AND warehouse_id=#{warehouseId}
|
|
||||||
AND pallet_status='OPEN' AND version=#{expectedVersion}
|
|
||||||
""")
|
|
||||||
int seal(@Param("taskId") long taskId, @Param("palletId") long palletId,
|
|
||||||
@Param("warehouseId") long warehouseId, @Param("expectedVersion") int expectedVersion,
|
|
||||||
@Param("operationId") String operationId, @Param("operatorId") long operatorId,
|
|
||||||
@Param("operatorName") String operatorName);
|
|
||||||
|
|
||||||
record TaskContext(Long taskId, Long loadingPlanId, String taskStatus, Long warehouseId,
|
|
||||||
Long assigneeId, Integer dropshipItemCount) {}
|
|
||||||
}
|
|
||||||
+5
@@ -37,6 +37,11 @@ public class PlatformOceanBookingController {
|
|||||||
@RequestParam long originWarehouseId) {
|
@RequestParam long originWarehouseId) {
|
||||||
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(originWarehouseId));
|
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(originWarehouseId));
|
||||||
}
|
}
|
||||||
|
@GetMapping("/booking-agents") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
|
||||||
|
public ApiResponse<List<BookingAgentOption>> bookingAgents(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam String carrierCode) {
|
||||||
|
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.bookingAgents(carrierCode));
|
||||||
|
}
|
||||||
@GetMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
|
@GetMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
|
||||||
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
|
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
|
||||||
|
|
||||||
|
|||||||
@@ -100,4 +100,5 @@ public final class OceanBookingDtos {
|
|||||||
|
|
||||||
public record ChannelOption(Long priceBookVersionId, String channelCode, String channelName,
|
public record ChannelOption(Long priceBookVersionId, String channelCode, String channelName,
|
||||||
String priceModel, List<String> pricingTaxModes) {}
|
String priceModel, List<String> pricingTaxModes) {}
|
||||||
|
public record BookingAgentOption(Long supplierId, String name, List<String> carrierCodes) {}
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-9
@@ -6,6 +6,8 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
|
||||||
|
import com.baiyue.firstleg.client.WarehousePartnerClient;
|
||||||
|
import com.baiyue.firstleg.client.WarehouseSupplierClient;
|
||||||
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
|
||||||
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
|
||||||
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
|
||||||
@@ -23,6 +25,7 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import feign.FeignException;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@@ -44,6 +47,7 @@ public class OceanBookingService {
|
|||||||
@Resource private ObjectMapper objectMapper;
|
@Resource private ObjectMapper objectMapper;
|
||||||
@Resource private WarehouseSupplierSnapshotService suppliers;
|
@Resource private WarehouseSupplierSnapshotService suppliers;
|
||||||
@Resource private WarehouseMasterDataClient warehouses;
|
@Resource private WarehouseMasterDataClient warehouses;
|
||||||
|
@Resource private WarehousePartnerClient partners;
|
||||||
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
|
||||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||||
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
|
||||||
@@ -56,6 +60,14 @@ public class OceanBookingService {
|
|||||||
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
|
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<BookingAgentOption> bookingAgents(String carrierCode) {
|
||||||
|
String normalized = normalizeCarrierCode(carrierCode);
|
||||||
|
return suppliers.bookingCandidates(normalized).stream()
|
||||||
|
.filter(value -> "ACTIVE".equals(value.status()) && "BOOKING".equals(value.category())
|
||||||
|
&& value.carrierCodes() != null && value.carrierCodes().contains(normalized))
|
||||||
|
.map(value -> new BookingAgentOption(value.supplierId(), value.name(), value.carrierCodes())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
public PageResult<LinkCandidate> linkCandidates(LinkCandidateQuery query) {
|
||||||
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
return repository.linkCandidates(new LinkCandidateQuery(query.warehouseId(),
|
||||||
query.channelCode().trim(), query.containerType().trim(), query.page(), query.size()));
|
query.channelCode().trim(), query.containerType().trim(), query.page(), query.size()));
|
||||||
@@ -138,6 +150,7 @@ public class OceanBookingService {
|
|||||||
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
|
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
|
||||||
requireStatus(before, OceanBookingStatus.DRAFT);
|
requireStatus(before, OceanBookingStatus.DRAFT);
|
||||||
validateRelease(before, repository.parties(id), repository.requirements(id));
|
validateRelease(before, repository.parties(id), repository.requirements(id));
|
||||||
|
validateMasterData(before);
|
||||||
if (repository.transition(id, OceanBookingStatus.DRAFT.name(), OceanBookingStatus.RELEASED.name(),
|
if (repository.transition(id, OceanBookingStatus.DRAFT.name(), OceanBookingStatus.RELEASED.name(),
|
||||||
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
|
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
|
||||||
OceanBookingEntity after = required(id);
|
OceanBookingEntity after = required(id);
|
||||||
@@ -238,27 +251,27 @@ public class OceanBookingService {
|
|||||||
return value.trim();
|
return value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
private record ResolvedDraft(String warehouseName, String channelName, String bookingAgentName) {}
|
private record ResolvedDraft(String warehouseName, String channelName, String shippingCompanyName,
|
||||||
|
String carrierCode, String bookingAgentName) {}
|
||||||
private ResolvedDraft resolveDraft(DraftData d) {
|
private ResolvedDraft resolveDraft(DraftData d) {
|
||||||
String warehouseName = null; String channelName = null; String bookingAgentName = trim(d.bookingAgentName());
|
String warehouseName = null; String channelName = null;
|
||||||
if (d.warehouseId() != null && StringUtils.hasText(d.channelCode())) {
|
if (d.warehouseId() != null && StringUtils.hasText(d.channelCode())) {
|
||||||
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(d.warehouseId());
|
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(d.warehouseId());
|
||||||
warehouseName = warehouse.warehouseName();
|
warehouseName = warehouse.warehouseName();
|
||||||
channelName = requireActiveChannel(warehouse.id(), d.channelCode()).channelName();
|
channelName = requireActiveChannel(warehouse.id(), d.channelCode()).channelName();
|
||||||
}
|
}
|
||||||
if (d.bookingAgentId() != null) {
|
var carrier = resolveCarrier(d.shippingCompanyId(), d.shippingCompanyName(), d.carrierCode());
|
||||||
var snapshot = suppliers.booking(d.bookingAgentId(), d.bookingAgentName());
|
var agent = resolveBookingAgent(d.bookingAgentId(), d.bookingAgentName(), carrier == null ? null : carrier.carrierCode());
|
||||||
bookingAgentName = snapshot == null ? null : snapshot.name();
|
return new ResolvedDraft(warehouseName, channelName, carrier == null ? null : carrier.partnerName(),
|
||||||
} else if (StringUtils.hasText(d.bookingAgentName())) throw badRequest("订舱代理名称不能脱离供应商ID提交");
|
carrier == null ? null : carrier.carrierCode(), agent == null ? null : agent.name());
|
||||||
return new ResolvedDraft(warehouseName, channelName, bookingAgentName);
|
|
||||||
}
|
}
|
||||||
private static void copy(OceanBookingEntity v, DraftData d, ResolvedDraft resolved) {
|
private static void copy(OceanBookingEntity v, DraftData d, ResolvedDraft resolved) {
|
||||||
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
|
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
|
||||||
v.setChannelCode(trim(d.channelCode())); v.setChannelName(resolved.channelName());
|
v.setChannelCode(trim(d.channelCode())); v.setChannelName(resolved.channelName());
|
||||||
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(resolved.warehouseName());
|
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(resolved.warehouseName());
|
||||||
v.setBillOfLadingNo(trim(d.billOfLadingNo()));
|
v.setBillOfLadingNo(trim(d.billOfLadingNo()));
|
||||||
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
|
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(resolved.shippingCompanyName());
|
||||||
v.setCarrierCode(d.carrierCode() == null ? null : d.carrierCode().trim().toUpperCase());
|
v.setCarrierCode(resolved.carrierCode());
|
||||||
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(resolved.bookingAgentName());
|
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(resolved.bookingAgentName());
|
||||||
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
|
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
|
||||||
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
|
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
|
||||||
@@ -268,6 +281,58 @@ public class OceanBookingService {
|
|||||||
v.setRemark(trim(d.remark()));
|
v.setRemark(trim(d.remark()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateMasterData(OceanBookingEntity booking) {
|
||||||
|
resolveCarrier(booking.getShippingCompanyId(), booking.getShippingCompanyName(), booking.getCarrierCode());
|
||||||
|
resolveBookingAgent(booking.getBookingAgentId(), booking.getBookingAgentName(), booking.getCarrierCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private WarehousePartnerClient.Snapshot resolveCarrier(Long id, String suppliedName, String suppliedCode) {
|
||||||
|
if (id == null) {
|
||||||
|
if (StringUtils.hasText(suppliedName) || StringUtils.hasText(suppliedCode)) throw badRequest("船公司名称和编码不能脱离shippingCompanyId提交");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
var response = partners.active(internalToken, 0L, id);
|
||||||
|
var value = response == null ? null : response.data();
|
||||||
|
if (response == null || !response.successful() || value == null || value.id() != id
|
||||||
|
|| value.tenantId() != 0L || !"ACTIVE".equals(value.status())
|
||||||
|
|| value.types() == null || !value.types().contains("CARRIER")
|
||||||
|
|| !StringUtils.hasText(value.carrierCode())) throw badRequest("所选船公司不是启用中的CARRIER往来单位");
|
||||||
|
String code = normalizeCarrierCode(value.carrierCode());
|
||||||
|
if (StringUtils.hasText(suppliedCode) && !code.equals(normalizeCarrierCode(suppliedCode))) throw badRequest("船司标准编码与主数据快照不一致");
|
||||||
|
if (StringUtils.hasText(suppliedName) && !value.partnerName().equals(suppliedName.trim())) throw badRequest("船公司名称与主数据快照不一致");
|
||||||
|
return value;
|
||||||
|
} catch (FeignException exception) {
|
||||||
|
throw new BizException(com.baiyue.common.core.api.ResultCode.SERVICE_UNAVAILABLE, "船公司主数据服务暂不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WarehouseSupplierClient.Snapshot resolveBookingAgent(Long id, String name, String carrierCode) {
|
||||||
|
if (id == null) {
|
||||||
|
if (StringUtils.hasText(name)) throw badRequest("订舱代理名称不能脱离供应商ID提交");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
WarehouseSupplierClient.Snapshot snapshot;
|
||||||
|
try {
|
||||||
|
snapshot = suppliers.booking(id, name);
|
||||||
|
} catch (BizException exception) {
|
||||||
|
if (exception.getResultCode() == ResultCode.SERVICE_UNAVAILABLE) throw exception;
|
||||||
|
throw badRequest("所选订舱代理未关联当前船公司");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(carrierCode) || snapshot.carrierCodes() == null
|
||||||
|
|| !snapshot.carrierCodes().contains(normalizeCarrierCode(carrierCode))) {
|
||||||
|
throw badRequest("所选订舱代理未关联当前船公司");
|
||||||
|
}
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String normalizeCarrierCode(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) throw badRequest("carrierCode不能为空");
|
||||||
|
String normalized = value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
|
if (!CARRIER_CODES.contains(normalized)) throw badRequest("carrierCode仅支持MATSON、SML、ZIM、COSCO、EMC、HEDE或OTHER");
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { if (requests == null) return List.of(); return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
|
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { if (requests == null) return List.of(); return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
|
||||||
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { if (requests == null) return List.of(); int[] sort = {0}; return requests.stream().map(r -> { var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg()); OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(specification.containerType()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
|
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { if (requests == null) return List.of(); int[] sort = {0}; return requests.stream().map(r -> { var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg()); OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(specification.containerType()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
|
||||||
private WarehouseMasterDataClient.Warehouse requireDomesticTransitWarehouse(long warehouseId) {
|
private WarehouseMasterDataClient.Warehouse requireDomesticTransitWarehouse(long warehouseId) {
|
||||||
|
|||||||
@@ -13,9 +13,12 @@ import org.springframework.stereotype.Repository;
|
|||||||
public class ContainerRepository {
|
public class ContainerRepository {
|
||||||
@Resource private ContainerBookingMapper mapper;
|
@Resource private ContainerBookingMapper mapper;
|
||||||
public PageResult<ContainerBookingEntity> page(Query query) {
|
public PageResult<ContainerBookingEntity> page(Query query) {
|
||||||
return PageResult.from(mapper.selectBookingPage(new Page<>(query.page(), query.size()), query));
|
Page<ContainerBookingEntity> result = mapper.selectBookingPage(
|
||||||
|
new Page<>(query.page(), query.size()), query);
|
||||||
|
result.getRecords().forEach(this::resolveLoadingTime);
|
||||||
|
return PageResult.from(result);
|
||||||
}
|
}
|
||||||
public ContainerBookingEntity find(long id) { return mapper.selectBooking(id); }
|
public ContainerBookingEntity find(long id) { return resolveLoadingTime(mapper.selectBooking(id)); }
|
||||||
public ContainerBookingEntity findForUpdate(long id) { return mapper.selectBookingForUpdate(id); }
|
public ContainerBookingEntity findForUpdate(long id) { return mapper.selectBookingForUpdate(id); }
|
||||||
public ContainerBookingEntity findActiveForUpdate(long id) { return mapper.selectActiveBookingForUpdate(id); }
|
public ContainerBookingEntity findActiveForUpdate(long id) { return mapper.selectActiveBookingForUpdate(id); }
|
||||||
public boolean containerNoExists(String no, Long excludedId) {
|
public boolean containerNoExists(String no, Long excludedId) {
|
||||||
@@ -56,4 +59,12 @@ public class ContainerRepository {
|
|||||||
return mapper.syncReleasedTransport(oceanBookingId, billOfLadingNo, vesselName, voyageNo,
|
return mapper.syncReleasedTransport(oceanBookingId, billOfLadingNo, vesselName, voyageNo,
|
||||||
estimatedSailingAt, operatorId, operatorName);
|
estimatedSailingAt, operatorId, operatorName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ContainerBookingEntity resolveLoadingTime(ContainerBookingEntity value) {
|
||||||
|
if (value != null && value.getLoadingAt() == null
|
||||||
|
&& "COMPLETED".equals(value.getPlanStatus())) {
|
||||||
|
value.setLoadingAt(value.getCompletedLoadingAt());
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ public class FbaPricingRepository {
|
|||||||
public int markInboundConfirmed(long tenantId, long inboundId) {
|
public int markInboundConfirmed(long tenantId, long inboundId) {
|
||||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
return inbounds.update(null, new LambdaUpdateWrapper<InboundPricingEntity>()
|
||||||
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
.eq(InboundPricingEntity::getTenantId, tenantId).eq(InboundPricingEntity::getId, inboundId)
|
||||||
.in(InboundPricingEntity::getProcessStatus, List.of("FEE_CONFIRMING", "WAIT_SPLIT"))
|
.in(InboundPricingEntity::getProcessStatus,
|
||||||
|
List.of("FEE_CONFIRMING", "WAIT_SPLIT", "LOADING", "LOADED", "DELIVERED"))
|
||||||
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
.eq(InboundPricingEntity::getFeeStatus, "CALCULATED")
|
||||||
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
.set(InboundPricingEntity::getFeeStatus, "CONFIRMED"));
|
||||||
}
|
}
|
||||||
|
|||||||
-10
@@ -62,16 +62,6 @@ public class InboundCartonLabelRepository {
|
|||||||
.set(InboundCartonLabelEntity::getCargoFingerprint, fingerprint));
|
.set(InboundCartonLabelEntity::getCargoFingerprint, fingerprint));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void initializeLegacySku(long tenantId, long inboundId, long inboundDetailId, String skuCode) {
|
|
||||||
if (skuCode == null || skuCode.isBlank()) return;
|
|
||||||
labels.update(null, new LambdaUpdateWrapper<InboundCartonLabelEntity>()
|
|
||||||
.eq(InboundCartonLabelEntity::getTenantId, tenantId)
|
|
||||||
.eq(InboundCartonLabelEntity::getInboundId, inboundId)
|
|
||||||
.eq(InboundCartonLabelEntity::getInboundDetailId, inboundDetailId)
|
|
||||||
.isNull(InboundCartonLabelEntity::getSkuCode)
|
|
||||||
.set(InboundCartonLabelEntity::getSkuCode, skuCode.trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean initializeOriginalPackageCount(long tenantId, long inboundId, int packageCount) {
|
public boolean initializeOriginalPackageCount(long tenantId, long inboundId, int packageCount) {
|
||||||
return inbounds.update(null, new LambdaUpdateWrapper<InboundOrderEntity>()
|
return inbounds.update(null, new LambdaUpdateWrapper<InboundOrderEntity>()
|
||||||
.eq(InboundOrderEntity::getTenantId, tenantId)
|
.eq(InboundOrderEntity::getTenantId, tenantId)
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
package com.baiyue.firstleg.repository;
|
||||||
|
|
||||||
|
import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity;
|
||||||
|
import com.baiyue.firstleg.mapper.LoadingCompletionCommandMapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class LoadingCompletionCommandRepository {
|
||||||
|
@Resource private LoadingCompletionCommandMapper mapper;
|
||||||
|
|
||||||
|
public int insert(LoadingCompletionCommandEntity command) { return mapper.insertCommand(command); }
|
||||||
|
public LoadingCompletionCommandEntity operation(String operationId) { return mapper.findByOperation(operationId); }
|
||||||
|
public LoadingCompletionCommandEntity active(long taskId) { return mapper.findActiveByTask(taskId); }
|
||||||
|
public LoadingCompletionCommandEntity latest(long taskId) { return mapper.findLatestByTask(taskId); }
|
||||||
|
public List<LoadingCompletionCommandEntity> due(int limit) { return mapper.findDue(limit); }
|
||||||
|
public boolean claim(long id, String worker, LocalDateTime leaseUntil) { return mapper.claim(id, worker, leaseUntil) == 1; }
|
||||||
|
public boolean renew(long id, String worker, LocalDateTime leaseUntil) { return mapper.renew(id, worker, leaseUntil) == 1; }
|
||||||
|
public boolean complete(long id, String worker) { return mapper.complete(id, worker) == 1; }
|
||||||
|
public boolean fail(long id, String worker, String status, LocalDateTime nextRetryAt, String stage,
|
||||||
|
String code, String requestId, String traceId) {
|
||||||
|
return mapper.fail(id, worker, status, nextRetryAt, stage, code, requestId, traceId) == 1;
|
||||||
|
}
|
||||||
|
public boolean resume(long id) { return mapper.resume(id) == 1; }
|
||||||
|
public String checkpoint(long commandId, long tenantId, long inboundId) {
|
||||||
|
return mapper.checkpoint(commandId, tenantId, inboundId);
|
||||||
|
}
|
||||||
|
public void checkpoint(long commandId, long tenantId, long inboundId, int sequence, String stage) {
|
||||||
|
mapper.checkpointCompleted(commandId, tenantId, inboundId, sequence, stage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,7 +188,7 @@ public class LoadingListRepository {
|
|||||||
|
|
||||||
private List<History> histories(long planId) {
|
private List<History> histories(long planId) {
|
||||||
return mapper.selectHistories(planId).stream().map(row -> new History(row.getHistoryId(), row.getItemId(),
|
return mapper.selectHistories(planId).stream().map(row -> new History(row.getHistoryId(), row.getItemId(),
|
||||||
row.getActionType(), row.getFromStatus(), row.getToStatus(), row.getBeforeSnapshot(),
|
row.getOperationId(), row.getActionType(), row.getFromStatus(), row.getToStatus(), row.getBeforeSnapshot(),
|
||||||
row.getAfterSnapshot(), row.getOperatorId(), row.getOperatorName(), row.getOperationReason(),
|
row.getAfterSnapshot(), row.getOperatorId(), row.getOperatorName(), row.getOperationReason(),
|
||||||
row.getOccurredAt())).toList();
|
row.getOccurredAt())).toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,18 @@ public class LoadingPickRepository {
|
|||||||
return tasks.touch(taskId, expectedVersion);
|
return tasks.touch(taskId, expectedVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int acceptCompletion(long taskId, int expectedVersion) {
|
||||||
|
return tasks.acceptCompletion(taskId, expectedVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int completeAccepted(long taskId, long operatorId, String operatorName) {
|
||||||
|
return tasks.completeAccepted(taskId, operatorId, operatorName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int failCompletion(long taskId) { return tasks.failCompletion(taskId); }
|
||||||
|
|
||||||
|
public int resumeCompletion(long taskId) { return tasks.resumeCompletion(taskId); }
|
||||||
|
|
||||||
public LoadingPickTaskEntity taskByPlan(long planId) {
|
public LoadingPickTaskEntity taskByPlan(long planId) {
|
||||||
return tasks.selectOne(new LambdaQueryWrapper<LoadingPickTaskEntity>()
|
return tasks.selectOne(new LambdaQueryWrapper<LoadingPickTaskEntity>()
|
||||||
.eq(LoadingPickTaskEntity::getLoadingPlanId, planId));
|
.eq(LoadingPickTaskEntity::getLoadingPlanId, planId));
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ import org.springframework.stereotype.Service;
|
|||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ContainerService {
|
public class ContainerService {
|
||||||
@@ -151,31 +155,87 @@ public class ContainerService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ContainerBookingEntity updateTransport(long id, long operatorId, String operatorName,
|
public ContainerBookingEntity updateTransport(long id, long operatorId, String operatorName,
|
||||||
TransportUpdateRequest request) {
|
String idempotencyKey, TransportUpdateRequest request) {
|
||||||
|
String operationId = transportOperationId(id, idempotencyKey);
|
||||||
|
String containerNo = trim(request.containerNo());
|
||||||
|
String voyageNo = trim(request.voyageNo());
|
||||||
|
String requestHash = sha256("TRANSPORT|" + id + '|' + request.version() + '|'
|
||||||
|
+ Objects.toString(containerNo, "") + '|' + Objects.toString(voyageNo, "") + '|'
|
||||||
|
+ request.reason().trim());
|
||||||
|
ContainerTransportHistoryEntity replay = transportHistories == null ? null
|
||||||
|
: transportHistories.selectOperation(id, operatorId, operationId);
|
||||||
|
if (replay != null) {
|
||||||
|
if (!requestHash.equals(replay.getRequestHash())) {
|
||||||
|
throw conflict("Idempotency-Key已用于不同运输字段请求");
|
||||||
|
}
|
||||||
|
return required(id);
|
||||||
|
}
|
||||||
ContainerBookingEntity current = required(id);
|
ContainerBookingEntity current = required(id);
|
||||||
requireVersion(current, request.version());
|
requireVersion(current, request.version());
|
||||||
if (status(current) == SmartLoadingStatus.COMPLETED) throw conflict("装柜完成后货柜全部只读,不能修改");
|
if (status(current) == SmartLoadingStatus.COMPLETED) throw conflict("装柜完成后货柜全部只读,不能修改");
|
||||||
String containerNo = trim(request.containerNo());
|
replay = transportHistories == null ? null
|
||||||
|
: transportHistories.selectOperation(id, operatorId, operationId);
|
||||||
|
if (replay != null) {
|
||||||
|
if (!requestHash.equals(replay.getRequestHash())) {
|
||||||
|
throw conflict("Idempotency-Key已用于不同运输字段请求");
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
if (Objects.equals(current.getContainerNo(), containerNo)
|
||||||
|
&& Objects.equals(current.getVoyageNo(), voyageNo)) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(id);
|
LoadingPlanEntity plan = smartLoadingRepository.planByBooking(id);
|
||||||
if (containerNo == null && plan != null && loadingPickRepository.taskByPlan(plan.getId()) != null) {
|
if (containerNo == null && plan != null && loadingPickRepository.taskByPlan(plan.getId()) != null) {
|
||||||
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
||||||
}
|
}
|
||||||
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
|
if (repository.containerNoExists(containerNo, id)) throw conflict("集装箱号已存在");
|
||||||
try {
|
try {
|
||||||
if (repository.updateTransport(id, containerNo, trim(request.voyageNo()), request.version(),
|
if (repository.updateTransport(id, containerNo, voyageNo, request.version(),
|
||||||
operatorId, operatorName) != 1) throw conflict("货柜状态或版本已变化,请刷新后重试");
|
operatorId, operatorName) != 1) {
|
||||||
|
ContainerTransportHistoryEntity concurrent = transportHistories == null ? null
|
||||||
|
: transportHistories.selectOperation(id, operatorId, operationId);
|
||||||
|
if (concurrent != null && requestHash.equals(concurrent.getRequestHash())) {
|
||||||
|
return required(id);
|
||||||
|
}
|
||||||
|
throw conflict("货柜状态或版本已变化,请刷新后重试");
|
||||||
|
}
|
||||||
} catch (DuplicateKeyException ex) { throw conflict("集装箱号已存在"); }
|
} catch (DuplicateKeyException ex) { throw conflict("集装箱号已存在"); }
|
||||||
ContainerTransportHistoryEntity history = new ContainerTransportHistoryEntity();
|
ContainerTransportHistoryEntity history = new ContainerTransportHistoryEntity();
|
||||||
history.setContainerBookingId(id); history.setBeforeContainerNo(current.getContainerNo());
|
history.setContainerBookingId(id); history.setBeforeContainerNo(current.getContainerNo());
|
||||||
history.setAfterContainerNo(containerNo); history.setBeforeVoyageNo(current.getVoyageNo());
|
history.setAfterContainerNo(containerNo); history.setBeforeVoyageNo(current.getVoyageNo());
|
||||||
history.setAfterVoyageNo(trim(request.voyageNo())); history.setReason(request.reason().trim());
|
history.setAfterVoyageNo(voyageNo); history.setOperationId(operationId);
|
||||||
|
history.setRequestHash(requestHash); history.setReason(request.reason().trim());
|
||||||
history.setOperatorId(operatorId); history.setOperatorName(operatorName);
|
history.setOperatorId(operatorId); history.setOperatorName(operatorName);
|
||||||
if (transportHistories != null) transportHistories.insert(history);
|
try {
|
||||||
|
if (transportHistories != null) transportHistories.insert(history);
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
ContainerTransportHistoryEntity concurrent = transportHistories.selectOperation(id, operatorId, operationId);
|
||||||
|
if (concurrent == null || !requestHash.equals(concurrent.getRequestHash())) {
|
||||||
|
throw conflict("Idempotency-Key已被并发用于不同运输字段请求");
|
||||||
|
}
|
||||||
|
}
|
||||||
ContainerBookingEntity updated = required(id);
|
ContainerBookingEntity updated = required(id);
|
||||||
loadingPickService.beginIfReady(updated.getId(), operatorId, operatorName);
|
loadingPickService.beginIfReady(updated.getId(), operatorId, operatorName);
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String transportOperationId(long id, String value) {
|
||||||
|
String key = trim(value);
|
||||||
|
if (key == null) throw new BizException(ResultCode.BAD_REQUEST, "Idempotency-Key不能为空");
|
||||||
|
if (key.length() > 128) throw new BizException(ResultCode.BAD_REQUEST, "Idempotency-Key长度不能超过128");
|
||||||
|
return sha256("CONTAINER_TRANSPORT|" + id + '|' + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String sha256(String value) {
|
||||||
|
try {
|
||||||
|
return java.util.HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
|
||||||
|
.digest(value.getBytes(StandardCharsets.UTF_8)));
|
||||||
|
} catch (NoSuchAlgorithmException exception) {
|
||||||
|
throw new IllegalStateException("SHA-256不可用", exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ContainerBookingEntity required(long id) {
|
private ContainerBookingEntity required(long id) {
|
||||||
ContainerBookingEntity value = repository.find(id);
|
ContainerBookingEntity value = repository.find(id);
|
||||||
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
if (value == null) throw new BizException(ResultCode.NOT_FOUND, "货柜不存在");
|
||||||
|
|||||||
@@ -50,14 +50,6 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
|||||||
throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许首次生成箱号");
|
throw new BizException(ResultCode.CONFLICT, "当前入库单状态不允许首次生成箱号");
|
||||||
}
|
}
|
||||||
List<OrderCargoClient.CargoLabelBinding> cargoes = loadCargoes(inbound);
|
List<OrderCargoClient.CargoLabelBinding> cargoes = loadCargoes(inbound);
|
||||||
if ("ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())
|
|
||||||
&& cargoes.stream().anyMatch(cargo -> cargo.skuNo() == null || cargo.skuNo().isBlank())) {
|
|
||||||
throw new BizException(ResultCode.VALIDATION_FAILED, "一件代发货物明细缺少SKU,不能生成箱唛");
|
|
||||||
}
|
|
||||||
existing.forEach(label -> cargoes.stream()
|
|
||||||
.filter(cargo -> java.util.Objects.equals(cargo.cargoId(), label.getInboundDetailId()))
|
|
||||||
.findFirst().ifPresent(cargo -> repository.initializeLegacySku(tenantId, inboundId,
|
|
||||||
label.getInboundDetailId(), cargo.skuNo())));
|
|
||||||
String fingerprint = fingerprint(cargoes);
|
String fingerprint = fingerprint(cargoes);
|
||||||
if (!existing.isEmpty()) {
|
if (!existing.isEmpty()) {
|
||||||
String existingFingerprint = consistent.get(0).getCargoFingerprint();
|
String existingFingerprint = consistent.get(0).getCargoFingerprint();
|
||||||
@@ -210,7 +202,6 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
|||||||
label.setInboundDetailId(cargo.cargoId());
|
label.setInboundDetailId(cargo.cargoId());
|
||||||
label.setCargoItemNo(cargo.itemNo());
|
label.setCargoItemNo(cargo.itemNo());
|
||||||
label.setCargoName(cargo.cargoName());
|
label.setCargoName(cargo.cargoName());
|
||||||
label.setSkuCode(cargo.skuNo());
|
|
||||||
label.setDetailPackageCount(cargo.packageCount());
|
label.setDetailPackageCount(cargo.packageCount());
|
||||||
label.setInboundCode(inbound.getInboundCode());
|
label.setInboundCode(inbound.getInboundCode());
|
||||||
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
|
label.setBoxCode(inbound.getInboundCode() + "#" + total + "-" + index);
|
||||||
@@ -265,8 +256,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
|
|||||||
|
|
||||||
private String fingerprint(List<OrderCargoClient.CargoLabelBinding> cargoes) {
|
private String fingerprint(List<OrderCargoClient.CargoLabelBinding> cargoes) {
|
||||||
String source = cargoes.stream().map(cargo -> String.join("|", String.valueOf(cargo.itemNo()),
|
String source = cargoes.stream().map(cargo -> String.join("|", String.valueOf(cargo.itemNo()),
|
||||||
cargo.cargoName() == null ? "" : cargo.cargoName().trim(),
|
cargo.cargoName() == null ? "" : cargo.cargoName().trim(), String.valueOf(cargo.packageCount()),
|
||||||
cargo.skuNo() == null ? "" : cargo.skuNo().trim(), String.valueOf(cargo.packageCount()),
|
|
||||||
decimalOrEmpty(cargo.lengthCm()), decimalOrEmpty(cargo.widthCm()), decimalOrEmpty(cargo.heightCm())))
|
decimalOrEmpty(cargo.lengthCm()), decimalOrEmpty(cargo.widthCm()), decimalOrEmpty(cargo.heightCm())))
|
||||||
.collect(java.util.stream.Collectors.joining("\n"));
|
.collect(java.util.stream.Collectors.joining("\n"));
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import jakarta.annotation.Resource;
|
|||||||
|
|
||||||
import com.baiyue.common.core.api.ResultCode;
|
import com.baiyue.common.core.api.ResultCode;
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.common.core.exception.BizException;
|
import com.baiyue.common.core.exception.BizException;
|
||||||
import com.baiyue.firstleg.client.OrderCargoClient;
|
import com.baiyue.firstleg.client.OrderCargoClient;
|
||||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||||
@@ -73,14 +74,16 @@ public class InboundMeasurementService {
|
|||||||
return toDetail(inbound, measurement, items, weights, review, declaration);
|
return toDetail(inbound, measurement, items, weights, review, declaration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
public PageResult<InboundMeasurementResponses.PalletSnapshot> weightPalletOptions(
|
||||||
long tenantId, long inboundId) {
|
long tenantId, long inboundId, long page, long size, String keyword) {
|
||||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||||
return loadPalletSnapshots(inbound).stream()
|
PageResult<WarehousePalletLocationClient.PalletSnapshot> source =
|
||||||
|
loadPalletSnapshotPage(inbound, page, size, keyword);
|
||||||
|
return new PageResult<>(source.records().stream()
|
||||||
.map(snapshot -> new InboundMeasurementResponses.PalletSnapshot(
|
.map(snapshot -> new InboundMeasurementResponses.PalletSnapshot(
|
||||||
snapshot.palletId(), snapshot.palletCode(), snapshot.palletType(),
|
snapshot.palletId(), snapshot.palletCode(), snapshot.palletType(),
|
||||||
snapshot.capacityCbm(), snapshot.tareWeightKg()))
|
snapshot.capacityCbm(), snapshot.tareWeightKg()))
|
||||||
.toList();
|
.toList(), source.page(), source.size(), source.total(), source.pages());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -175,11 +178,8 @@ public class InboundMeasurementService {
|
|||||||
if (repository.findWeightByFile(tenantId, inboundId, request.fileId()) != null) {
|
if (repository.findWeightByFile(tenantId, inboundId, request.fileId()) != null) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "同一称重图片不能重复计重");
|
throw new BizException(ResultCode.CONFLICT, "同一称重图片不能重复计重");
|
||||||
}
|
}
|
||||||
WarehousePalletLocationClient.PalletSnapshot pallet = loadPalletSnapshots(inbound).stream()
|
WarehousePalletLocationClient.PalletSnapshot pallet = loadPalletSnapshot(
|
||||||
.filter(snapshot -> snapshot.palletId() == request.palletId())
|
inbound, request.palletId());
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new BizException(ResultCode.VALIDATION_FAILED,
|
|
||||||
"所选托盘不属于当前入库仓库或已被删除"));
|
|
||||||
validatePalletSnapshot(pallet);
|
validatePalletSnapshot(pallet);
|
||||||
String unit = rawOcrUnit(request.ocrWeightUnit());
|
String unit = rawOcrUnit(request.ocrWeightUnit());
|
||||||
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
InboundWeightResultEntity weight = new InboundWeightResultEntity();
|
||||||
@@ -527,23 +527,43 @@ public class InboundMeasurementService {
|
|||||||
weight.getAdjustedAt(), weight.getConfirmedBy(), weight.getConfirmedAt(), weight.getVersion());
|
weight.getAdjustedAt(), weight.getConfirmedBy(), weight.getConfirmedAt(), weight.getVersion());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<WarehousePalletLocationClient.PalletSnapshot> loadPalletSnapshots(
|
private PageResult<WarehousePalletLocationClient.PalletSnapshot> loadPalletSnapshotPage(
|
||||||
InboundOrderEntity inbound) {
|
InboundOrderEntity inbound, long page, long size, String keyword) {
|
||||||
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
|
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效仓库,无法选择称重托盘");
|
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效仓库,无法选择称重托盘");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ApiResponse<List<WarehousePalletLocationClient.PalletSnapshot>> response =
|
ApiResponse<PageResult<WarehousePalletLocationClient.PalletSnapshot>> response =
|
||||||
warehousePallets.palletSnapshots(internalToken, inbound.getWarehouseId());
|
warehousePallets.palletSnapshots(internalToken, inbound.getWarehouseId(), page, size, keyword);
|
||||||
if (response == null || !response.successful() || response.data() == null) {
|
if (response == null || !response.successful() || response.data() == null) {
|
||||||
String message = response == null || !StringUtils.hasText(response.message())
|
String message = response == null || !StringUtils.hasText(response.message())
|
||||||
? "托盘快照查询失败" : response.message();
|
? "托盘候选查询失败" : response.message();
|
||||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||||
: ResultCode.of(response.code()), message);
|
: ResultCode.of(response.code()), message);
|
||||||
}
|
}
|
||||||
return response.data();
|
return response.data();
|
||||||
} catch (FeignException exception) {
|
} catch (FeignException exception) {
|
||||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法查询托盘快照");
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法查询托盘候选");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private WarehousePalletLocationClient.PalletSnapshot loadPalletSnapshot(
|
||||||
|
InboundOrderEntity inbound, long palletId) {
|
||||||
|
if (inbound.getWarehouseId() == null || inbound.getWarehouseId() <= 0) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "入库单未绑定有效仓库,无法选择称重托盘");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ApiResponse<WarehousePalletLocationClient.PalletSnapshot> response = warehousePallets.palletSnapshot(
|
||||||
|
internalToken, palletId, inbound.getWarehouseId());
|
||||||
|
if (response == null || !response.successful() || response.data() == null) {
|
||||||
|
String message = response == null || !StringUtils.hasText(response.message())
|
||||||
|
? "所选托盘当前不可用于该入库仓库" : response.message();
|
||||||
|
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||||
|
: ResultCode.of(response.code()), message);
|
||||||
|
}
|
||||||
|
return response.data();
|
||||||
|
} catch (FeignException exception) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库服务暂不可用,无法校验托盘资格");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-67
@@ -37,7 +37,7 @@ import java.util.Map;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
/** 在装柜完成事务内按客户写跨区业务 Outbox。 */
|
/** 在装柜完成事务内按客户及可信客户名称快照写跨区业务 Outbox。 */
|
||||||
@Service
|
@Service
|
||||||
public class LoadingCompletedOutboxService {
|
public class LoadingCompletedOutboxService {
|
||||||
private static final long HANDOVER_VERSION = 2L;
|
private static final long HANDOVER_VERSION = 2L;
|
||||||
@@ -49,34 +49,15 @@ public class LoadingCompletedOutboxService {
|
|||||||
public void publish(long planId) {
|
public void publish(long planId) {
|
||||||
List<Row> rows = snapshotMapper.rows(planId);
|
List<Row> rows = snapshotMapper.rows(planId);
|
||||||
if (rows.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜计划没有最终实装入仓单,不能生成尾程交接事件");
|
if (rows.isEmpty()) throw new BizException(ResultCode.CONFLICT, "装柜计划没有最终实装入仓单,不能生成尾程交接事件");
|
||||||
|
validateCustomerSnapshots(rows);
|
||||||
Map<Long, List<String>> cartonMarks = cartonLabelPools(rows);
|
Map<Long, List<String>> cartonMarks = cartonLabelPools(rows);
|
||||||
List<PalletRow> palletRows = palletRows(planId);
|
List<PalletRow> palletRows = snapshotMapper.palletRows(planId);
|
||||||
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId,
|
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId,
|
||||||
java.util.TreeMap::new, java.util.stream.Collectors.toList()))
|
java.util.TreeMap::new, java.util.stream.Collectors.toList()))
|
||||||
.forEach((accountId, accountRows) -> insert(envelope(accountId, accountRows,
|
.forEach((accountId, accountRows) -> insert(envelope(accountId, accountRows,
|
||||||
palletRows.stream().filter(item -> accountId.equals(item.getAccountId())).toList(), cartonMarks)));
|
palletRows.stream().filter(item -> accountId.equals(item.getAccountId())).toList(), cartonMarks)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 已开展逐箱打托的入仓单使用现场事实;未开展的存量入仓单继续兼容原托盘分配快照。 */
|
|
||||||
private List<PalletRow> palletRows(long planId) {
|
|
||||||
int workPalletCount = snapshotMapper.workPalletCount(planId);
|
|
||||||
if (snapshotMapper.openWorkPalletCount(planId) > 0) {
|
|
||||||
throw new BizException(ResultCode.CONFLICT, "一件代发存在未封托的现场托盘,不能完成装柜交接");
|
|
||||||
}
|
|
||||||
List<PalletRow> actual = snapshotMapper.actualPalletRows(planId);
|
|
||||||
if (workPalletCount > 0 && actual.isEmpty()) {
|
|
||||||
throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘没有有效箱唛,不能完成装柜交接");
|
|
||||||
}
|
|
||||||
if (actual.isEmpty()) return snapshotMapper.palletRows(planId);
|
|
||||||
Set<InboundPalletKey> actualInbounds = actual.stream().map(row ->
|
|
||||||
new InboundPalletKey(row.getAccountId(), row.getSourceInboundId())).collect(java.util.stream.Collectors.toSet());
|
|
||||||
List<PalletRow> result = new ArrayList<>(actual);
|
|
||||||
snapshotMapper.palletRows(planId).stream()
|
|
||||||
.filter(row -> !actualInbounds.contains(new InboundPalletKey(row.getAccountId(), row.getSourceInboundId())))
|
|
||||||
.forEach(result::add);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private EventEnvelope envelope(long accountId, List<Row> rows, List<PalletRow> palletRows,
|
private EventEnvelope envelope(long accountId, List<Row> rows, List<PalletRow> palletRows,
|
||||||
Map<Long, List<String>> cartonMarks) {
|
Map<Long, List<String>> cartonMarks) {
|
||||||
Row first = rows.get(0);
|
Row first = rows.get(0);
|
||||||
@@ -87,7 +68,8 @@ public class LoadingCompletedOutboxService {
|
|||||||
.filter(item -> Objects.equals(item.getSourceInboundId(), value.get(0).getSourceInboundId())).toList(), cartonMarks)).toList();
|
.filter(item -> Objects.equals(item.getSourceInboundId(), value.get(0).getSourceInboundId())).toList(), cartonMarks)).toList();
|
||||||
var payload = new FirstLegLoadingPlanCompletedV2(String.valueOf(first.getLoadingPlanId()),
|
var payload = new FirstLegLoadingPlanCompletedV2(String.valueOf(first.getLoadingPlanId()),
|
||||||
first.getPlanNo(), string(first.getContainerBookingId()), first.getContainerNo(), first.getCabinetNo(),
|
first.getPlanNo(), string(first.getContainerBookingId()), first.getContainerNo(), first.getCabinetNo(),
|
||||||
string(first.getWarehouseId()), first.getWarehouseName(), orders);
|
string(first.getWarehouseId()), first.getWarehouseName(), iso(first.getEstimatedPortArrivalAt()),
|
||||||
|
iso(first.getActualPortArrivalAt()), iso(first.getEstimatedWarehouseArrivalAt()), orders);
|
||||||
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
JsonNode payloadNode = objectMapper.valueToTree(payload);
|
||||||
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
String hash = EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payloadNode));
|
||||||
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + first.getLoadingPlanId() + ':' + accountId + ':' + HANDOVER_VERSION;
|
String eventKey = "FIRSTLEG_LOADING_COMPLETED:" + first.getLoadingPlanId() + ':' + accountId + ':' + HANDOVER_VERSION;
|
||||||
@@ -122,12 +104,38 @@ public class LoadingCompletedOutboxService {
|
|||||||
string(first.getRootInboundId()), string(first.getSourceOrderId()), first.getSourceOrderNo(),
|
string(first.getRootInboundId()), string(first.getSourceOrderId()), first.getSourceOrderNo(),
|
||||||
string(first.getRootOrderId()), first.getInboundCode(), string(first.getDestinationWarehouseId()),
|
string(first.getRootOrderId()), first.getInboundCode(), string(first.getDestinationWarehouseId()),
|
||||||
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), sumInt(sorted, Row::getPackageCount),
|
first.getDestinationWarehouseCode(), first.getDestinationWarehouseName(), sumInt(sorted, Row::getPackageCount),
|
||||||
sumNullableInt(sorted, Row::getProductQuantity), "ONE_CLICK_DROPSHIP".equals(orderType)
|
sumNullableInt(sorted, Row::getProductQuantity), actualPalletCount(palletRows),
|
||||||
? pallets.size() : 0, sum(sorted, Row::getGrossWeightKg), sum(sorted, Row::getNetWeightKg),
|
sum(sorted, Row::getGrossWeightKg), sum(sorted, Row::getNetWeightKg),
|
||||||
sum(sorted, Row::getVolumeCbm), items, string(first.getAccountId()), orderType, pallets,
|
sum(sorted, Row::getVolumeCbm), sum(sorted, Row::getDimensionalWeightKg), items,
|
||||||
|
string(first.getAccountId()), first.getCustomerName().trim(), orderType, pallets,
|
||||||
destinationType(sorted));
|
destinationType(sorted));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 只按 Warehouse 已分配的物理 palletId 计数;无分配事实时保持 null。 */
|
||||||
|
private static Integer actualPalletCount(List<PalletRow> palletRows) {
|
||||||
|
Set<Long> palletIds = palletRows.stream().map(PalletRow::getPalletId)
|
||||||
|
.filter(Objects::nonNull).filter(value -> value > 0).collect(java.util.stream.Collectors.toSet());
|
||||||
|
return palletIds.isEmpty() ? null : palletIds.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateCustomerSnapshots(List<Row> rows) {
|
||||||
|
for (Row row : rows) {
|
||||||
|
if (row.getAccountId() == null || !Objects.equals(row.getAccountId(), row.getBookingCustomerId())) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "装柜计划客户与柜次客户快照不一致,不能生成尾程交接事件");
|
||||||
|
}
|
||||||
|
if (row.getCustomerName() == null || row.getCustomerName().isBlank()) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "柜次缺少客户名称快照,不能生成尾程交接事件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId)).forEach((accountId, values) -> {
|
||||||
|
Set<String> names = values.stream().map(Row::getCustomerName).map(String::trim)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
if (names.size() != 1) {
|
||||||
|
throw new BizException(ResultCode.CONFLICT, "同一客户事件存在多个客户名称快照");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
|
/** 国内按实装箱数从可用标签池稳定随机抽取箱唛,重复发布同一计划时结果不变。 */
|
||||||
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
|
private Map<Long, List<String>> cartonLabelPools(List<Row> rows) {
|
||||||
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
|
Map<Long, List<String>> result = new java.util.LinkedHashMap<>();
|
||||||
@@ -202,13 +210,9 @@ public class LoadingCompletedOutboxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private record LabelPoolKey(long accountId, long inboundId) { }
|
private record LabelPoolKey(long accountId, long inboundId) { }
|
||||||
private record InboundPalletKey(long accountId, long inboundId) { }
|
|
||||||
|
|
||||||
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
|
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
|
||||||
if (source.isEmpty()) return List.of();
|
if (source.isEmpty()) return List.of();
|
||||||
if (source.stream().anyMatch(item -> Boolean.TRUE.equals(item.getActualBinding()))) {
|
|
||||||
return actualPalletSnapshots(values, source);
|
|
||||||
}
|
|
||||||
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
|
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
|
||||||
p -> (p.getAllocationNo() == null ? "" : p.getAllocationNo()) + "|" + p.getPalletCode(),
|
p -> (p.getAllocationNo() == null ? "" : p.getAllocationNo()) + "|" + p.getPalletCode(),
|
||||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
||||||
@@ -259,43 +263,6 @@ public class LoadingCompletedOutboxService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 现场逐箱扫描已给出精确箱到托关系,不再使用旧的按托盘容量顺序推算。 */
|
|
||||||
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> actualPalletSnapshots(
|
|
||||||
List<Row> values, List<PalletRow> source) {
|
|
||||||
Map<Long, Row> details = values.stream().collect(java.util.stream.Collectors.toMap(
|
|
||||||
Row::getSourceInboundDetailId, value -> value, (left, right) -> left, LinkedHashMap::new));
|
|
||||||
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
|
|
||||||
PalletRow::getPalletCode, LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
|
||||||
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> result = new ArrayList<>();
|
|
||||||
int actualPackages = 0;
|
|
||||||
for (Map.Entry<String, List<PalletRow>> pallet : byPallet.entrySet()) {
|
|
||||||
Map<Long, List<PalletRow>> byDetail = pallet.getValue().stream().collect(
|
|
||||||
java.util.stream.Collectors.groupingBy(PalletRow::getSourceInboundDetailId,
|
|
||||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
|
||||||
List<FirstLegLoadingPlanCompletedV2.SkuPackageSnapshot> skus = new ArrayList<>();
|
|
||||||
for (Map.Entry<Long, List<PalletRow>> detailRows : byDetail.entrySet()) {
|
|
||||||
Row detail = details.get(detailRows.getKey());
|
|
||||||
if (detail == null) throw new BizException(ResultCode.CONFLICT, "现场托盘箱唛无法匹配装柜货物明细");
|
|
||||||
List<String> marks = detailRows.getValue().stream().map(PalletRow::getBoxCode)
|
|
||||||
.filter(Objects::nonNull).distinct().toList();
|
|
||||||
if (marks.size() != detailRows.getValue().size()) {
|
|
||||||
throw new BizException(ResultCode.CONFLICT, "现场托盘包含空箱唛或重复箱唛");
|
|
||||||
}
|
|
||||||
int quantity = detail.getProductQuantity() == null ? marks.size()
|
|
||||||
: (int) Math.round((double) detail.getProductQuantity() * marks.size()
|
|
||||||
/ Math.max(1, detail.getPackageCount()));
|
|
||||||
skus.add(new FirstLegLoadingPlanCompletedV2.SkuPackageSnapshot(detail.getSkuCode(),
|
|
||||||
detail.getProductNameZh(), marks.size(), quantity, marks));
|
|
||||||
actualPackages += marks.size();
|
|
||||||
}
|
|
||||||
result.add(new FirstLegLoadingPlanCompletedV2.PalletSnapshot(pallet.getKey(), skus));
|
|
||||||
}
|
|
||||||
if (actualPackages != sumInt(values, Row::getPackageCount)) {
|
|
||||||
throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘箱唛数与实装箱数不一致");
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean dropship(String value) {
|
private static boolean dropship(String value) {
|
||||||
return "ONE_CLICK_DROPSHIP".equals(value);
|
return "ONE_CLICK_DROPSHIP".equals(value);
|
||||||
}
|
}
|
||||||
@@ -317,6 +284,7 @@ public class LoadingCompletedOutboxService {
|
|||||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value); }
|
private String json(Object value) { try { return objectMapper.writeValueAsString(value); }
|
||||||
catch (JsonProcessingException e) { throw new BizException(ResultCode.INTERNAL_ERROR, "装柜完成事件序列化失败"); } }
|
catch (JsonProcessingException e) { throw new BizException(ResultCode.INTERNAL_ERROR, "装柜完成事件序列化失败"); } }
|
||||||
private static String string(Long value) { return value == null ? null : String.valueOf(value); }
|
private static String string(Long value) { return value == null ? null : String.valueOf(value); }
|
||||||
|
private static String iso(java.time.LocalDateTime value) { return value == null ? null : value.toString(); }
|
||||||
private static int sumInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { return rows.stream().map(getter).filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum(); }
|
private static int sumInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { return rows.stream().map(getter).filter(java.util.Objects::nonNull).mapToInt(Integer::intValue).sum(); }
|
||||||
private static Integer sumNullableInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { List<Integer> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().mapToInt(Integer::intValue).sum(); }
|
private static Integer sumNullableInt(List<Row> rows, java.util.function.Function<Row,Integer> getter) { List<Integer> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().mapToInt(Integer::intValue).sum(); }
|
||||||
private static BigDecimal sum(List<Row> rows, java.util.function.Function<Row,BigDecimal> getter) { List<BigDecimal> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
|
private static BigDecimal sum(List<Row> rows, java.util.function.Function<Row,BigDecimal> getter) { List<BigDecimal> values=rows.stream().map(getter).filter(java.util.Objects::nonNull).toList(); return values.isEmpty()?null:values.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package com.baiyue.firstleg.service;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.exception.BizException;
|
||||||
|
import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity;
|
||||||
|
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
||||||
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/** Lease-based recovery worker for PDA completion commands. */
|
||||||
|
@Service
|
||||||
|
public class LoadingCompletionWorker {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(LoadingCompletionWorker.class);
|
||||||
|
private static final int MAX_ATTEMPTS = 5;
|
||||||
|
private final String workerId = "pda-completion-" + UUID.randomUUID();
|
||||||
|
|
||||||
|
@Resource private LoadingCompletionCommandRepository commands;
|
||||||
|
@Resource private LoadingPickRepository picks;
|
||||||
|
@Resource private LoadingPickService loadingPicks;
|
||||||
|
|
||||||
|
@Scheduled(fixedDelayString = "${baiyue.firstleg.pda-completion.fixed-delay-ms:2000}")
|
||||||
|
public void dispatch() {
|
||||||
|
for (LoadingCompletionCommandEntity candidate : commands.due(10)) {
|
||||||
|
if (!commands.claim(candidate.getId(), workerId, LocalDateTime.now().plusMinutes(10))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loadingPicks.processCompletion(candidate, workerId);
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
handleFailure(candidate, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleFailure(LoadingCompletionCommandEntity command, RuntimeException exception) {
|
||||||
|
Throwable cause = exception instanceof LoadingPickService.CompletionProcessingException
|
||||||
|
&& exception.getCause() != null ? exception.getCause() : exception;
|
||||||
|
String stage = exception instanceof LoadingPickService.CompletionProcessingException staged
|
||||||
|
? staged.stage() : "UNEXPECTED";
|
||||||
|
String code = cause instanceof BizException business
|
||||||
|
? business.getResultCode().code() : "S0500";
|
||||||
|
String requestId = cause instanceof PdaPickSettlementService.SettlementFailure remote
|
||||||
|
? remote.requestId() : null;
|
||||||
|
String traceId = cause instanceof PdaPickSettlementService.SettlementFailure remote
|
||||||
|
? remote.traceId() : null;
|
||||||
|
int attempt = command.getAttemptCount() == null ? 1 : command.getAttemptCount() + 1;
|
||||||
|
boolean terminal = attempt >= MAX_ATTEMPTS;
|
||||||
|
LocalDateTime retryAt = LocalDateTime.now().plusSeconds(Math.min(300L, 15L << Math.min(attempt - 1, 4)));
|
||||||
|
boolean recorded = commands.fail(command.getId(), workerId, terminal ? "FAILED" : "RETRYABLE", retryAt,
|
||||||
|
stage, code, requestId, traceId);
|
||||||
|
if (terminal && recorded) {
|
||||||
|
picks.failCompletion(command.getPickTaskId());
|
||||||
|
}
|
||||||
|
log.warn("pda_completion_worker_failure commandId={} taskId={} operationId={} stage={} code={} "
|
||||||
|
+ "requestId={} traceId={} attempt={} terminal={}", command.getId(), command.getPickTaskId(),
|
||||||
|
command.getOperationId(), stage, code, requestId, traceId, attempt, terminal, cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -496,6 +496,10 @@ public class LoadingListService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
requireLoadingMutable(before);
|
requireLoadingMutable(before);
|
||||||
|
if (java.util.Objects.equals(before.getContainerNo(), containerNo)
|
||||||
|
&& java.util.Objects.equals(before.getVoyageNo(), voyageNo)) {
|
||||||
|
return repository.preloadDetail(before);
|
||||||
|
}
|
||||||
if (containerNo == null && pickRepository.taskByPlan(planId) != null) {
|
if (containerNo == null && pickRepository.taskByPlan(planId) != null) {
|
||||||
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
throw conflict("PDA拣货任务已创建,不能清空真实集装箱号");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
|
|||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.CompleteLoadingRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.Operation;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.Operation;
|
||||||
|
import com.baiyue.firstleg.dto.LoadingPickDtos.CompletionCommand;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.BatchItemRequest;
|
||||||
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryResource;
|
import com.baiyue.firstleg.dto.LoadingPickDtos.InventoryResource;
|
||||||
@@ -26,6 +27,7 @@ import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
|
|||||||
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
|
||||||
|
import com.baiyue.firstleg.entity.LoadingCompletionCommandEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
|
||||||
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
|
||||||
@@ -39,6 +41,7 @@ import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
|
|||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper.CartonLabelRow;
|
||||||
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
|
||||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||||
|
import com.baiyue.firstleg.repository.LoadingCompletionCommandRepository;
|
||||||
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
import com.baiyue.firstleg.repository.SmartLoadingRepository;
|
||||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
@@ -66,6 +69,7 @@ import java.util.Objects;
|
|||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
|
|
||||||
/** 装柜中 PDA 拣货任务及明细同步。 */
|
/** 装柜中 PDA 拣货任务及明细同步。 */
|
||||||
@Service
|
@Service
|
||||||
@@ -76,6 +80,8 @@ public class LoadingPickService {
|
|||||||
@Resource
|
@Resource
|
||||||
private LoadingPickRepository repository;
|
private LoadingPickRepository repository;
|
||||||
@Resource
|
@Resource
|
||||||
|
private LoadingCompletionCommandRepository completionCommands;
|
||||||
|
@Resource
|
||||||
private SmartLoadingRepository smartLoadingRepository;
|
private SmartLoadingRepository smartLoadingRepository;
|
||||||
@Resource
|
@Resource
|
||||||
private OceanBookingRepository oceanBookingRepository;
|
private OceanBookingRepository oceanBookingRepository;
|
||||||
@@ -440,9 +446,31 @@ public class LoadingPickService {
|
|||||||
String operationId = operation(request.operationId());
|
String operationId = operation(request.operationId());
|
||||||
String remark = trim(request.remark());
|
String remark = trim(request.remark());
|
||||||
String requestHash = hash("COMPLETE|" + taskId + '|' + request.expectedTaskVersion() + '|' + remark);
|
String requestHash = hash("COMPLETE|" + taskId + '|' + request.expectedTaskVersion() + '|' + remark);
|
||||||
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
|
LoadingCompletionCommandEntity replay = completionCommands.operation(operationId);
|
||||||
if (replay != null) return replay;
|
if (replay != null) {
|
||||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
requireSameCompletionCommand(replay, taskId, requestHash, operatorId);
|
||||||
|
if ("FAILED".equals(replay.getCommandStatus())) {
|
||||||
|
LoadingPickTaskEntity failed = requiredTaskForUpdate(taskId);
|
||||||
|
if (!"COMPLETION_FAILED".equals(failed.getTaskStatus())
|
||||||
|
|| !completionCommands.resume(replay.getId()) || repository.resumeCompletion(taskId) != 1) {
|
||||||
|
throw conflict("装柜完成命令状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return view(requiredTask(taskId), null);
|
||||||
|
}
|
||||||
|
LoadingPickTaskEntity task = requiredTaskForUpdate(taskId);
|
||||||
|
LoadingCompletionCommandEntity concurrentReplay = completionCommands.operation(operationId);
|
||||||
|
if (concurrentReplay != null) {
|
||||||
|
requireSameCompletionCommand(concurrentReplay, taskId, requestHash, operatorId);
|
||||||
|
return view(task, null);
|
||||||
|
}
|
||||||
|
LoadingCompletionCommandEntity active = completionCommands.active(taskId);
|
||||||
|
if (active != null) {
|
||||||
|
throw conflict("已有其他装柜完成命令正在处理");
|
||||||
|
}
|
||||||
|
if (!"LOADING".equals(task.getTaskStatus())) {
|
||||||
|
throw conflict("装柜任务正在完成或已经完成");
|
||||||
|
}
|
||||||
if (!request.expectedTaskVersion().equals(task.getVersion())) {
|
if (!request.expectedTaskVersion().equals(task.getVersion())) {
|
||||||
throw conflict("PDA任务版本已变化,请刷新后复核");
|
throw conflict("PDA任务版本已变化,请刷新后复核");
|
||||||
}
|
}
|
||||||
@@ -454,40 +482,122 @@ public class LoadingPickService {
|
|||||||
.filter(owner -> owner.tenantId() != null && owner.inboundId() != null)
|
.filter(owner -> owner.tenantId() != null && owner.inboundId() != null)
|
||||||
.toList();
|
.toList();
|
||||||
if (discoveredOwners.isEmpty()) throw conflict("PDA任务未关联有效入库单");
|
if (discoveredOwners.isEmpty()) throw conflict("PDA任务未关联有效入库单");
|
||||||
LoadingPickItemMapper.InboundOwner evidenceOwner = discoveredOwners.get(0);
|
LoadingCompletionCommandEntity command = new LoadingCompletionCommandEntity();
|
||||||
List<LoadingPickItemMapper.InboundOwner> owners = discoveredOwners.stream()
|
command.setId(IdWorker.getId());
|
||||||
|
command.setPickTaskId(taskId);
|
||||||
|
command.setOperationId(operationId);
|
||||||
|
command.setRequestHash(requestHash);
|
||||||
|
command.setExpectedTaskVersion(request.expectedTaskVersion());
|
||||||
|
command.setRemark(remark);
|
||||||
|
command.setOperatorId(operatorId);
|
||||||
|
command.setOperatorName(trim(operatorName));
|
||||||
|
if (completionCommands.insert(command) != 1) {
|
||||||
|
LoadingCompletionCommandEntity concurrent = completionCommands.operation(operationId);
|
||||||
|
requireSameCompletionCommand(concurrent, taskId, requestHash, operatorId);
|
||||||
|
return view(requiredTask(taskId), null);
|
||||||
|
}
|
||||||
|
if (repository.acceptCompletion(taskId, task.getVersion()) != 1) {
|
||||||
|
throw conflict("PDA任务版本已变化,请刷新后复核");
|
||||||
|
}
|
||||||
|
return view(requiredTask(taskId), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called only by the completion worker, after its durable lease has committed. */
|
||||||
|
public void processCompletion(LoadingCompletionCommandEntity command, String worker) {
|
||||||
|
long taskId = command.getPickTaskId();
|
||||||
|
LoadingPickTaskEntity task = requiredTask(taskId);
|
||||||
|
List<LoadingPickItemMapper.InboundOwner> owners = repository.inboundOwners(taskId).stream()
|
||||||
|
.filter(owner -> owner.tenantId() != null && owner.inboundId() != null)
|
||||||
.sorted(Comparator.comparing(LoadingPickItemMapper.InboundOwner::tenantId)
|
.sorted(Comparator.comparing(LoadingPickItemMapper.InboundOwner::tenantId)
|
||||||
.thenComparing(LoadingPickItemMapper.InboundOwner::inboundId))
|
.thenComparing(LoadingPickItemMapper.InboundOwner::inboundId))
|
||||||
.toList();
|
.toList();
|
||||||
for (LoadingPickItemMapper.InboundOwner owner : owners) {
|
if (owners.isEmpty()) throw new CompletionProcessingException("OWNER_DISCOVERY", conflict("PDA任务未关联有效入库单"));
|
||||||
LoadingInboundMapper.Snapshot snapshot = repository.inboundForUpdate(owner.tenantId(), owner.inboundId());
|
long evidenceInboundId = owners.get(0).inboundId();
|
||||||
if (snapshot == null) throw conflict("PDA任务关联入库单不存在");
|
for (int index = 0; index < owners.size(); index++) {
|
||||||
completionStage(taskId, "完成前业务门禁", () ->
|
requireCompletionLease(command.getId(), worker);
|
||||||
loadingDeliveryService.requirePdaCompletionReady(owner.tenantId(), owner.inboundId(),
|
LoadingPickItemMapper.InboundOwner owner = owners.get(index);
|
||||||
evidenceOwner.inboundId()));
|
String checkpoint = completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId());
|
||||||
|
if (!checkpointAtLeast(checkpoint, "READY")) {
|
||||||
|
runCompletionStage("OWNER_READY", () -> loadingDeliveryService.requirePdaCompletionReady(
|
||||||
|
owner.tenantId(), owner.inboundId(), evidenceInboundId));
|
||||||
|
requireCompletionLease(command.getId(), worker);
|
||||||
|
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(), index + 1, "READY");
|
||||||
|
}
|
||||||
|
if (!checkpointAtLeast(checkpoint, "SETTLED")) {
|
||||||
|
runCompletionStage("WAREHOUSE_SETTLEMENT", () -> settlementService.settleOwner(
|
||||||
|
taskId, owner.inboundId(), command.getOperatorId(), command.getOperatorName()));
|
||||||
|
requireCompletionLease(command.getId(), worker);
|
||||||
|
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(), index + 1, "SETTLED");
|
||||||
|
}
|
||||||
|
if (!checkpointAtLeast(checkpoint, "INBOUND_COMPLETED")) {
|
||||||
|
runCompletionStage("INBOUND_COMPLETE", () -> loadingDeliveryService.completeAfterPdaSettlement(
|
||||||
|
owner.tenantId(), owner.inboundId(), evidenceInboundId, command.getOperatorId(),
|
||||||
|
command.getOperatorName(), "PDA-" + command.getOperationId() + '-' + owner.inboundId(),
|
||||||
|
command.getRemark()));
|
||||||
|
requireCompletionLease(command.getId(), worker);
|
||||||
|
completionCommands.checkpoint(command.getId(), owner.tenantId(), owner.inboundId(), index + 1,
|
||||||
|
"INBOUND_COMPLETED");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
completionStage(taskId, "拆单与库位结算", () ->
|
|
||||||
settlementService.settle(taskId, operatorId, operatorName));
|
|
||||||
if (!repository.allPlanItemsPicked(task.getLoadingPlanId())) {
|
if (!repository.allPlanItemsPicked(task.getLoadingPlanId())) {
|
||||||
throw conflict("柜次仍有未完成PDA拣货的箱数");
|
throw new CompletionProcessingException("PLAN_GATE", conflict("柜次仍有未完成PDA拣货的箱数"));
|
||||||
}
|
}
|
||||||
if (repository.approve(taskId, operatorId, operatorName, remark) != 1) {
|
if (repository.review(taskId) == null && repository.approve(taskId, command.getOperatorId(),
|
||||||
throw conflict("PDA任务已经复核");
|
command.getOperatorName(), command.getRemark()) != 1) {
|
||||||
|
throw new CompletionProcessingException("TASK_REVIEW", conflict("PDA任务已经复核"));
|
||||||
}
|
}
|
||||||
insertOperation(taskId, operationId, requestHash, "COMPLETE_LOADING", operatorId, operatorName);
|
if (repository.operation(command.getOperationId()) == null) {
|
||||||
completionStage(taskId, "装柜计划完成", () ->
|
insertOperation(taskId, command.getOperationId(), command.getRequestHash(), "COMPLETE_LOADING",
|
||||||
loadingListService.finishFromPda(task.getLoadingPlanId(), taskId, operatorId, operatorName));
|
command.getOperatorId(), command.getOperatorName());
|
||||||
repository.syncInboundProjections(taskId, operatorId, operatorName);
|
|
||||||
for (LoadingPickItemMapper.InboundOwner owner : owners) {
|
|
||||||
completionStage(taskId, "入库单状态完成", () ->
|
|
||||||
loadingDeliveryService.completeAfterPdaSettlement(owner.tenantId(), owner.inboundId(),
|
|
||||||
evidenceOwner.inboundId(), operatorId, operatorName,
|
|
||||||
"PDA-" + operationId + '-' + owner.inboundId(), remark));
|
|
||||||
}
|
}
|
||||||
completionStage(taskId, "完成事件发布", () ->
|
runCompletionStage("PLAN_COMPLETE", () -> loadingListService.finishFromPda(task.getLoadingPlanId(), taskId,
|
||||||
loadingListService.publishCompletedEvent(task.getLoadingPlanId()));
|
command.getOperatorId(), command.getOperatorName()));
|
||||||
completeTruckAfterCommit(task.getContainerBookingId(), taskId, operatorId, operatorName);
|
repository.syncInboundProjections(taskId, command.getOperatorId(), command.getOperatorName());
|
||||||
return view(requiredTask(taskId), null);
|
runCompletionStage("COMPLETED_EVENT", () -> loadingListService.publishCompletedEvent(task.getLoadingPlanId()));
|
||||||
|
requireCompletionLease(command.getId(), worker);
|
||||||
|
LoadingPickTaskEntity current = requiredTask(taskId);
|
||||||
|
if (!"COMPLETED".equals(current.getTaskStatus())
|
||||||
|
&& repository.completeAccepted(taskId, command.getOperatorId(), command.getOperatorName()) != 1) {
|
||||||
|
throw new CompletionProcessingException("TASK_FINALIZE", conflict("PDA任务完成状态已变化"));
|
||||||
|
}
|
||||||
|
if (!completionCommands.complete(command.getId(), worker)) {
|
||||||
|
throw new CompletionProcessingException("COMMAND_FINALIZE", conflict("装柜完成命令租约已变化"));
|
||||||
|
}
|
||||||
|
completeTruckAfterCommit(task.getContainerBookingId(), taskId, command.getOperatorId(), command.getOperatorName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireCompletionLease(long commandId, String worker) {
|
||||||
|
if (!completionCommands.renew(commandId, worker, LocalDateTime.now().plusMinutes(10))) {
|
||||||
|
throw new CompletionProcessingException("LEASE_LOST", conflict("装柜完成命令租约已变化"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void requireSameCompletionCommand(LoadingCompletionCommandEntity command, long taskId,
|
||||||
|
String requestHash, long operatorId) {
|
||||||
|
if (command == null || !Long.valueOf(taskId).equals(command.getPickTaskId())
|
||||||
|
|| !Long.valueOf(operatorId).equals(command.getOperatorId())
|
||||||
|
|| !requestHash.equals(command.getRequestHash())) {
|
||||||
|
throw conflict("operationId已用于不同装柜完成命令");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean checkpointAtLeast(String checkpoint, String target) {
|
||||||
|
List<String> stages = List.of("READY", "SETTLED", "INBOUND_COMPLETED");
|
||||||
|
return checkpoint != null && stages.indexOf(checkpoint) >= stages.indexOf(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void runCompletionStage(String stage, Runnable action) {
|
||||||
|
try {
|
||||||
|
action.run();
|
||||||
|
} catch (RuntimeException exception) {
|
||||||
|
throw new CompletionProcessingException(stage, exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class CompletionProcessingException extends RuntimeException {
|
||||||
|
private final String stage;
|
||||||
|
CompletionProcessingException(String stage, RuntimeException cause) { super(cause); this.stage = stage; }
|
||||||
|
public String stage() { return stage; }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void completionStage(long taskId, String stage, Runnable action) {
|
private void completionStage(long taskId, String stage, Runnable action) {
|
||||||
@@ -692,6 +802,13 @@ public class LoadingPickService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Task view(LoadingPickTaskEntity task, Long warehouseId) {
|
private Task view(LoadingPickTaskEntity task, Long warehouseId) {
|
||||||
|
var booking = repository.activeBookingByTask(task.getId());
|
||||||
|
if (booking == null || !Objects.equals(task.getContainerBookingId(), booking.getId())) {
|
||||||
|
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录");
|
||||||
|
}
|
||||||
|
if (warehouseId != null && !Objects.equals(warehouseId, booking.getWarehouseId())) {
|
||||||
|
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或不属于当前仓库");
|
||||||
|
}
|
||||||
List<LoadingPickItemEntity> source = warehouseId == null
|
List<LoadingPickItemEntity> source = warehouseId == null
|
||||||
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
|
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
|
||||||
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
|
Map<Long, List<CartonLabelRow>> cartonsByItem = repository.cartonLabels(task.getId()).stream()
|
||||||
@@ -714,24 +831,28 @@ public class LoadingPickService {
|
|||||||
Review review = reviewed == null ? null : new Review(reviewed.getReviewStatus(),
|
Review review = reviewed == null ? null : new Review(reviewed.getReviewStatus(),
|
||||||
reviewed.getReviewedBy(), reviewed.getReviewedByName(), reviewed.getRemark(), reviewed.getVersion());
|
reviewed.getReviewedBy(), reviewed.getReviewedByName(), reviewed.getRemark(), reviewed.getVersion());
|
||||||
String workflowStatus = "COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED"
|
String workflowStatus = "COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED"
|
||||||
|
: "COMPLETING".equals(task.getTaskStatus()) ? "PROCESSING"
|
||||||
|
: "COMPLETION_FAILED".equals(task.getTaskStatus()) ? "COMPLETION_FAILED"
|
||||||
: assigned == null ? "UNASSIGNED"
|
: assigned == null ? "UNASSIGNED"
|
||||||
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
||||||
var booking = repository.activeBookingByTask(task.getId());
|
|
||||||
if (booking == null || !Objects.equals(task.getContainerBookingId(), booking.getId())) {
|
|
||||||
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货任务不存在或柜次已非有效存续记录");
|
|
||||||
}
|
|
||||||
var owners = repository.inboundOwners(task.getId());
|
var owners = repository.inboundOwners(task.getId());
|
||||||
Long detailInboundId = source.isEmpty() ? null : source.get(0).getInboundId();
|
Long detailInboundId = source.isEmpty() ? null : source.get(0).getInboundId();
|
||||||
var owner = detailInboundId == null ? null : owners.stream()
|
var owner = detailInboundId == null ? null : owners.stream()
|
||||||
.filter(value -> detailInboundId.equals(value.inboundId())).findFirst().orElse(null);
|
.filter(value -> detailInboundId.equals(value.inboundId())).findFirst().orElse(null);
|
||||||
var inbound = owner == null || owner.tenantId() == null || owner.inboundId() == null
|
var inbound = owner == null || owner.tenantId() == null || owner.inboundId() == null
|
||||||
? null : repository.inbound(owner.tenantId(), owner.inboundId());
|
? null : repository.inbound(owner.tenantId(), owner.inboundId());
|
||||||
|
LoadingCompletionCommandEntity command = "LOADING".equals(task.getTaskStatus())
|
||||||
|
? null : completionCommands.latest(task.getId());
|
||||||
|
CompletionCommand completionCommand = command == null ? null : new CompletionCommand(command.getId(),
|
||||||
|
command.getOperationId(), command.getCommandStatus(), command.getExpectedTaskVersion(), command.getRemark(),
|
||||||
|
command.getAttemptCount(), command.getNextRetryAt(), command.getFailureStage(), command.getFailureCode(),
|
||||||
|
command.getFailureRequestId(), command.getFailureTraceId(), command.getCompletedAt());
|
||||||
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(),
|
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerBookingId(),
|
||||||
owner == null ? null : owner.inboundId(), inbound == null ? null : inbound.version(),
|
owner == null ? null : owner.inboundId(), inbound == null ? null : inbound.version(),
|
||||||
inbound == null ? null : inbound.version(), task.getCabinetNo(),
|
inbound == null ? null : inbound.version(), task.getCabinetNo(),
|
||||||
booking.getContainerNo(), booking.getSourceType(), booking.getRecordStatus(),
|
booking.getContainerNo(), booking.getSourceType(), booking.getRecordStatus(),
|
||||||
booking.getMergedIntoContainerBookingId(), task.getTaskStatus(),
|
booking.getMergedIntoContainerBookingId(), task.getTaskStatus(),
|
||||||
workflowStatus, task.getVersion(), assignment, review, items);
|
workflowStatus, task.getVersion(), assignment, review, completionCommand, items);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<BatchLine> validateBatchLines(LoadingPickTaskEntity task, BatchRequest request) {
|
private List<BatchLine> validateBatchLines(LoadingPickTaskEntity task, BatchRequest request) {
|
||||||
|
|||||||
@@ -1,206 +0,0 @@
|
|||||||
package com.baiyue.firstleg.service;
|
|
||||||
|
|
||||||
import com.baiyue.common.core.api.ResultCode;
|
|
||||||
import com.baiyue.common.core.exception.BizException;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Carton;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.OpenRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.Pallet;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.ScanRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SealRequest;
|
|
||||||
import com.baiyue.firstleg.dto.LoadingPalletDtos.SkuSummary;
|
|
||||||
import com.baiyue.firstleg.entity.LoadingWorkPalletCartonEntity;
|
|
||||||
import com.baiyue.firstleg.entity.LoadingWorkPalletEntity;
|
|
||||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper;
|
|
||||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletCartonMapper.CartonCandidate;
|
|
||||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper;
|
|
||||||
import com.baiyue.firstleg.mapper.LoadingWorkPalletMapper.TaskContext;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/** 一件代发现场打托旁路;不改变既有拣货数量和仓库资源结算。 */
|
|
||||||
@Service
|
|
||||||
public class LoadingWorkPalletService {
|
|
||||||
@Resource private LoadingWorkPalletMapper pallets;
|
|
||||||
@Resource private LoadingWorkPalletCartonMapper cartons;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Pallet open(long taskId, OpenRequest request, long operatorId, String operatorName) {
|
|
||||||
TaskContext context = requireWorkTask(taskId, request.warehouseId(), operatorId);
|
|
||||||
String code = request.palletCode().trim();
|
|
||||||
String operationId = request.operationId().trim();
|
|
||||||
LoadingWorkPalletEntity replay = pallets.selectByOpenOperation(operationId);
|
|
||||||
if (replay != null) {
|
|
||||||
if (replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
|
|
||||||
throw conflict("开托幂等号已用于其他请求");
|
|
||||||
}
|
|
||||||
return view(replay);
|
|
||||||
}
|
|
||||||
LoadingWorkPalletEntity value = new LoadingWorkPalletEntity();
|
|
||||||
value.setPickTaskId(taskId);
|
|
||||||
value.setLoadingPlanId(context.loadingPlanId());
|
|
||||||
value.setWarehouseId(request.warehouseId());
|
|
||||||
value.setWarehousePalletId(request.warehousePalletId());
|
|
||||||
value.setPalletCode(code);
|
|
||||||
value.setOpenOperationId(operationId);
|
|
||||||
value.setPalletStatus("OPEN");
|
|
||||||
value.setCreatedBy(operatorId);
|
|
||||||
value.setCreatedByName(operatorName);
|
|
||||||
value.setVersion(0);
|
|
||||||
try {
|
|
||||||
pallets.insert(value);
|
|
||||||
} catch (DuplicateKeyException exception) {
|
|
||||||
replay = pallets.selectByOpenOperation(operationId);
|
|
||||||
if (replay == null || replay.getPickTaskId() != taskId || !replay.getPalletCode().equals(code)) {
|
|
||||||
throw conflict("托盘号已存在或开托操作重复");
|
|
||||||
}
|
|
||||||
value = replay;
|
|
||||||
}
|
|
||||||
return view(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Pallet scan(long taskId, long palletId, ScanRequest request,
|
|
||||||
long operatorId, String operatorName) {
|
|
||||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
|
||||||
LoadingWorkPalletEntity target = requireOpenPallet(taskId, palletId, request.warehouseId());
|
|
||||||
String operationId = request.operationId().trim();
|
|
||||||
LoadingWorkPalletCartonEntity replay = cartons.selectByOperation(operationId);
|
|
||||||
if (replay != null) {
|
|
||||||
if (replay.getPickTaskId() != taskId || replay.getWorkPalletId() != palletId
|
|
||||||
|| !replay.getBoxCode().equals(request.boxCode().trim())) {
|
|
||||||
throw conflict("箱唛操作幂等号已用于其他请求");
|
|
||||||
}
|
|
||||||
return view(target);
|
|
||||||
}
|
|
||||||
List<CartonCandidate> candidates = cartons.selectCandidates(taskId, request.warehouseId(),
|
|
||||||
request.boxCode().trim());
|
|
||||||
if (candidates.isEmpty()) throw new BizException(ResultCode.NOT_FOUND, "箱唛不属于当前任务的一件代发货物");
|
|
||||||
if (candidates.size() > 1) throw conflict("箱唛在当前多客户任务中不唯一,请检查箱唛数据");
|
|
||||||
CartonCandidate candidate = candidates.get(0);
|
|
||||||
if (candidate.getSkuCode() == null || candidate.getSkuCode().isBlank()) {
|
|
||||||
throw conflict("一件代发箱唛缺少SKU,不能上托");
|
|
||||||
}
|
|
||||||
LoadingWorkPalletCartonEntity current = cartons.selectBinding(taskId, candidate.getCartonLabelId());
|
|
||||||
if (current == null) {
|
|
||||||
current = binding(taskId, palletId, candidate, operationId, operatorId, operatorName);
|
|
||||||
try {
|
|
||||||
cartons.insert(current);
|
|
||||||
} catch (DuplicateKeyException exception) {
|
|
||||||
LoadingWorkPalletCartonEntity concurrent = cartons.selectBinding(taskId, candidate.getCartonLabelId());
|
|
||||||
if (concurrent == null || concurrent.getWorkPalletId() != palletId) {
|
|
||||||
throw conflict("箱唛已被其他托盘绑定");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return view(target);
|
|
||||||
}
|
|
||||||
if (current.getWorkPalletId() == palletId) return view(target);
|
|
||||||
LoadingWorkPalletEntity source = pallets.selectScoped(taskId, current.getWorkPalletId(), request.warehouseId());
|
|
||||||
if (source == null || !"OPEN".equals(source.getPalletStatus())) {
|
|
||||||
throw conflict("来源托盘已封托,不能直接移箱");
|
|
||||||
}
|
|
||||||
if (cartons.move(current.getId(), current.getVersion(), palletId, operatorId, operatorName, operationId) != 1) {
|
|
||||||
throw conflict("箱唛绑定已变化,请刷新后重试");
|
|
||||||
}
|
|
||||||
return view(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public Pallet seal(long taskId, long palletId, SealRequest request,
|
|
||||||
long operatorId, String operatorName) {
|
|
||||||
requireWorkTask(taskId, request.warehouseId(), operatorId);
|
|
||||||
LoadingWorkPalletEntity replay = pallets.selectBySealOperation(request.operationId().trim());
|
|
||||||
if (replay != null) {
|
|
||||||
if (replay.getId() != palletId || replay.getPickTaskId() != taskId) {
|
|
||||||
throw conflict("封托幂等号已用于其他请求");
|
|
||||||
}
|
|
||||||
return view(replay);
|
|
||||||
}
|
|
||||||
requireOpenPallet(taskId, palletId, request.warehouseId());
|
|
||||||
if (cartons.countPalletCartons(palletId) == 0) throw conflict("空托盘不能封托");
|
|
||||||
if (cartons.countUnreconciled(taskId) > 0) {
|
|
||||||
throw conflict("现场上托箱数超过已提交拣货箱数,请先完成原PDA拣货提交");
|
|
||||||
}
|
|
||||||
if (pallets.seal(taskId, palletId, request.warehouseId(), request.expectedVersion(),
|
|
||||||
request.operationId().trim(), operatorId, operatorName) != 1) {
|
|
||||||
throw conflict("托盘状态或版本已变化,请刷新后重试");
|
|
||||||
}
|
|
||||||
return view(pallets.selectScoped(taskId, palletId, request.warehouseId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public List<Pallet> list(long taskId, long warehouseId) {
|
|
||||||
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
|
|
||||||
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
|
|
||||||
return pallets.selectTaskPallets(taskId, warehouseId).stream().map(this::view).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private TaskContext requireWorkTask(long taskId, long warehouseId, long operatorId) {
|
|
||||||
TaskContext context = pallets.selectTaskContext(taskId, warehouseId);
|
|
||||||
if (context == null) throw new BizException(ResultCode.NOT_FOUND, "未找到当前仓库的PDA任务");
|
|
||||||
if (!"LOADING".equals(context.taskStatus())) throw conflict("PDA任务已完成,不能继续打托");
|
|
||||||
if (context.dropshipItemCount() == null || context.dropshipItemCount() == 0) {
|
|
||||||
throw conflict("当前任务没有一件代发货物");
|
|
||||||
}
|
|
||||||
if (context.assigneeId() == null || context.assigneeId() != operatorId) {
|
|
||||||
throw conflict("请先领取PDA任务后再执行打托");
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
|
|
||||||
private LoadingWorkPalletEntity requireOpenPallet(long taskId, long palletId, long warehouseId) {
|
|
||||||
LoadingWorkPalletEntity pallet = pallets.selectScoped(taskId, palletId, warehouseId);
|
|
||||||
if (pallet == null) throw new BizException(ResultCode.NOT_FOUND, "现场托盘不存在");
|
|
||||||
if (!"OPEN".equals(pallet.getPalletStatus())) throw conflict("托盘不是打托中状态");
|
|
||||||
return pallet;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LoadingWorkPalletCartonEntity binding(long taskId, long palletId,
|
|
||||||
CartonCandidate source, String operationId,
|
|
||||||
long operatorId, String operatorName) {
|
|
||||||
LoadingWorkPalletCartonEntity value = new LoadingWorkPalletCartonEntity();
|
|
||||||
value.setPickTaskId(taskId);
|
|
||||||
value.setWorkPalletId(palletId);
|
|
||||||
value.setLoadingPlanItemId(source.getLoadingPlanItemId());
|
|
||||||
value.setPickItemId(source.getPickItemId());
|
|
||||||
value.setTenantId(source.getTenantId());
|
|
||||||
value.setInboundId(source.getInboundId());
|
|
||||||
value.setInboundDetailId(source.getInboundDetailId());
|
|
||||||
value.setCartonLabelId(source.getCartonLabelId());
|
|
||||||
value.setBoxCode(source.getBoxCode());
|
|
||||||
value.setSkuCode(source.getSkuCode());
|
|
||||||
value.setUnitsPerCarton(source.getUnitsPerCarton());
|
|
||||||
value.setBoundBy(operatorId);
|
|
||||||
value.setBoundByName(operatorName);
|
|
||||||
value.setOperationId(operationId);
|
|
||||||
value.setVersion(0);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pallet view(LoadingWorkPalletEntity pallet) {
|
|
||||||
List<LoadingWorkPalletCartonEntity> values = cartons.selectPalletCartons(pallet.getId());
|
|
||||||
Map<String, List<LoadingWorkPalletCartonEntity>> bySku = values.stream().collect(
|
|
||||||
java.util.stream.Collectors.groupingBy(LoadingWorkPalletCartonEntity::getSkuCode,
|
|
||||||
LinkedHashMap::new, java.util.stream.Collectors.toList()));
|
|
||||||
List<SkuSummary> skus = bySku.entrySet().stream().map(entry -> new SkuSummary(entry.getKey(),
|
|
||||||
entry.getValue().size(), entry.getValue().stream().map(LoadingWorkPalletCartonEntity::getUnitsPerCarton)
|
|
||||||
.filter(Objects::nonNull).mapToLong(Integer::longValue).sum())).toList();
|
|
||||||
List<Carton> cartonViews = values.stream().map(value -> new Carton(value.getId(), value.getBoxCode(),
|
|
||||||
value.getSkuCode(), value.getTenantId(), value.getInboundId(), value.getInboundDetailId(),
|
|
||||||
value.getUnitsPerCarton())).toList();
|
|
||||||
return new Pallet(pallet.getId(), pallet.getPalletCode(), pallet.getWarehousePalletId(),
|
|
||||||
pallet.getPalletStatus(), pallet.getVersion(), values.size(), bySku.size(),
|
|
||||||
(int) values.stream().map(LoadingWorkPalletCartonEntity::getTenantId).distinct().count(),
|
|
||||||
skus, cartonViews);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static BizException conflict(String message) {
|
|
||||||
return new BizException(ResultCode.CONFLICT, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-135
@@ -1,135 +0,0 @@
|
|||||||
package com.baiyue.firstleg.service;
|
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
||||||
import com.baiyue.common.event.EventEnvelope;
|
|
||||||
import com.baiyue.common.event.EventPayloadCanonicalizer;
|
|
||||||
import com.baiyue.common.event.EventPayloadHash;
|
|
||||||
import com.baiyue.common.event.EventTypes;
|
|
||||||
import com.baiyue.common.event.OverseasInboundOrderDetainedChangedV1;
|
|
||||||
import com.baiyue.common.event.RegionCode;
|
|
||||||
import com.baiyue.firstleg.entity.ContainerEventInboxEntity;
|
|
||||||
import com.baiyue.firstleg.entity.InboundHoldRecordEntity;
|
|
||||||
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|
||||||
import com.baiyue.firstleg.mapper.ContainerEventInboxMapper;
|
|
||||||
import com.baiyue.firstleg.mapper.InboundHoldRecordMapper;
|
|
||||||
import com.baiyue.firstleg.mapper.InboundOrderMapper;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/** 将海外暂扣事实按来源入仓单绑定回写到国内头程入仓单。 */
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class OverseasInboundOrderDetainedConsumer {
|
|
||||||
private final InboundOrderMapper inboundOrders;
|
|
||||||
private final InboundHoldRecordMapper holdRecords;
|
|
||||||
private final ContainerEventInboxMapper inbox;
|
|
||||||
private final ObjectMapper objectMapper;
|
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public InboundOrderEntity consume(EventEnvelope event) {
|
|
||||||
validateEnvelope(event);
|
|
||||||
OverseasInboundOrderDetainedChangedV1 payload = payload(event);
|
|
||||||
long inboundId = parsePositive(payload.sourceInboundId(), "来源入仓单 ID 无效");
|
|
||||||
long tenantId = parsePositive(event.tenantId(), "客户 ID 无效");
|
|
||||||
if (!payload.sourceInboundId().equals(event.aggregateId())) {
|
|
||||||
throw new IllegalArgumentException("事件 aggregateId 与来源入仓单 ID 不一致");
|
|
||||||
}
|
|
||||||
if (payload.changedAt() == null || payload.inboundCode() == null || payload.inboundCode().isBlank()
|
|
||||||
|| payload.reason() == null || payload.reason().isBlank()) {
|
|
||||||
throw new IllegalArgumentException("暂扣事件缺少入仓码、原因或变更时间");
|
|
||||||
}
|
|
||||||
|
|
||||||
ContainerEventInboxEntity existing = inbox.selectById(event.eventId());
|
|
||||||
if (existing != null) {
|
|
||||||
if (!existing.getPayloadHash().equalsIgnoreCase(event.payloadHash())) {
|
|
||||||
throw new IllegalArgumentException("相同 eventId 对应的事件摘要冲突");
|
|
||||||
}
|
|
||||||
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
|
||||||
}
|
|
||||||
ContainerEventInboxEntity inboxRow = inbox(event);
|
|
||||||
try {
|
|
||||||
inbox.insert(inboxRow);
|
|
||||||
} catch (DuplicateKeyException duplicate) {
|
|
||||||
ContainerEventInboxEntity raced = inbox.selectById(event.eventId());
|
|
||||||
if (raced == null || !raced.getPayloadHash().equalsIgnoreCase(event.payloadHash())) throw duplicate;
|
|
||||||
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
InboundOrderEntity inbound = requiredInbound(inboundId, tenantId, payload.inboundCode());
|
|
||||||
if (inbound.getLastOverseasHoldVersion() == null
|
|
||||||
|| event.aggregateVersion() > inbound.getLastOverseasHoldVersion()) {
|
|
||||||
int updated = inboundOrders.applyOverseasHold(tenantId, inboundId, payload.detained(),
|
|
||||||
payload.detained() ? payload.reason() : null, event.aggregateVersion(),
|
|
||||||
parseOperatorId(payload.operatorId()));
|
|
||||||
if (updated == 1) insertHistory(event, payload, tenantId, inboundId);
|
|
||||||
}
|
|
||||||
inboxRow.setStatus("PROCESSED");
|
|
||||||
inboxRow.setProcessedAt(LocalDateTime.now(ZoneOffset.UTC));
|
|
||||||
inbox.updateById(inboxRow);
|
|
||||||
return requiredInbound(inboundId, tenantId, payload.inboundCode());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateEnvelope(EventEnvelope event) {
|
|
||||||
event.requireValid();
|
|
||||||
if (!EventTypes.OVERSEAS_INBOUND_ORDER_DETAINED_CHANGED.equals(event.eventType())
|
|
||||||
|| event.eventVersion() != 1 || event.sourceRegion() != RegionCode.OVERSEAS
|
|
||||||
|| event.targetRegion() != RegionCode.CN || !"INBOUND_ORDER".equals(event.aggregateType())) {
|
|
||||||
throw new IllegalArgumentException("不支持的海外入仓单暂扣事件");
|
|
||||||
}
|
|
||||||
if (!EventPayloadHash.matches(EventPayloadCanonicalizer.canonicalize(event.payload()), event.payloadHash())) {
|
|
||||||
throw new IllegalArgumentException("payloadHash 与事件负载不一致");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private OverseasInboundOrderDetainedChangedV1 payload(EventEnvelope event) {
|
|
||||||
try { return objectMapper.treeToValue(event.payload(), OverseasInboundOrderDetainedChangedV1.class); }
|
|
||||||
catch (Exception exception) { throw new IllegalArgumentException("海外入仓单暂扣事件负载无效", exception); }
|
|
||||||
}
|
|
||||||
|
|
||||||
private InboundOrderEntity requiredInbound(long inboundId, long tenantId, String inboundCode) {
|
|
||||||
InboundOrderEntity value = inboundOrders.selectActiveByIdForUpdate(inboundId);
|
|
||||||
if (value == null) throw new IllegalArgumentException("来源头程入仓单不存在");
|
|
||||||
if (!Objects.equals(value.getTenantId(), tenantId) || !Objects.equals(value.getInboundCode(), inboundCode)) {
|
|
||||||
throw new IllegalArgumentException("事件与来源头程入仓单归属不一致");
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void insertHistory(EventEnvelope event, OverseasInboundOrderDetainedChangedV1 payload,
|
|
||||||
long tenantId, long inboundId) {
|
|
||||||
InboundHoldRecordEntity row = new InboundHoldRecordEntity();
|
|
||||||
row.setId(IdWorker.getId()); row.setTenantId(tenantId); row.setInboundId(inboundId);
|
|
||||||
row.setHoldAction(payload.detained() ? "HOLD" : "RELEASE"); row.setReason(payload.reason());
|
|
||||||
row.setOperatorId(parseOperatorId(payload.operatorId())); row.setOperatorName(payload.operatorName());
|
|
||||||
row.setOperatedAt(LocalDateTime.ofInstant(payload.changedAt(), ZoneOffset.UTC));
|
|
||||||
holdRecords.insert(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
private ContainerEventInboxEntity inbox(EventEnvelope event) {
|
|
||||||
ContainerEventInboxEntity row = new ContainerEventInboxEntity();
|
|
||||||
row.setEventId(event.eventId()); row.setEventType(event.eventType()); row.setEventVersion(event.eventVersion());
|
|
||||||
row.setAggregateId(event.aggregateId()); row.setAggregateVersion(event.aggregateVersion());
|
|
||||||
row.setPayloadHash(event.payloadHash().toLowerCase(Locale.ROOT)); row.setPayloadJson(event.payload().toString());
|
|
||||||
row.setStatus("RECEIVED"); row.setReceivedAt(LocalDateTime.now(ZoneOffset.UTC));
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parsePositive(String value, String message) {
|
|
||||||
try { long parsed = Long.parseLong(value); if (parsed > 0) return parsed; }
|
|
||||||
catch (RuntimeException ignored) { }
|
|
||||||
throw new IllegalArgumentException(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private long parseOperatorId(String value) {
|
|
||||||
try { return value == null ? 0L : Long.parseLong(value); }
|
|
||||||
catch (NumberFormatException ignored) { return 0L; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -18,6 +18,8 @@ import jakarta.annotation.Resource;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -32,6 +34,7 @@ import java.util.stream.Collectors;
|
|||||||
/** PDA 任务结算:按柜次生成实装/余量履约分片,并结算测量与库位资源。 */
|
/** PDA 任务结算:按柜次生成实装/余量履约分片,并结算测量与库位资源。 */
|
||||||
@Service
|
@Service
|
||||||
public class PdaPickSettlementService {
|
public class PdaPickSettlementService {
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(PdaPickSettlementService.class);
|
||||||
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
private static final long PLATFORM_WAREHOUSE_RESOURCE_TENANT_ID = 0L;
|
||||||
private static final String SOURCE_SERVICE = "firstleg-service";
|
private static final String SOURCE_SERVICE = "firstleg-service";
|
||||||
|
|
||||||
@@ -82,6 +85,35 @@ public class PdaPickSettlementService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Worker boundary: one inbound owner per transaction, so a cabinet never holds 15 owner locks at once. */
|
||||||
|
@Transactional
|
||||||
|
public void settleOwner(long taskId, long inboundId, long operatorId, String operatorName) {
|
||||||
|
if (!picks.allTaskItemsResolved(taskId)) {
|
||||||
|
throw conflict("PDA任务仍有未处理明细");
|
||||||
|
}
|
||||||
|
List<LoadingPickItemEntity> taskItems = picks.items(taskId);
|
||||||
|
Map<Long, Integer> pickedByInbound = pickedByInbound(taskItems);
|
||||||
|
Integer pickedPackageCount = pickedByInbound.get(inboundId);
|
||||||
|
if (pickedPackageCount == null) {
|
||||||
|
throw conflict("PDA任务不包含指定入库单");
|
||||||
|
}
|
||||||
|
PdaPickSettlementPlan.Resolution resolution = PdaPickSettlementPlan.resolve(taskItems);
|
||||||
|
for (Long planItemId : resolution.skippedPlanItemIds()) {
|
||||||
|
int changed = splits.removeSkippedPlanItem(planItemId, operatorId, operatorName);
|
||||||
|
if (changed != 0 && changed != 1) {
|
||||||
|
throw conflict("跳单明细已被并发修改,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean newlySplit = settleInbound(inboundId, pickedPackageCount, taskItems, taskId, operatorId, operatorName);
|
||||||
|
if (newlySplit) {
|
||||||
|
for (PdaPickSettlementPlan.SplitLine line : resolution.partialByInbound()
|
||||||
|
.getOrDefault(inboundId, List.of())) {
|
||||||
|
requireAffected(splits.convergePartialPlanItem(line.loadingPlanItemId(), operatorId, operatorName),
|
||||||
|
"部分拣货装柜明细");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean settleInbound(long parentInboundId, int pickedPackageCount,
|
private boolean settleInbound(long parentInboundId, int pickedPackageCount,
|
||||||
List<LoadingPickItemEntity> taskItems, long taskId,
|
List<LoadingPickItemEntity> taskItems, long taskId,
|
||||||
long operatorId, String operatorName) {
|
long operatorId, String operatorName) {
|
||||||
@@ -379,7 +411,7 @@ public class PdaPickSettlementService {
|
|||||||
|
|
||||||
private static List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
|
private static List<Long> releasedReservationIds(List<InboundPalletAllocationEntity> allocations) {
|
||||||
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
|
return allocations.stream().filter(item -> remaining(item) == 0).map(PdaPickSettlementService::reservationId)
|
||||||
.sorted().toList();
|
.distinct().sorted().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
|
private static List<WarehousePalletLocationClient.SettlementTransferItem> remainingTransferItems(
|
||||||
@@ -422,18 +454,51 @@ public class PdaPickSettlementService {
|
|||||||
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
operatorId, operatorName, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||||
releasedReservationIds, remainingItems));
|
releasedReservationIds, remainingItems));
|
||||||
} catch (FeignException exception) {
|
} catch (FeignException exception) {
|
||||||
|
log.warn("PDA warehouse settlement transport failure taskId={} sourceInboundId={} operationId={} "
|
||||||
|
+ "httpStatus={} releasedReservationIds={} remainingReservations={}",
|
||||||
|
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||||
|
exception.status(), releasedReservationIds, settlementTransferFacts(remainingItems));
|
||||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓储服务不可用,无法结算PDA库位资源");
|
||||||
}
|
}
|
||||||
if (response == null || !response.successful() || response.data() == null
|
if (response == null || !response.successful() || response.data() == null
|
||||||
|| response.data().released() == null || response.data().transferred() == null
|
|| response.data().released() == null || response.data().transferred() == null
|
||||||
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|
|| !sameReleasedReservations(releasedReservationIds, response.data().released())
|
||||||
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
|
|| !sameTransferredResources(remainingItems, response.data().transferred())) {
|
||||||
|
log.warn("PDA warehouse settlement rejected taskId={} sourceInboundId={} operationId={} "
|
||||||
|
+ "responseCode={} requestId={} traceId={} expectedAction=RELEASE_OR_TRANSFER "
|
||||||
|
+ "releasedReservationIds={} remainingReservations={}",
|
||||||
|
taskId, parentInboundId, "PDA-SETTLE-" + taskId + '-' + parentInboundId,
|
||||||
|
response == null ? null : response.code(), response == null ? null : response.requestId(),
|
||||||
|
response == null ? null : response.traceId(), releasedReservationIds,
|
||||||
|
settlementTransferFacts(remainingItems));
|
||||||
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
ResultCode code = response == null ? ResultCode.SERVICE_UNAVAILABLE : ResultCode.of(response.code());
|
||||||
throw new BizException(code, response == null || response.message() == null
|
throw new SettlementFailure(code, response == null || response.message() == null
|
||||||
? "仓储服务原子结算PDA库位资源失败" : response.message());
|
? "仓储服务原子结算PDA库位资源失败" : response.message(),
|
||||||
|
response == null ? null : response.requestId(), response == null ? null : response.traceId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Carries only correlation facts needed by the durable command diagnosis record. */
|
||||||
|
public static final class SettlementFailure extends BizException {
|
||||||
|
private final String requestId;
|
||||||
|
private final String traceId;
|
||||||
|
|
||||||
|
SettlementFailure(ResultCode code, String message, String requestId, String traceId) {
|
||||||
|
super(code, message);
|
||||||
|
this.requestId = requestId;
|
||||||
|
this.traceId = traceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String requestId() { return requestId; }
|
||||||
|
public String traceId() { return traceId; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> settlementTransferFacts(
|
||||||
|
List<WarehousePalletLocationClient.SettlementTransferItem> remainingItems) {
|
||||||
|
return remainingItems.stream().map(item -> item.reservationId() + ":" + item.remainingPackageCount())
|
||||||
|
.sorted().toList();
|
||||||
|
}
|
||||||
|
|
||||||
private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) {
|
private long existingSettlementTarget(long sourceInboundId, String idempotencyKey) {
|
||||||
try {
|
try {
|
||||||
ApiResponse<Long> response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey);
|
ApiResponse<Long> response = warehouse.settlementTarget(internalToken, sourceInboundId, idempotencyKey);
|
||||||
|
|||||||
@@ -317,17 +317,27 @@ public class SmartLoadingService {
|
|||||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||||
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
||||||
checkRouteAndCapacity(booking, others, candidate, request.packageCount());
|
checkRouteAndCapacity(booking, others, candidate, request.packageCount());
|
||||||
|
String before = json(adjustSnapshot(item));
|
||||||
|
LoadingPlanItemEntity desired = new LoadingPlanItemEntity();
|
||||||
|
desired.setPackageCount(request.packageCount());
|
||||||
|
desired.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||||
|
desired.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||||
|
desired.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount())));
|
||||||
|
desired.setPalletCount(estimatedPalletCount(candidate, request.packageCount()));
|
||||||
|
applyCargo(desired, cargo, request.packageCount());
|
||||||
|
if (sameAdjustSnapshot(item, desired)) {
|
||||||
|
return view(plan, booking);
|
||||||
|
}
|
||||||
touch(plan, operatorId, operatorName);
|
touch(plan, operatorId, operatorName);
|
||||||
item.setPackageCount(request.packageCount());
|
item.setPackageCount(desired.getPackageCount());
|
||||||
item.setGrossWeightKg(unit(candidate.unitGrossWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
item.setGrossWeightKg(desired.getGrossWeightKg());
|
||||||
item.setNetWeightKg(unit(candidate.unitNetWeightKg()).multiply(BigDecimal.valueOf(request.packageCount())));
|
item.setNetWeightKg(desired.getNetWeightKg());
|
||||||
item.setVolumeCbm(unit(candidate.unitVolumeCbm()).multiply(BigDecimal.valueOf(request.packageCount())));
|
item.setVolumeCbm(desired.getVolumeCbm());
|
||||||
item.setPalletCount(estimatedPalletCount(candidate, request.packageCount())); applyCargo(item, cargo, request.packageCount());
|
item.setPalletCount(desired.getPalletCount()); applyCargo(item, cargo, request.packageCount());
|
||||||
item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName);
|
item.setUpdatedBy(operatorId); item.setUpdatedByName(operatorName);
|
||||||
if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改");
|
if (repository.update(item) != 1) throw conflict("装柜明细已被并发修改");
|
||||||
history(planId, itemId, "ADJUST_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation,
|
history(planId, itemId, "ADJUST_ITEM", plan.getPlanStatus(), plan.getPlanStatus(), operation,
|
||||||
json(values("version", request.expectedPlanVersion(), "packageCount", item.getPackageCount())),
|
before, json(values("requestHash", requestHash, "business", adjustSnapshot(item))),
|
||||||
json(values("requestHash", requestHash, "packageCount", request.packageCount())),
|
|
||||||
operatorId, operatorName);
|
operatorId, operatorName);
|
||||||
loadingPickService.sync(planId, item);
|
loadingPickService.sync(planId, item);
|
||||||
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
return view(repository.plan(planId), requireBooking(plan.getContainerBookingId()));
|
||||||
@@ -349,6 +359,9 @@ public class SmartLoadingService {
|
|||||||
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
LoadingPlanEntity plan = editing(planId, request.expectedPlanVersion(), operatorId, operatorName);
|
||||||
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
LoadingPlanItemEntity item = repository.item(planId, itemId);
|
||||||
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "装柜明细不存在");
|
||||||
|
if (Objects.equals(item.getWarehouseRemark(), remark)) {
|
||||||
|
return view(plan, requireBooking(plan.getContainerBookingId()));
|
||||||
|
}
|
||||||
String before = json(values("warehouseRemark", item.getWarehouseRemark(), "version", item.getVersion()));
|
String before = json(values("warehouseRemark", item.getWarehouseRemark(), "version", item.getVersion()));
|
||||||
try {
|
try {
|
||||||
touch(plan, operatorId, operatorName);
|
touch(plan, operatorId, operatorName);
|
||||||
@@ -480,6 +493,33 @@ public class SmartLoadingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean sameAdjustSnapshot(LoadingPlanItemEntity current, LoadingPlanItemEntity desired) {
|
||||||
|
return Objects.equals(current.getPackageCount(), desired.getPackageCount())
|
||||||
|
&& decimalEquals(current.getGrossWeightKg(), desired.getGrossWeightKg())
|
||||||
|
&& decimalEquals(current.getNetWeightKg(), desired.getNetWeightKg())
|
||||||
|
&& decimalEquals(current.getVolumeCbm(), desired.getVolumeCbm())
|
||||||
|
&& Objects.equals(current.getPalletCount(), desired.getPalletCount())
|
||||||
|
&& Objects.equals(current.getSkuCode(), desired.getSkuCode())
|
||||||
|
&& Objects.equals(current.getProductNameEn(), desired.getProductNameEn())
|
||||||
|
&& Objects.equals(current.getUnitsPerCarton(), desired.getUnitsPerCarton())
|
||||||
|
&& Objects.equals(current.getProductQuantity(), desired.getProductQuantity())
|
||||||
|
&& Objects.equals(current.getCustomerRemark(), desired.getCustomerRemark())
|
||||||
|
&& Objects.equals(current.getInternalRemark(), desired.getInternalRemark());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean decimalEquals(BigDecimal left, BigDecimal right) {
|
||||||
|
return left == null ? right == null : right != null && left.compareTo(right) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, Object> adjustSnapshot(LoadingPlanItemEntity item) {
|
||||||
|
return values("packageCount", item.getPackageCount(), "grossWeightKg", item.getGrossWeightKg(),
|
||||||
|
"netWeightKg", item.getNetWeightKg(), "volumeCbm", item.getVolumeCbm(),
|
||||||
|
"palletCount", item.getPalletCount(), "skuCode", item.getSkuCode(),
|
||||||
|
"productNameEn", item.getProductNameEn(), "unitsPerCarton", item.getUnitsPerCarton(),
|
||||||
|
"productQuantity", item.getProductQuantity(), "customerRemark", item.getCustomerRemark(),
|
||||||
|
"internalRemark", item.getInternalRemark(), "version", item.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) {
|
private OrderCargoClient.CargoBinding loadCargo(Candidate candidate) {
|
||||||
try {
|
try {
|
||||||
ApiResponse<OrderCargoClient.CargoBinding> response = orderCargoClient.detail(internalToken,
|
ApiResponse<OrderCargoClient.CargoBinding> response = orderCargoClient.detail(internalToken,
|
||||||
|
|||||||
+13
@@ -8,6 +8,7 @@ import jakarta.annotation.Resource;
|
|||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class WarehouseSupplierSnapshotService {
|
public class WarehouseSupplierSnapshotService {
|
||||||
@@ -18,6 +19,18 @@ public class WarehouseSupplierSnapshotService {
|
|||||||
return resolve(supplierId, suppliedName, "BOOKING");
|
return resolve(supplierId, suppliedName, "BOOKING");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<WarehouseSupplierClient.Snapshot> bookingCandidates(String carrierCode) {
|
||||||
|
try {
|
||||||
|
var response = client.candidates(internalToken, "BOOKING", carrierCode);
|
||||||
|
if (response == null || !response.successful() || response.data() == null) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订舱代理候选服务暂不可用");
|
||||||
|
}
|
||||||
|
return response.data();
|
||||||
|
} catch (FeignException exception) {
|
||||||
|
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订舱代理候选服务暂不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
|
public WarehouseSupplierClient.Snapshot trucking(Long supplierId, String suppliedName) {
|
||||||
return resolve(supplierId, suppliedName, "TRUCKING");
|
return resolve(supplierId, suppliedName, "TRUCKING");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import org.springframework.util.StringUtils;
|
|||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/** 分柜、预装清单及其主状态推进;不校验资料完整度和客户确认。 */
|
/** 分柜、预装清单及其主状态推进;不校验资料完整度和客户确认。 */
|
||||||
@Service
|
@Service
|
||||||
@@ -73,8 +74,8 @@ public class SplitPreloadService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* A running PDA task may restore or add an unpicked allocation after the plan-level preload
|
* A running PDA task may restore or add an unpicked allocation after the plan-level preload
|
||||||
* confirmation. CONFIRMING remains immutable; CONFIRMED is accepted because the caller owns
|
* confirmation. A running PDA task owns the mutation lock, so CONFIRMING and CONFIRMED
|
||||||
* the active PDA task lock and will synchronise the restored loading item immediately.
|
* lifecycles may be reused while the restored loading item is synchronised atomically.
|
||||||
*/
|
*/
|
||||||
@Transactional
|
@Transactional
|
||||||
public void ensureForRunningLoadingAllocation(long tenantId, long inboundId, long operatorId,
|
public void ensureForRunningLoadingAllocation(long tenantId, long inboundId, long operatorId,
|
||||||
@@ -96,7 +97,8 @@ public class SplitPreloadService {
|
|||||||
}
|
}
|
||||||
if (plan != null) {
|
if (plan != null) {
|
||||||
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|
if (!PreloadStatus.PENDING.equals(plan.getStatus())
|
||||||
&& !(running && PreloadStatus.CONFIRMED.equals(plan.getStatus()))) {
|
&& !(running && Set.of(PreloadStatus.CONFIRMING, PreloadStatus.CONFIRMED)
|
||||||
|
.contains(plan.getStatus()))) {
|
||||||
throw new BizException(ResultCode.CONFLICT, "预装计划已进入确认流程,不能修改智能装柜分配");
|
throw new BizException(ResultCode.CONFLICT, "预装计划已进入确认流程,不能修改智能装柜分配");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ DELIMITER ;
|
|||||||
CALL phase2_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
|
CALL phase2_carton_label_add_column('label_version', 'INT UNSIGNED NOT NULL DEFAULT 1 COMMENT ''内部兼容批次字段,固定为1且不进入箱码'' AFTER carton_index');
|
||||||
CALL phase2_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
|
CALL phase2_carton_label_add_column('batch_status', 'VARCHAR(16) NOT NULL DEFAULT ''ACTIVE'' COMMENT ''批次状态:ACTIVE有效、VOID已作废'' AFTER label_version');
|
||||||
CALL phase2_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
|
CALL phase2_carton_label_add_column('cargo_fingerprint', 'CHAR(64) NULL COMMENT ''影响标签字段的SHA-256指纹'' AFTER batch_status');
|
||||||
CALL phase2_carton_label_add_column('sku_code', 'VARCHAR(128) NULL COMMENT ''箱唛绑定的SKU快照;二维码内容不变'' AFTER cargo_name');
|
|
||||||
CALL phase2_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
|
CALL phase2_carton_label_add_column('voided_at', 'DATETIME(3) NULL COMMENT ''批次作废时间'' AFTER cargo_fingerprint');
|
||||||
DROP PROCEDURE phase2_carton_label_add_column;
|
DROP PROCEDURE phase2_carton_label_add_column;
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
|||||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
||||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
|
||||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_order (
|
|||||||
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
destination_name VARCHAR(128) NULL COMMENT '目的仓名称快照',
|
||||||
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
hold_flag TINYINT(1) NOT NULL DEFAULT 0 COMMENT '当前是否扣货',
|
||||||
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
current_hold_reason VARCHAR(500) NULL COMMENT '当前扣货理由快照',
|
||||||
last_overseas_hold_version BIGINT UNSIGNED NULL COMMENT '海外暂扣事件最新聚合版本,用于阻止乱序覆盖',
|
|
||||||
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
total_package_count INT UNSIGNED NULL COMMENT '确认后的总箱数',
|
||||||
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片含托盘毛重汇总kg',
|
actual_gross_weight_kg DECIMAL(18,3) NULL COMMENT '已确认称重图片含托盘毛重汇总kg',
|
||||||
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
chargeable_weight_kg DECIMAL(18,3) NULL COMMENT '当前费用快照的计费重量kg',
|
||||||
@@ -197,7 +196,6 @@ CREATE TABLE IF NOT EXISTS fl_inbound_carton_label (
|
|||||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '订单货物明细ID,不建立跨库外键',
|
||||||
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
cargo_item_no INT UNSIGNED NOT NULL COMMENT '订单货物明细序号快照',
|
||||||
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
cargo_name VARCHAR(255) NULL COMMENT '货物名称快照',
|
||||||
sku_code VARCHAR(128) NULL COMMENT '箱唛绑定的SKU快照;二维码内容不变',
|
|
||||||
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
detail_package_count INT UNSIGNED NOT NULL COMMENT '该订单货物明细箱数快照',
|
||||||
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
inbound_code VARCHAR(40) NOT NULL COMMENT '入仓码快照',
|
||||||
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
box_code VARCHAR(80) NOT NULL COMMENT '固定二维码内容:入仓码#总箱数-箱下标',
|
||||||
|
|||||||
@@ -166,9 +166,12 @@ CREATE TABLE IF NOT EXISTS fl_container_transport_history (
|
|||||||
after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
after_container_no VARCHAR(64) NULL COMMENT '变更后真实集装箱号',
|
||||||
before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',
|
before_voyage_no VARCHAR(64) NULL COMMENT '变更前航次号',
|
||||||
after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
after_voyage_no VARCHAR(64) NULL COMMENT '变更后航次号',
|
||||||
|
operation_id VARCHAR(64) NOT NULL COMMENT '运输字段操作幂等ID',
|
||||||
|
request_hash CHAR(64) NOT NULL COMMENT '运输字段请求哈希',
|
||||||
reason VARCHAR(500) NULL COMMENT '运输字段变更原因',
|
reason VARCHAR(500) NULL COMMENT '运输字段变更原因',
|
||||||
operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
operator_id BIGINT UNSIGNED NOT NULL COMMENT '变更操作人ID',
|
||||||
operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',
|
operator_name VARCHAR(64) NULL COMMENT '变更操作人名称快照',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '变更记录创建时间', PRIMARY KEY(id),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '变更记录创建时间', PRIMARY KEY(id),
|
||||||
|
UNIQUE KEY uk_container_transport_history_operation (container_booking_id,operator_id,operation_id),
|
||||||
KEY idx_container_transport_history (container_booking_id,created_at,id)
|
KEY idx_container_transport_history (container_booking_id,created_at,id)
|
||||||
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
|
) ENGINE=InnoDB COMMENT='柜次真实运输字段不可变变更审计';
|
||||||
|
|||||||
@@ -102,8 +102,8 @@ CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
|||||||
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID',
|
event_id VARCHAR(64) NOT NULL COMMENT '跨区事件全局唯一ID',
|
||||||
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
|
||||||
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本',
|
event_version INT UNSIGNED NOT NULL COMMENT '事件契约版本',
|
||||||
aggregate_id VARCHAR(64) NOT NULL COMMENT '跨区业务聚合ID',
|
aggregate_id VARCHAR(64) NOT NULL COMMENT '货柜聚合ID',
|
||||||
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '跨区业务聚合版本',
|
aggregate_version BIGINT UNSIGNED NOT NULL COMMENT '货柜聚合版本',
|
||||||
payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
payload_hash VARCHAR(80) NOT NULL COMMENT '规范化负载SHA-256摘要',
|
||||||
payload_json JSON NOT NULL COMMENT '原始事件负载',
|
payload_json JSON NOT NULL COMMENT '原始事件负载',
|
||||||
status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
status VARCHAR(16) NOT NULL COMMENT 'RECEIVED或PROCESSED',
|
||||||
@@ -111,7 +111,7 @@ CREATE TABLE IF NOT EXISTS fl_container_event_inbox (
|
|||||||
processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
processed_at DATETIME(3) NULL COMMENT '处理完成时间(UTC)',
|
||||||
PRIMARY KEY (event_id),
|
PRIMARY KEY (event_id),
|
||||||
KEY idx_container_event_aggregate (aggregate_id, aggregate_version)
|
KEY idx_container_event_aggregate (aggregate_id, aggregate_version)
|
||||||
) ENGINE=InnoDB COMMENT='海外到国内业务事件消费Inbox';
|
) ENGINE=InnoDB COMMENT='海外货柜事件消费Inbox';
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS phase3_container_real_unit_columns;
|
DROP PROCEDURE IF EXISTS phase3_container_real_unit_columns;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
@@ -564,7 +564,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
|||||||
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
container_booking_id BIGINT UNSIGNED NOT NULL COMMENT '稳定柜次ID',
|
||||||
cabinet_no VARCHAR(64) NOT NULL COMMENT '稳定内部柜次号快照',
|
cabinet_no VARCHAR(64) NOT NULL COMMENT '稳定内部柜次号快照',
|
||||||
container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份或随运输号变更重写',
|
container_no VARCHAR(64) NULL COMMENT '废弃兼容字段;不得作为任务身份或随运输号变更重写',
|
||||||
task_status VARCHAR(16) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING拣货装柜中、COMPLETED已完成',
|
task_status VARCHAR(24) NOT NULL DEFAULT 'LOADING' COMMENT 'PDA任务状态:LOADING、COMPLETING、COMPLETION_FAILED、COMPLETED',
|
||||||
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
started_by BIGINT UNSIGNED NOT NULL COMMENT '开始装柜人',
|
||||||
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
started_by_name VARCHAR(64) NULL COMMENT '开始装柜人名称',
|
||||||
started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
|
started_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '开始时间',
|
||||||
@@ -577,9 +577,52 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_task (
|
|||||||
UNIQUE KEY uk_loading_pick_task_booking (container_booking_id),
|
UNIQUE KEY uk_loading_pick_task_booking (container_booking_id),
|
||||||
KEY idx_loading_pick_container (container_no, task_status, started_at),
|
KEY idx_loading_pick_container (container_no, task_status, started_at),
|
||||||
CONSTRAINT fk_loading_pick_task_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
CONSTRAINT fk_loading_pick_task_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
||||||
CONSTRAINT chk_loading_pick_task_status CHECK (task_status IN ('LOADING', 'COMPLETED'))
|
CONSTRAINT chk_loading_pick_task_status CHECK (task_status IN ('LOADING','COMPLETING','COMPLETION_FAILED','COMPLETED'))
|
||||||
) ENGINE=InnoDB COMMENT='PDA装柜拣货任务';
|
) ENGINE=InnoDB COMMENT='PDA装柜拣货任务';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fl_loading_completion_command (
|
||||||
|
id BIGINT UNSIGNED NOT NULL COMMENT '可靠装柜完成命令ID',
|
||||||
|
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA任务ID',
|
||||||
|
operation_id VARCHAR(128) NOT NULL COMMENT '客户端全局幂等操作ID',
|
||||||
|
request_hash CHAR(64) NOT NULL COMMENT '规范化请求摘要',
|
||||||
|
command_status VARCHAR(16) NOT NULL COMMENT 'PROCESSING、RETRYABLE、FAILED、COMPLETED',
|
||||||
|
expected_task_version INT UNSIGNED NOT NULL COMMENT '受理时客户端任务版本',
|
||||||
|
remark VARCHAR(500) NULL COMMENT '完成备注',
|
||||||
|
operator_id BIGINT UNSIGNED NOT NULL COMMENT '操作人ID',
|
||||||
|
operator_name VARCHAR(64) NULL COMMENT '操作人名称快照',
|
||||||
|
attempt_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'worker认领次数',
|
||||||
|
next_retry_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '下次处理时间',
|
||||||
|
lease_owner VARCHAR(128) NULL COMMENT 'worker租约持有者',
|
||||||
|
lease_until DATETIME(3) NULL COMMENT 'worker租约截止时间',
|
||||||
|
failure_stage VARCHAR(40) NULL COMMENT '脱敏失败阶段',
|
||||||
|
failure_code VARCHAR(32) NULL COMMENT '下游或业务错误码',
|
||||||
|
failure_request_id VARCHAR(64) NULL COMMENT '下游请求ID',
|
||||||
|
failure_trace_id VARCHAR(64) NULL COMMENT '下游链路ID',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
completed_at DATETIME(3) NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_loading_completion_operation (operation_id),
|
||||||
|
KEY idx_loading_completion_dispatch (command_status,next_retry_at,lease_until),
|
||||||
|
CONSTRAINT fk_loading_completion_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task(id),
|
||||||
|
CONSTRAINT chk_loading_completion_status CHECK (command_status IN ('PROCESSING','RETRYABLE','FAILED','COMPLETED'))
|
||||||
|
) ENGINE=InnoDB COMMENT='PDA装柜完成可靠异步命令';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS fl_loading_completion_checkpoint (
|
||||||
|
command_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
tenant_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
inbound_id BIGINT UNSIGNED NOT NULL,
|
||||||
|
owner_sequence INT UNSIGNED NOT NULL,
|
||||||
|
checkpoint_stage VARCHAR(24) NOT NULL COMMENT 'READY、SETTLED、INBOUND_COMPLETED',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||||
|
PRIMARY KEY (command_id,tenant_id,inbound_id),
|
||||||
|
KEY idx_loading_completion_owner (command_id,owner_sequence),
|
||||||
|
CONSTRAINT fk_loading_completion_checkpoint_command FOREIGN KEY (command_id)
|
||||||
|
REFERENCES fl_loading_completion_command(id),
|
||||||
|
CONSTRAINT chk_loading_completion_checkpoint CHECK (checkpoint_stage IN ('READY','SETTLED','INBOUND_COMPLETED'))
|
||||||
|
) ENGINE=InnoDB COMMENT='PDA装柜完成按入库单恢复检查点';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_pick_item (
|
CREATE TABLE IF NOT EXISTS fl_loading_pick_item (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
||||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID',
|
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货任务ID',
|
||||||
@@ -623,66 +666,6 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_history (
|
|||||||
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
CONSTRAINT fk_loading_pick_history_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id)
|
||||||
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
) ENGINE=InnoDB COMMENT='PDA拣货幂等和操作审计历史';
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet (
|
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场作业托盘ID',
|
|
||||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
|
||||||
loading_plan_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划ID快照',
|
|
||||||
warehouse_id BIGINT UNSIGNED NOT NULL COMMENT '作业仓库ID',
|
|
||||||
warehouse_pallet_id BIGINT UNSIGNED NULL COMMENT '仓库实物托盘ID;未绑定仓库资源时为空',
|
|
||||||
pallet_code VARCHAR(64) NOT NULL COMMENT '现场托盘号',
|
|
||||||
open_operation_id VARCHAR(128) NOT NULL COMMENT '开托幂等操作号',
|
|
||||||
seal_operation_id VARCHAR(128) NULL COMMENT '封托幂等操作号',
|
|
||||||
pallet_status VARCHAR(16) NOT NULL DEFAULT 'OPEN' COMMENT '托盘状态:OPEN打托中、SEALED已封托、CANCELLED已取消',
|
|
||||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建操作人ID',
|
|
||||||
created_by_name VARCHAR(64) NULL COMMENT '创建操作人名称快照',
|
|
||||||
sealed_by BIGINT UNSIGNED NULL COMMENT '封托操作人ID',
|
|
||||||
sealed_by_name VARCHAR(64) NULL COMMENT '封托操作人名称快照',
|
|
||||||
sealed_at DATETIME(3) NULL COMMENT '封托时间',
|
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_code (pick_task_id, pallet_code),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_open_operation (open_operation_id),
|
|
||||||
UNIQUE KEY uk_loading_work_pallet_seal_operation (seal_operation_id),
|
|
||||||
KEY idx_loading_work_pallet_plan (loading_plan_id, pallet_status, id),
|
|
||||||
CONSTRAINT fk_loading_work_pallet_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
|
||||||
CONSTRAINT fk_loading_work_pallet_plan FOREIGN KEY (loading_plan_id) REFERENCES fl_loading_plan (id),
|
|
||||||
CONSTRAINT chk_loading_work_pallet_status CHECK (pallet_status IN ('OPEN','SEALED','CANCELLED'))
|
|
||||||
) ENGINE=InnoDB COMMENT='一件代发PDA现场实际托盘;允许同托盘承载多个客户订单';
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS fl_loading_work_pallet_carton (
|
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '一件代发现场托盘箱唛绑定ID',
|
|
||||||
pick_task_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA装柜拣货任务ID',
|
|
||||||
work_pallet_id BIGINT UNSIGNED NOT NULL COMMENT '现场作业托盘ID',
|
|
||||||
loading_plan_item_id BIGINT UNSIGNED NOT NULL COMMENT '装柜计划货物明细ID',
|
|
||||||
pick_item_id BIGINT UNSIGNED NOT NULL COMMENT 'PDA拣货明细ID',
|
|
||||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '箱唛所属客户ID',
|
|
||||||
inbound_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓单ID',
|
|
||||||
inbound_detail_id BIGINT UNSIGNED NOT NULL COMMENT '来源入仓货物明细ID',
|
|
||||||
carton_label_id BIGINT UNSIGNED NOT NULL COMMENT '稳定箱唛记录ID',
|
|
||||||
box_code VARCHAR(80) NOT NULL COMMENT '稳定箱唛箱码快照',
|
|
||||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照',
|
|
||||||
units_per_carton INT UNSIGNED NULL COMMENT '每箱产品数量快照',
|
|
||||||
bound_by BIGINT UNSIGNED NOT NULL COMMENT '最后绑定操作人ID',
|
|
||||||
bound_by_name VARCHAR(64) NULL COMMENT '最后绑定操作人名称快照',
|
|
||||||
operation_id VARCHAR(128) NOT NULL COMMENT '最近一次绑定或移托幂等操作号',
|
|
||||||
version INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次上托时间',
|
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后移托时间',
|
|
||||||
PRIMARY KEY (id),
|
|
||||||
UNIQUE KEY uk_loading_work_carton_task_label (pick_task_id, carton_label_id),
|
|
||||||
UNIQUE KEY uk_loading_work_carton_operation (operation_id),
|
|
||||||
KEY idx_loading_work_carton_pallet (work_pallet_id, sku_code, id),
|
|
||||||
KEY idx_loading_work_carton_plan_item (loading_plan_item_id, id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_pallet FOREIGN KEY (work_pallet_id) REFERENCES fl_loading_work_pallet (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_plan_item FOREIGN KEY (loading_plan_item_id) REFERENCES fl_loading_plan_item (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_pick_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
|
|
||||||
CONSTRAINT fk_loading_work_carton_label FOREIGN KEY (carton_label_id) REFERENCES fl_inbound_carton_label (id),
|
|
||||||
CONSTRAINT chk_loading_work_carton_units CHECK (units_per_carton IS NULL OR units_per_carton > 0)
|
|
||||||
) ENGINE=InnoDB COMMENT='一件代发PDA逐箱上托事实;客户订单不合并但实际托盘可混托';
|
|
||||||
|
|
||||||
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
DROP PROCEDURE IF EXISTS repair_phase3_loading_pick_task_foreign_keys;
|
||||||
DELIMITER $$
|
DELIMITER $$
|
||||||
CREATE PROCEDURE repair_phase3_loading_pick_task_foreign_keys()
|
CREATE PROCEDURE repair_phase3_loading_pick_task_foreign_keys()
|
||||||
@@ -879,7 +862,6 @@ BEGIN
|
|||||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||||
WHERE pick_item.loading_plan_item_id = item.id
|
WHERE pick_item.loading_plan_item_id = item.id
|
||||||
AND pick_task.loading_plan_id = OLD.id
|
AND pick_task.loading_plan_id = OLD.id
|
||||||
AND pick_task.task_status = 'LOADING'
|
|
||||||
AND (
|
AND (
|
||||||
(pick_item.item_status = 'PICKED'
|
(pick_item.item_status = 'PICKED'
|
||||||
AND pick_item.picked_package_count = item.package_count)
|
AND pick_item.picked_package_count = item.package_count)
|
||||||
@@ -1078,8 +1060,10 @@ BEGIN
|
|||||||
OR NOT (NEW.cabinet_no <=> OLD.cabinet_no) THEN
|
OR NOT (NEW.cabinet_no <=> OLD.cabinet_no) THEN
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PDA拣货任务稳定归属不可修改';
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PDA拣货任务稳定归属不可修改';
|
||||||
END IF;
|
END IF;
|
||||||
IF NOT (OLD.task_status = 'LOADING' AND NEW.task_status IN ('LOADING', 'COMPLETED')) THEN
|
IF NOT ((OLD.task_status = 'LOADING' AND NEW.task_status IN ('LOADING','COMPLETING'))
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PDA拣货任务状态只允许完成';
|
OR (OLD.task_status = 'COMPLETING' AND NEW.task_status IN ('COMPLETING','COMPLETION_FAILED','COMPLETED'))
|
||||||
|
OR (OLD.task_status = 'COMPLETION_FAILED' AND NEW.task_status = 'COMPLETING')) THEN
|
||||||
|
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'PDA拣货任务状态流转非法';
|
||||||
END IF;
|
END IF;
|
||||||
END$$
|
END$$
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -330,9 +330,10 @@ paths:
|
|||||||
/api/platform/v1/containers/{id}/transport:
|
/api/platform/v1/containers/{id}/transport:
|
||||||
patch:
|
patch:
|
||||||
summary: 修改集装箱号和航次
|
summary: 修改集装箱号和航次
|
||||||
description: 需要 platform:firstleg:container:edit 权限;LOADING 仅开放此白名单,COMPLETED 全部只读
|
description: 需要 platform:firstleg:container:edit 权限;LOADING 仅开放此白名单,COMPLETED 全部只读;相同幂等键精确重放零新增历史,相同业务字段为无效变更且不递增版本、不写历史
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/ContainerBookingId'}
|
- {$ref: '#/components/parameters/ContainerBookingId'}
|
||||||
|
- {$ref: '#/components/parameters/IdempotencyKey'}
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content:
|
content:
|
||||||
@@ -755,11 +756,14 @@ paths:
|
|||||||
get:
|
get:
|
||||||
summary: 查询称重可选托盘快照
|
summary: 查询称重可选托盘快照
|
||||||
description: >-
|
description: >-
|
||||||
需要入库测量查看或称重编辑权限。Firstleg 按入库单 warehouseId 查询 Warehouse 公共托盘快照;
|
需要入库测量查看或称重编辑权限。Firstleg 按入库单 warehouseId 分页查询 Warehouse 当前称重托盘候选;
|
||||||
仅限定同仓且未删除,不校验托盘状态或占用资格,Admin 不需要 platform:pallet:list。
|
Warehouse 校验平台公共作用域、启用国内仓、同仓归属和当前未删除资格。托盘可复用,不按预占或占用过滤,Admin 不需要 platform:pallet:list。
|
||||||
parameters:
|
parameters:
|
||||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||||
|
- {name: page, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, default: 1}}
|
||||||
|
- {name: size, in: query, required: false, schema: {type: integer, format: int64, minimum: 1, maximum: 100, default: 20}}
|
||||||
|
- {name: keyword, in: query, required: false, description: 匹配托盘编码或类型, schema: {type: string, maxLength: 50}}
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 成功或业务失败,按响应体 code 判断
|
description: 成功或业务失败,按响应体 code 判断
|
||||||
@@ -770,7 +774,7 @@ paths:
|
|||||||
properties:
|
properties:
|
||||||
code: {type: string}
|
code: {type: string}
|
||||||
message: {type: string}
|
message: {type: string}
|
||||||
data: {type: array, items: {$ref: '#/components/schemas/WeightPalletSnapshot'}}
|
data: {$ref: '#/components/schemas/WeightPalletOptionPage'}
|
||||||
'403': {description: 无入库测量查看或称重编辑权限}
|
'403': {description: 无入库测量查看或称重编辑权限}
|
||||||
/api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:
|
/api/platform/v1/first-leg/inbounds/{inboundId}/weight-results:
|
||||||
post:
|
post:
|
||||||
@@ -1294,6 +1298,18 @@ components:
|
|||||||
palletType: {type: string}
|
palletType: {type: string}
|
||||||
capacityCbm: {type: number, format: double, minimum: 0.0001}
|
capacityCbm: {type: number, format: double, minimum: 0.0001}
|
||||||
tareWeightKg: {type: number, format: double, minimum: 0, multipleOf: 0.001}
|
tareWeightKg: {type: number, format: double, minimum: 0, multipleOf: 0.001}
|
||||||
|
WeightPalletOptionPage:
|
||||||
|
type: object
|
||||||
|
required: [records, page, size, total, pages]
|
||||||
|
properties:
|
||||||
|
records:
|
||||||
|
type: array
|
||||||
|
maxItems: 100
|
||||||
|
items: {$ref: '#/components/schemas/WeightPalletSnapshot'}
|
||||||
|
page: {type: integer, format: int64, minimum: 1}
|
||||||
|
size: {type: integer, format: int64, minimum: 1, maximum: 100}
|
||||||
|
total: {type: integer, format: int64, minimum: 0}
|
||||||
|
pages: {type: integer, format: int64, minimum: 0}
|
||||||
CreateWeightResultRequest:
|
CreateWeightResultRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [fileId, palletId]
|
required: [fileId, palletId]
|
||||||
|
|||||||
@@ -427,10 +427,11 @@ components:
|
|||||||
itemVersion: {type: integer, format: int32, minimum: 0, description: 明细乐观锁版本;更新仓库备注时作为 expectedItemVersion}
|
itemVersion: {type: integer, format: int32, minimum: 0, description: 明细乐观锁版本;更新仓库备注时作为 expectedItemVersion}
|
||||||
OperationHistory:
|
OperationHistory:
|
||||||
type: object
|
type: object
|
||||||
required: [historyId, actionType, operatorId, occurredAt]
|
required: [historyId, operationId, actionType, operatorId, occurredAt]
|
||||||
properties:
|
properties:
|
||||||
historyId: {$ref: '#/components/schemas/LongId'}
|
historyId: {$ref: '#/components/schemas/LongId'}
|
||||||
itemId: {$ref: '#/components/schemas/NullableLongId'}
|
itemId: {$ref: '#/components/schemas/NullableLongId'}
|
||||||
|
operationId: {type: string, minLength: 1, maxLength: 128, description: 稳定业务动作或幂等事件标识;客户端据此识别重放,不按展示文案去重}
|
||||||
actionType: {type: string, description: 操作类型}
|
actionType: {type: string, description: 操作类型}
|
||||||
fromStatus: {type: string, nullable: true, description: 操作前状态}
|
fromStatus: {type: string, nullable: true, description: 操作前状态}
|
||||||
toStatus: {type: string, nullable: true, description: 操作后状态}
|
toStatus: {type: string, nullable: true, description: 操作后状态}
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ paths:
|
|||||||
/api/platform/v1/ocean-bookings/link-candidates:
|
/api/platform/v1/ocean-bookings/link-candidates:
|
||||||
get:
|
get:
|
||||||
summary: Page released booking requirement candidates for one container
|
summary: Page released booking requirement candidates for one container
|
||||||
description: Requires platform:firstleg:ocean-booking:list. Returns only linkable requirement candidates: RELEASED, remainingQuantity greater than zero, and exact warehouseId, channelCode and containerType. Capacity and max weight are derived display snapshots and never eligibility fields. Allocated quantity counts only ACTIVE containers still pointing to the same booking requirement and is calculated by one bounded set query; booking detail calls are not used. An empty result is a normal successful page. Final target-container mutability is revalidated by the link command.
|
description: >-
|
||||||
|
Requires platform:firstleg:ocean-booking:list. Returns only linkable requirement candidates:
|
||||||
|
RELEASED, remainingQuantity greater than zero, and exact warehouseId, channelCode and containerType.
|
||||||
|
Capacity and max weight are derived display snapshots and never eligibility fields. Allocated quantity
|
||||||
|
counts only ACTIVE containers still pointing to the same booking requirement and is calculated by one
|
||||||
|
bounded set query; booking detail calls are not used. An empty result is a normal successful page.
|
||||||
|
Final target-container mutability is revalidated by the link command.
|
||||||
x-permission: platform:firstleg:ocean-booking:list
|
x-permission: platform:firstleg:ocean-booking:list
|
||||||
parameters:
|
parameters:
|
||||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
@@ -62,6 +68,19 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema: {type: array, items: {$ref: '#/components/schemas/ChannelOption'}}
|
schema: {type: array, items: {$ref: '#/components/schemas/ChannelOption'}}
|
||||||
|
/api/platform/v1/ocean-bookings/booking-agents:
|
||||||
|
get:
|
||||||
|
summary: List ACTIVE BOOKING agents associated with one carrier code
|
||||||
|
description: Requires platform:firstleg:ocean-booking:list. Filtering is enforced by Warehouse master data; clients must not infer associations.
|
||||||
|
x-permission: platform:firstleg:ocean-booking:list
|
||||||
|
parameters:
|
||||||
|
- {name: carrierCode, in: query, required: true, schema: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE, OTHER]}}
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Server-filtered booking agent options
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {type: array, items: {$ref: '#/components/schemas/BookingAgentOption'}}
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
OceanBookingId: {name: oceanBookingId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
OceanBookingId: {name: oceanBookingId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
@@ -121,6 +140,13 @@ components:
|
|||||||
channelName: {type: string}
|
channelName: {type: string}
|
||||||
priceModel: {type: string}
|
priceModel: {type: string}
|
||||||
pricingTaxModes: {type: array, items: {type: string}}
|
pricingTaxModes: {type: array, items: {type: string}}
|
||||||
|
BookingAgentOption:
|
||||||
|
type: object
|
||||||
|
required: [supplierId, name, carrierCodes]
|
||||||
|
properties:
|
||||||
|
supplierId: {type: integer, format: int64}
|
||||||
|
name: {type: string}
|
||||||
|
carrierCodes: {type: array, items: {type: string, enum: [MATSON, SML, ZIM, COSCO, EMC, HEDE, OTHER]}}
|
||||||
LinkCandidate:
|
LinkCandidate:
|
||||||
type: object
|
type: object
|
||||||
required: [oceanBookingId, bookingNo, requirementId, containerType, capacityCbm, maxWeightKg, requiredQuantity, allocatedQuantity, remainingQuantity, customerName, warehouseName, channelName]
|
required: [oceanBookingId, bookingNo, requirementId, containerType, capacityCbm, maxWeightKg, requiredQuantity, allocatedQuantity, remainingQuantity, customerName, warehouseName, channelName]
|
||||||
|
|||||||
@@ -106,117 +106,24 @@ paths:
|
|||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/complete:
|
||||||
post:
|
post:
|
||||||
summary: PDA拣货全部处理后人工确认装柜完成
|
summary: PDA拣货全部处理后人工确认装柜完成
|
||||||
description: PDA拣货只锁定数量和仓储资源。本接口先无写入校验全部关联入库单的RELEASED订舱、稳定bridge和五类ACTIVE证据;拖车状态不是前置门禁。若当前稳定containerBookingId没有有效拖车则直接继续;恰有一条有效拖车时由服务端加锁推进为COMPLETED并写历史;多条有效拖车或并发修改返回A0409且全部Firstleg本地状态回滚。随后在同一事务内结算资源、完成task/plan并推进inbound为LOADED。同operationId重放返回冻结结果,不重复推进拖车或写历史。
|
description: 短事务校验任务版本、全部明细和operationId请求摘要,持久化可靠完成命令并将任务置为COMPLETING后立即返回PROCESSING事实。后台worker按稳定入库单顺序以checkpoint恢复Order、Warehouse、入库状态、计划和事件;请求事务内不调用任何远程服务。同operationId重放立即返回相同PROCESSING或最终结果;不同operationId在处理期间返回A0409。
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||||
requestBody:
|
requestBody:
|
||||||
required: true
|
required: true
|
||||||
content: {application/json: {schema: {$ref: '#/components/schemas/CompleteLoadingRequest'}}}
|
content: {application/json: {schema: {$ref: '#/components/schemas/CompleteLoadingRequest'}}}
|
||||||
responses:
|
responses:
|
||||||
'200': {description: 完成任务;存在未处理明细或版本冲突时返回A0409}
|
'200': {description: 已可靠受理;taskStatus=COMPLETING且workflowStatus=PROCESSING,或幂等返回最终COMPLETED结果;冲突返回A0409}
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/operations:
|
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/operations:
|
||||||
get:
|
get:
|
||||||
summary: Read immutable task-level claim, assignment and review audit records
|
summary: Read immutable task-level claim, assignment and review audit records
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
||||||
responses:
|
responses:
|
||||||
'200': {description: Ordered operation history}
|
'200': {description: Ordered operation history}
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets:
|
|
||||||
get:
|
|
||||||
summary: 查询一件代发现场实际托盘、SKU汇总和逐箱明细
|
|
||||||
parameters:
|
|
||||||
- {$ref: '#/components/parameters/TaskId'}
|
|
||||||
- {name: warehouseId, in: query, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
|
||||||
responses:
|
|
||||||
'200':
|
|
||||||
description: 当前任务未取消的现场托盘
|
|
||||||
content: {application/json: {schema: {type: array, items: {$ref: '#/components/schemas/WorkPallet'}}}}
|
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/open:
|
|
||||||
post:
|
|
||||||
summary: 为已领取任务开启一个一件代发现场托盘
|
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}]
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content: {application/json: {schema: {$ref: '#/components/schemas/OpenPalletRequest'}}}
|
|
||||||
responses:
|
|
||||||
'200': {description: 已开启托盘或相同operationId的幂等结果}
|
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/{palletId}/cartons/scan:
|
|
||||||
post:
|
|
||||||
summary: 扫描一件代发箱唛上托;箱唛已在其他未封托托盘时按箱移托
|
|
||||||
description: 本操作只记录实际箱到托关系,不推进既有拣货数量、订单状态或仓库资源结算。
|
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/PalletId'}]
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content: {application/json: {schema: {$ref: '#/components/schemas/ScanCartonRequest'}}}
|
|
||||||
responses:
|
|
||||||
'200': {description: 包含最新SKU与箱唛汇总的目标托盘}
|
|
||||||
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/pallets/{palletId}/seal:
|
|
||||||
post:
|
|
||||||
summary: 封托并校验现场上托箱数未超过既有PDA已提交拣货箱数
|
|
||||||
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/PalletId'}]
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content: {application/json: {schema: {$ref: '#/components/schemas/SealPalletRequest'}}}
|
|
||||||
responses:
|
|
||||||
'200': {description: 已封托托盘或A0409数量/版本冲突}
|
|
||||||
components:
|
components:
|
||||||
parameters:
|
parameters:
|
||||||
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
TaskId: {name: taskId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
ItemId: {name: itemId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
ItemId: {name: itemId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
||||||
PalletId: {name: palletId, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
|
|
||||||
schemas:
|
schemas:
|
||||||
OpenPalletRequest:
|
|
||||||
type: object
|
|
||||||
required: [warehouseId, palletCode, operationId]
|
|
||||||
properties:
|
|
||||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
|
||||||
warehousePalletId: {type: integer, format: int64, minimum: 1, nullable: true}
|
|
||||||
palletCode: {type: string, maxLength: 64}
|
|
||||||
operationId: {type: string, maxLength: 128}
|
|
||||||
ScanCartonRequest:
|
|
||||||
type: object
|
|
||||||
required: [warehouseId, boxCode, operationId]
|
|
||||||
properties:
|
|
||||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
|
||||||
boxCode: {type: string, maxLength: 80}
|
|
||||||
operationId: {type: string, maxLength: 128}
|
|
||||||
SealPalletRequest:
|
|
||||||
type: object
|
|
||||||
required: [warehouseId, expectedVersion, operationId]
|
|
||||||
properties:
|
|
||||||
warehouseId: {type: integer, format: int64, minimum: 1}
|
|
||||||
expectedVersion: {type: integer, minimum: 0}
|
|
||||||
operationId: {type: string, maxLength: 128}
|
|
||||||
WorkPallet:
|
|
||||||
type: object
|
|
||||||
required: [id, palletCode, status, version, cartonCount, skuCount, customerCount, skus, cartons]
|
|
||||||
properties:
|
|
||||||
id: {type: integer, format: int64}
|
|
||||||
palletCode: {type: string}
|
|
||||||
warehousePalletId: {type: integer, format: int64, nullable: true}
|
|
||||||
status: {type: string, enum: [OPEN, SEALED]}
|
|
||||||
version: {type: integer, minimum: 0}
|
|
||||||
cartonCount: {type: integer, minimum: 0}
|
|
||||||
skuCount: {type: integer, minimum: 0}
|
|
||||||
customerCount: {type: integer, minimum: 0}
|
|
||||||
skus: {type: array, items: {$ref: '#/components/schemas/WorkPalletSku'}}
|
|
||||||
cartons: {type: array, items: {$ref: '#/components/schemas/WorkPalletCarton'}}
|
|
||||||
WorkPalletSku:
|
|
||||||
type: object
|
|
||||||
required: [skuCode, cartonCount, productQuantity]
|
|
||||||
properties:
|
|
||||||
skuCode: {type: string}
|
|
||||||
cartonCount: {type: integer, minimum: 1}
|
|
||||||
productQuantity: {type: integer, format: int64, minimum: 0}
|
|
||||||
WorkPalletCarton:
|
|
||||||
type: object
|
|
||||||
required: [id, boxCode, skuCode, tenantId, inboundId, inboundDetailId]
|
|
||||||
properties:
|
|
||||||
id: {type: integer, format: int64}
|
|
||||||
boxCode: {type: string}
|
|
||||||
skuCode: {type: string}
|
|
||||||
tenantId: {type: integer, format: int64}
|
|
||||||
inboundId: {type: integer, format: int64}
|
|
||||||
inboundDetailId: {type: integer, format: int64}
|
|
||||||
unitsPerCarton: {type: integer, minimum: 1, nullable: true}
|
|
||||||
Task:
|
Task:
|
||||||
type: object
|
type: object
|
||||||
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
|
required: [id, loadingPlanId, containerBookingId, inboundId, inboundVersion, loadingVersion, cabinetNo, sourceType, recordStatus, taskStatus, workflowStatus, version, items]
|
||||||
@@ -232,8 +139,9 @@ components:
|
|||||||
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING], description: Source of the surviving stable container identity}
|
sourceType: {type: string, enum: [SIMULATION, OCEAN_BOOKING], description: Source of the surviving stable container identity}
|
||||||
recordStatus: {type: string, enum: [ACTIVE], description: Task detail is served only from the ACTIVE container booking currently owned by the loading plan}
|
recordStatus: {type: string, enum: [ACTIVE], description: Task detail is served only from the ACTIVE container booking currently owned by the loading plan}
|
||||||
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for the surviving ACTIVE operational container; populated only on hidden MERGED source history}
|
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for the surviving ACTIVE operational container; populated only on hidden MERGED source history}
|
||||||
taskStatus: {type: string, enum: [LOADING, COMPLETED]}
|
taskStatus: {type: string, enum: [LOADING, COMPLETING, COMPLETION_FAILED, COMPLETED]}
|
||||||
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, COMPLETED]}
|
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, PROCESSING, COMPLETION_FAILED, COMPLETED]}
|
||||||
|
completionCommand: {$ref: '#/components/schemas/CompletionCommand', nullable: true}
|
||||||
version: {type: integer, minimum: 0}
|
version: {type: integer, minimum: 0}
|
||||||
assignment: {type: object, nullable: true}
|
assignment: {type: object, nullable: true}
|
||||||
review: {type: object, nullable: true}
|
review: {type: object, nullable: true}
|
||||||
@@ -274,8 +182,8 @@ components:
|
|||||||
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for an ACTIVE pending task}
|
mergedIntoContainerBookingId: {type: integer, format: int64, nullable: true, description: Always null for an ACTIVE pending task}
|
||||||
warehouseId: {type: integer, format: int64}
|
warehouseId: {type: integer, format: int64}
|
||||||
warehouseName: {type: string, nullable: true}
|
warehouseName: {type: string, nullable: true}
|
||||||
taskStatus: {type: string, enum: [LOADING]}
|
taskStatus: {type: string, enum: [LOADING, COMPLETING, COMPLETION_FAILED]}
|
||||||
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW], description: Server projection aligned with task detail; assigned LOADING tasks with pendingItemCount=0 remain visible as AWAITING_REVIEW}
|
workflowStatus: {type: string, enum: [UNASSIGNED, IN_PROGRESS, AWAITING_REVIEW, PROCESSING, COMPLETION_FAILED], description: Server projection aligned with task detail; completion acceptance is returned immediately as PROCESSING}
|
||||||
version: {type: integer, minimum: 0}
|
version: {type: integer, minimum: 0}
|
||||||
assignmentVersion: {type: integer, minimum: 0}
|
assignmentVersion: {type: integer, minimum: 0}
|
||||||
itemCount: {type: integer, minimum: 1}
|
itemCount: {type: integer, minimum: 1}
|
||||||
@@ -329,6 +237,22 @@ components:
|
|||||||
expectedTaskVersion: {type: integer, minimum: 0}
|
expectedTaskVersion: {type: integer, minimum: 0}
|
||||||
operationId: {type: string, maxLength: 128}
|
operationId: {type: string, maxLength: 128}
|
||||||
remark: {type: string, maxLength: 500}
|
remark: {type: string, maxLength: 500}
|
||||||
|
CompletionCommand:
|
||||||
|
type: object
|
||||||
|
required: [id, operationId, status, expectedTaskVersion, attemptCount]
|
||||||
|
properties:
|
||||||
|
id: {type: integer, format: int64}
|
||||||
|
operationId: {type: string, maxLength: 128, description: Retry must reuse this exact identity}
|
||||||
|
status: {type: string, enum: [PROCESSING, RETRYABLE, FAILED, COMPLETED]}
|
||||||
|
expectedTaskVersion: {type: integer, minimum: 0, description: Retry must preserve this request-hashed version}
|
||||||
|
remark: {type: string, maxLength: 500, nullable: true, description: Retry must preserve this request-hashed remark}
|
||||||
|
attemptCount: {type: integer, minimum: 0}
|
||||||
|
nextRetryAt: {type: string, format: date-time, nullable: true}
|
||||||
|
failureStage: {type: string, nullable: true}
|
||||||
|
failureCode: {type: string, nullable: true}
|
||||||
|
requestId: {type: string, nullable: true}
|
||||||
|
traceId: {type: string, nullable: true}
|
||||||
|
completedAt: {type: string, format: date-time, nullable: true}
|
||||||
CompleteLoadingRequest:
|
CompleteLoadingRequest:
|
||||||
type: object
|
type: object
|
||||||
required: [expectedTaskVersion, operationId]
|
required: [expectedTaskVersion, operationId]
|
||||||
|
|||||||
+17
-1
@@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.cloud.openfeign.FeignClient;
|
import org.springframework.cloud.openfeign.FeignClient;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
@@ -16,11 +17,26 @@ class WarehousePalletLocationClientContractTest {
|
|||||||
assertThat(client.path()).isEqualTo("/internal/warehouse/v1");
|
assertThat(client.path()).isEqualTo("/internal/warehouse/v1");
|
||||||
|
|
||||||
Method method = WarehousePalletLocationClient.class.getDeclaredMethod(
|
Method method = WarehousePalletLocationClient.class.getDeclaredMethod(
|
||||||
"palletSnapshots", String.class, long.class);
|
"palletSnapshots", String.class, long.class, long.class, long.class, String.class);
|
||||||
assertThat(method.getAnnotation(GetMapping.class).value())
|
assertThat(method.getAnnotation(GetMapping.class).value())
|
||||||
.containsExactly("/pallets/snapshots");
|
.containsExactly("/pallets/snapshots");
|
||||||
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).value())
|
assertThat(method.getParameters()[1].getAnnotation(RequestParam.class).value())
|
||||||
.isEqualTo("warehouseId");
|
.isEqualTo("warehouseId");
|
||||||
|
assertThat(method.getParameters()[2].getAnnotation(RequestParam.class).value())
|
||||||
|
.isEqualTo("page");
|
||||||
|
assertThat(method.getParameters()[3].getAnnotation(RequestParam.class).value())
|
||||||
|
.isEqualTo("size");
|
||||||
|
assertThat(method.getParameters()[4].getAnnotation(RequestParam.class).value())
|
||||||
|
.isEqualTo("keyword");
|
||||||
|
|
||||||
|
Method exact = WarehousePalletLocationClient.class.getDeclaredMethod(
|
||||||
|
"palletSnapshot", String.class, long.class, long.class);
|
||||||
|
assertThat(exact.getAnnotation(GetMapping.class).value())
|
||||||
|
.containsExactly("/pallets/{palletId}/snapshot");
|
||||||
|
assertThat(exact.getParameters()[1].getAnnotation(PathVariable.class).value())
|
||||||
|
.isEqualTo("palletId");
|
||||||
|
assertThat(exact.getParameters()[2].getAnnotation(RequestParam.class).value())
|
||||||
|
.isEqualTo("warehouseId");
|
||||||
|
|
||||||
var snapshot = new WarehousePalletLocationClient.PalletSnapshot(
|
var snapshot = new WarehousePalletLocationClient.PalletSnapshot(
|
||||||
101L, "PLT-101", "STANDARD", new java.math.BigDecimal("2.0000"),
|
101L, "PLT-101", "STANDARD", new java.math.BigDecimal("2.0000"),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user