提交
This commit is contained in:
@@ -88,7 +88,6 @@ PDA 部分拣货会生成本柜实装 `LOADED` 分片以及可继续分柜的 `R
|
||||
"loadingPlanNo": "LP202608130001",
|
||||
"containerBookingId": "208500001",
|
||||
"containerNo": "MSCU1234567",
|
||||
"cabinetNo": "CAB20260813001",
|
||||
"containerType": "40HQ",
|
||||
"sealNo": "SEAL001",
|
||||
"voyageNo": "V001",
|
||||
@@ -142,12 +141,20 @@ PDA 部分拣货会生成本柜实装 `LOADED` 分片以及可继续分柜的 `R
|
||||
- 文件只传不可变文件引用、区域和摘要,不传二进制内容。
|
||||
- V1 消费者忽略未知字段;已有字段不改变含义或类型。
|
||||
|
||||
### 5.1 订单类型分支
|
||||
|
||||
`orderType`、`pallets`、`palletNo`、`skuPackages` 和 `cartonMarks` 必须正式加入头程公共事件契约及发送端,不能长期依赖尾程读取原始 JSON 扩展字段。三个服务升级到同一 `common-event-contract` 版本后再启用生产流量。
|
||||
|
||||
`ONE_CLICK_DROPSHIP` 的一笔最终实装入仓单可包含一个或多个来源托盘快照。消费时必须满足:所有托盘的 SKU 箱数之和等于订单箱数、SKU 数量之和等于订单数量、每个 SKU 的箱唛数等于其箱数、同一托盘内 SKU 不重复、同一订单内来源托盘号和箱唛不重复。通过后按来源托盘创建一件代发档案及托盘级 SKU 库存,并创建箱唛记录和不可变箱唛流水。`source_pallet_no` 是头程冻结快照;`warehouse_pallet_id/pallet_no` 是 Warehouse API 分配的海外仓实物托盘,两者不得混用。
|
||||
|
||||
非 `ONE_CLICK_DROPSHIP` 类型只创建 `ll_order`、冻结明细和初始状态历史,来源 `palletCount` 按 0 入库,不创建一件代发 SKU/箱唛库存。后续入仓 PDA 分配的托盘属于海外仓作业资源。
|
||||
|
||||
## 6. 尾程建单幂等
|
||||
|
||||
尾程订单表必须有以下唯一约束:
|
||||
|
||||
```text
|
||||
UNIQUE (tenant_id, source_region, source_inbound_id)
|
||||
UNIQUE (customer_id, source_region, source_inbound_id)
|
||||
```
|
||||
|
||||
建议同时保存:
|
||||
@@ -170,6 +177,8 @@ UNIQUE (tenant_id, source_region, source_inbound_id)
|
||||
|
||||
事件内任意一笔订单建单失败时,整个事件消费事务回滚,不允许出现同一客户同一柜次只生成部分尾程订单。
|
||||
|
||||
同一实际集装箱的订单共享 `order_no`、`container_no` 和 `source_loading_plan_id`;`ll_order.id` 仍是每条订单独立的雪花 ID。整柜查询、整柜操作和转入库计划统一以 `containerNo` 聚合,不使用虚拟柜号 `cabinetNo`。数据库必须保留 `KEY idx_ll_order_container (customer_id, container_no)`。
|
||||
|
||||
## 7. 尾程订单初始状态
|
||||
|
||||
事件自动生成的订单建议使用:
|
||||
|
||||
@@ -20,7 +20,7 @@ public class BridgeInbox {
|
||||
private Integer eventVersion;
|
||||
private String sourceRegion;
|
||||
private String targetRegion;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String aggregateType;
|
||||
private String aggregateId;
|
||||
private Long aggregateVersion;
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ public class EventInboxService {
|
||||
inbox.setEventVersion(envelope.eventVersion());
|
||||
inbox.setSourceRegion(envelope.sourceRegion().name());
|
||||
inbox.setTargetRegion(envelope.targetRegion().name());
|
||||
inbox.setTenantId(envelope.tenantId());
|
||||
inbox.setCustomerId(envelope.tenantId());
|
||||
inbox.setAggregateType(envelope.aggregateType());
|
||||
inbox.setAggregateId(envelope.aggregateId());
|
||||
inbox.setAggregateVersion(envelope.aggregateVersion());
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ public class HttpLastlegEventClient implements LastlegEventClient {
|
||||
public String deliver(BridgeInbox row) throws Exception {
|
||||
EventEnvelope envelope = new EventEnvelope(row.getEventId(), row.getEventType(), row.getEventVersion(),
|
||||
row.getOccurredAt(), RegionCode.valueOf(row.getSourceRegion()), RegionCode.valueOf(row.getTargetRegion()),
|
||||
row.getTenantId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(),
|
||||
row.getCustomerId(), row.getAggregateType(), row.getAggregateId(), row.getAggregateVersion(),
|
||||
row.getHandoverVersion(), row.getTraceId(), row.getCorrelationId(), row.getCausationId(),
|
||||
objectMapper.readTree(row.getPayloadJson()), row.getPayloadHash());
|
||||
String body = objectMapper.writeValueAsString(envelope);
|
||||
|
||||
@@ -8,7 +8,7 @@ CREATE TABLE IF NOT EXISTS bridge_outbox (
|
||||
event_version INT NOT NULL COMMENT '事件契约版本',
|
||||
source_region VARCHAR(32) NOT NULL COMMENT '来源区域',
|
||||
target_region VARCHAR(32) NOT NULL COMMENT '目标区域',
|
||||
tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '租户标识',
|
||||
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型',
|
||||
aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识',
|
||||
aggregate_version BIGINT NOT NULL COMMENT '聚合版本',
|
||||
@@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS bridge_inbox (
|
||||
event_version INT NOT NULL COMMENT '事件契约版本',
|
||||
source_region VARCHAR(32) NOT NULL COMMENT '来源区域',
|
||||
target_region VARCHAR(32) NOT NULL COMMENT '目标区域',
|
||||
tenant_id VARCHAR(128) NOT NULL COMMENT '租户标识',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '租户标识',
|
||||
aggregate_type VARCHAR(64) NOT NULL COMMENT '聚合类型',
|
||||
aggregate_id VARCHAR(128) NOT NULL COMMENT '聚合标识',
|
||||
aggregate_version BIGINT NOT NULL COMMENT '聚合版本',
|
||||
|
||||
+5
-2
@@ -21,13 +21,17 @@
|
||||
| 托盘分配算法 | `inboundplan/service/PalletAllocator.java` |
|
||||
| PDA 入仓、实收、上架、合托、完成 | `inboundplan/pda/controller/`、`inboundplan/pda/service/InboundPdaWorkflowService.java` |
|
||||
| 仓储资源调用 | `inboundplan/pda/gateway/WarehouseResourceGateway.java`、`HttpWarehouseResourceGateway.java` |
|
||||
| 仓储转运、历史订单/入仓绑定、可选换标联动 | `transfer/controller/WarehouseTransferController.java`、`transfer/service/WarehouseTransferService.java` |
|
||||
| 一件代发接收、SKU/箱唛库存 | `dropship/service/DropshipOrderService.java`、`dropship/entity/`、`dropship/mapper/` |
|
||||
| 数据库 | `src/main/resources/db/lastleg.sql` |
|
||||
| 测试 | `src/test/java/com/baiyue/lastleg/` |
|
||||
|
||||
## 事件建单规则
|
||||
|
||||
- 只接受冻结契约支持的事件类型和版本;校验事件 ID、租户、区域、Payload 哈希及必要字段。
|
||||
- 同一事件/loading plan 生成一个共享尾程单号;`payload.orders` 每项按 `sourceInboundId` 独立落一条 `ll_order`,并保存对应明细和初始状态历史。
|
||||
- 同一实际集装箱 `containerNo` 生成一个共享业务尾程单号;`payload.orders` 每项按 `sourceInboundId` 独立落一条雪花 ID `ll_order`,并保存对应明细和初始状态历史。整柜操作不使用 `cabinetNo`。
|
||||
- `ONE_CLICK_DROPSHIP` 每个来源入仓单必须携带一个或多个不可变来源托盘快照;每托按 SKU 携带箱数、数量和箱唛。校验订单汇总一致、托盘号/SKU/箱唛唯一后,按托建立档案及 SKU 库存,并建立箱唛和流水。仓库实物托盘由 Warehouse API 分配,不得覆盖来源托盘快照。
|
||||
- 其他订单类型只建立入仓单维度的尾程订单与冻结明细,来源 `palletCount` 强制为 0;后续 PDA 托盘属于海外仓作业资源。
|
||||
- 整个事件消费在一个事务内完成;任意订单失败时不得留下部分订单。
|
||||
- 相同事件重复投递返回既有结果,不重新生成单号或插入明细。来源入仓单唯一约束是最终防线。
|
||||
- 内部接口先执行常量时间令牌校验;缺失或错误令牌不得进入消费事务。
|
||||
@@ -44,4 +48,3 @@
|
||||
- 事件契约变化同时检查 `common-event-contract` 版本、Event Bridge 消费方和旧 Payload 哈希兼容。
|
||||
- 定向验证:`mvn -pl lastleg -am test`。
|
||||
- 事件链路变更至少覆盖重复事件、相同来源入仓单、事务回滚、并发单号、内部令牌和租户越权测试。
|
||||
|
||||
|
||||
@@ -51,27 +51,27 @@ public class LastlegOrderController {
|
||||
@Operation(summary = "Page tail-leg order rows")
|
||||
public ApiResponse<PageResult<LastlegOrderSummary>> list(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
return ApiResponse.success(orderService.page(scope(user, tenantId), query));
|
||||
return ApiResponse.success(orderService.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/{id}")
|
||||
@Operation(summary = "Get one inbound-code-level tail-leg order")
|
||||
public ApiResponse<LastlegOrderDetailResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id) {
|
||||
return ApiResponse.success(orderService.detail(scope(user, tenantId), id));
|
||||
return ApiResponse.success(orderService.detail(scope(user, customerId), id));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/api/platform/v1/last-leg/orders/export", produces = "text/csv")
|
||||
@Operation(summary = "Export filtered tail-leg orders as UTF-8 CSV")
|
||||
public ResponseEntity<byte[]> export(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid LastlegOrderRequests.Query query) {
|
||||
byte[] body = orderService.exportCsv(scope(user, tenantId), query);
|
||||
byte[] body = orderService.exportCsv(scope(user, customerId), query);
|
||||
return ResponseEntity.ok()
|
||||
.contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=lastleg-orders.csv")
|
||||
@@ -82,31 +82,31 @@ public class LastlegOrderController {
|
||||
@Operation(summary = "Transfer one order to a destination warehouse")
|
||||
public ApiResponse<Integer> transferOne(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long id,
|
||||
@Valid @RequestBody LastlegOrderRequests.SingleTransferDestination request) {
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, tenantId), request.withOrderId(id)));
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request.withOrderId(id)));
|
||||
}
|
||||
|
||||
@PutMapping("/api/platform/v1/last-leg/orders/destination")
|
||||
@Operation(summary = "Transfer multiple orders to a destination warehouse")
|
||||
public ApiResponse<Integer> transferBatch(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody LastlegOrderRequests.TransferDestination request) {
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, tenantId), request));
|
||||
return ApiResponse.success(orderService.transferDestination(scope(user, customerId), request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform queries must specify a customer tenant");
|
||||
"Platform queries must specify a customer");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+13
-13
@@ -25,46 +25,46 @@ public class DeliveryTransferController {
|
||||
@Operation(summary = "Create one pending outbound order and one pending delivery order")
|
||||
public ApiResponse<DeliveryResponses.TransferResult> transfer(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DeliveryRequests.Transfer request) {
|
||||
return ApiResponse.success(service.transfer(scope(user, tenantId), dispatchNo, operator(user), request));
|
||||
return ApiResponse.success(service.transfer(scope(user, customerId), dispatchNo, operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders")
|
||||
public ApiResponse<PageResult<DeliveryResponses.OutboundRow>> outboundPage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@Valid DeliveryRequests.Query query) {
|
||||
return ApiResponse.success(service.outboundPage(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.outboundPage(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/outbound-orders/{outboundNo}")
|
||||
public ApiResponse<DeliveryResponses.OutboundDetail> outboundDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@PathVariable String outboundNo, @Valid DeliveryRequests.PalletQuery query) {
|
||||
return ApiResponse.success(service.outboundDetail(scope(user, tenantId), outboundNo, query));
|
||||
return ApiResponse.success(service.outboundDetail(scope(user, customerId), outboundNo, query));
|
||||
}
|
||||
|
||||
@GetMapping("/delivery-orders")
|
||||
public ApiResponse<PageResult<DeliveryResponses.DeliveryRow>> deliveryPage(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@Valid DeliveryRequests.Query query) {
|
||||
return ApiResponse.success(service.deliveryPage(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.deliveryPage(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/delivery-orders/{deliveryNo}")
|
||||
public ApiResponse<DeliveryResponses.OutboundDetail> deliveryDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String tenantId,
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@PathVariable String deliveryNo, @Valid DeliveryRequests.PalletQuery query) {
|
||||
return ApiResponse.success(service.deliveryDetail(scope(user, tenantId), deliveryNo, query));
|
||||
return ApiResponse.success(service.deliveryDetail(scope(user, customerId), deliveryNo, query));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant.trim();
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class DeliveryOrder {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String deliveryNo;
|
||||
private Long outboundOrderId;
|
||||
private String outboundNo;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class OutboundOrder {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String outboundNo;
|
||||
private Long dispatchOrderId;
|
||||
private String dispatchNo;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class OutboundPalletItem {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long outboundOrderId;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
|
||||
@@ -8,10 +8,10 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface DeliveryOrderMapper extends BaseMapper<DeliveryOrder> {
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND delivery_no=#{deliveryNo} LIMIT 1")
|
||||
DeliveryOrder findByNo(@Param("tenantId") String tenantId, @Param("deliveryNo") String deliveryNo);
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DeliveryOrder findByIdempotency(@Param("tenantId") String tenantId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE tenant_id=#{tenantId} AND outbound_order_id=#{outboundOrderId} LIMIT 1 FOR UPDATE")
|
||||
DeliveryOrder lockByOutbound(@Param("tenantId") String tenantId, @Param("outboundOrderId") Long outboundOrderId);
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND delivery_no=#{deliveryNo} LIMIT 1")
|
||||
DeliveryOrder findByNo(@Param("customerId") String customerId, @Param("deliveryNo") String deliveryNo);
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DeliveryOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_delivery_order WHERE customer_id=#{customerId} AND outbound_order_id=#{outboundOrderId} LIMIT 1 FOR UPDATE")
|
||||
DeliveryOrder lockByOutbound(@Param("customerId") String customerId, @Param("outboundOrderId") Long outboundOrderId);
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface OutboundOrderMapper extends BaseMapper<OutboundOrder> {
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND outbound_no=#{outboundNo} LIMIT 1")
|
||||
OutboundOrder findByNo(@Param("tenantId") String tenantId, @Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1")
|
||||
OutboundOrder findByIdempotency(@Param("tenantId") String tenantId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND outbound_no=#{outboundNo} LIMIT 1")
|
||||
OutboundOrder findByNo(@Param("customerId") String customerId, @Param("outboundNo") String outboundNo);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
OutboundOrder findByIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_outbound_order WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
OutboundOrder lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
}
|
||||
|
||||
+2
-2
@@ -9,6 +9,6 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface OutboundPalletItemMapper extends BaseMapper<OutboundPalletItem> {
|
||||
@Select("SELECT * FROM ll_outbound_pallet_item WHERE tenant_id=#{tenantId} AND outbound_order_id=#{outboundOrderId} ORDER BY id")
|
||||
List<OutboundPalletItem> findByOutbound(@Param("tenantId") String tenantId, @Param("outboundOrderId") Long outboundOrderId);
|
||||
@Select("SELECT * FROM ll_outbound_pallet_item WHERE customer_id=#{customerId} AND outbound_order_id=#{outboundOrderId} ORDER BY id")
|
||||
List<OutboundPalletItem> findByOutbound(@Param("customerId") String customerId, @Param("outboundOrderId") Long outboundOrderId);
|
||||
}
|
||||
|
||||
+32
-32
@@ -49,22 +49,22 @@ public class DeliveryTransferService {
|
||||
@Resource private LastlegOrderStatusHistoryMapper histories;
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.TransferResult transfer(String tenantId, String dispatchNo, String operatorId,
|
||||
public DeliveryResponses.TransferResult transfer(String customerId, String dispatchNo, String operatorId,
|
||||
DeliveryRequests.Transfer request) {
|
||||
DispatchOrder found = dispatchOrders.findByDispatchNo(tenantId, dispatchNo);
|
||||
DispatchOrder found = dispatchOrders.findByDispatchNo(customerId, dispatchNo);
|
||||
if (found == null) throw notFound("Dispatch order not found");
|
||||
DispatchOrder dispatch = dispatchOrders.lockById(tenantId, found.getId());
|
||||
OutboundOrder replay = outboundOrders.findByIdempotency(tenantId, request.idempotencyKey().trim());
|
||||
DispatchOrder dispatch = dispatchOrders.lockById(customerId, found.getId());
|
||||
OutboundOrder replay = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!dispatch.getId().equals(replay.getDispatchOrderId())) throw conflict("Idempotency key belongs to another dispatch");
|
||||
return result(replay, requireDelivery(tenantId, replay.getId()), true);
|
||||
return result(replay, requireDelivery(customerId, replay.getId()), true);
|
||||
}
|
||||
OutboundOrder existing = outboundOrders.lockByDispatch(tenantId, dispatch.getId());
|
||||
if (existing != null) return result(existing, requireDelivery(tenantId, existing.getId()), true);
|
||||
OutboundOrder existing = outboundOrders.lockByDispatch(customerId, dispatch.getId());
|
||||
if (existing != null) return result(existing, requireDelivery(customerId, existing.getId()), true);
|
||||
if (Set.of("CANCELLED", "DEPARTED", "PARTIAL_DEPARTED", "COMPLETED").contains(dispatch.getStatus())) {
|
||||
throw conflict("Dispatch cannot be transferred to delivery in its current status");
|
||||
}
|
||||
List<DispatchItem> items = dispatchItems.findByDispatchOrder(tenantId, dispatch.getId());
|
||||
List<DispatchItem> items = dispatchItems.findByDispatchOrder(customerId, dispatch.getId());
|
||||
if (items.isEmpty()) throw conflict("Dispatch has no pallets");
|
||||
if (items.stream().anyMatch(item -> item.getWarehousePalletId() == null)) {
|
||||
throw conflict("All dispatch pallets must be received before transfer to delivery");
|
||||
@@ -73,7 +73,7 @@ public class DeliveryTransferService {
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setTenantId(tenantId);
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo("OUT" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + outbound.getId());
|
||||
outbound.setDispatchOrderId(dispatch.getId()); outbound.setDispatchNo(dispatch.getDispatchNo());
|
||||
outbound.setOverseasWarehouseId(dispatch.getOverseasWarehouseId()); outbound.setStatus("WAIT_OUTBOUND");
|
||||
@@ -85,20 +85,20 @@ public class DeliveryTransferService {
|
||||
} catch (DuplicateKeyException e) {
|
||||
// The unique keys are the final concurrent guard. A client retry must observe
|
||||
// the document created by the winning transaction rather than receive a false failure.
|
||||
OutboundOrder concurrent = outboundOrders.findByIdempotency(tenantId, request.idempotencyKey().trim());
|
||||
OutboundOrder concurrent = outboundOrders.findByIdempotency(customerId, request.idempotencyKey().trim());
|
||||
if (concurrent != null && dispatch.getId().equals(concurrent.getDispatchOrderId())) {
|
||||
return result(concurrent, requireDelivery(tenantId, concurrent.getId()), true);
|
||||
return result(concurrent, requireDelivery(customerId, concurrent.getId()), true);
|
||||
}
|
||||
concurrent = outboundOrders.lockByDispatch(tenantId, dispatch.getId());
|
||||
concurrent = outboundOrders.lockByDispatch(customerId, dispatch.getId());
|
||||
if (concurrent != null) {
|
||||
return result(concurrent, requireDelivery(tenantId, concurrent.getId()), true);
|
||||
return result(concurrent, requireDelivery(customerId, concurrent.getId()), true);
|
||||
}
|
||||
throw conflict("Dispatch was concurrently transferred to delivery");
|
||||
}
|
||||
|
||||
for (DispatchItem source : items) {
|
||||
OutboundPalletItem item = new OutboundPalletItem();
|
||||
item.setId(IdWorker.getId()); item.setTenantId(tenantId); item.setOutboundOrderId(outbound.getId());
|
||||
item.setId(IdWorker.getId()); item.setCustomerId(customerId); item.setOutboundOrderId(outbound.getId());
|
||||
item.setWarehousePalletId(source.getWarehousePalletId()); item.setPalletNo(source.getPalletNo());
|
||||
item.setDestinationWarehouseId(source.getDestinationWarehouseId());
|
||||
item.setDestinationWarehouseCode(source.getDestinationWarehouseCode());
|
||||
@@ -109,7 +109,7 @@ public class DeliveryTransferService {
|
||||
}
|
||||
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(IdWorker.getId()); delivery.setTenantId(tenantId);
|
||||
delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
|
||||
delivery.setDeliveryNo("DEL" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + delivery.getId());
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setDispatchOrderId(dispatch.getId()); delivery.setDispatchNo(dispatch.getDispatchNo());
|
||||
@@ -117,13 +117,13 @@ public class DeliveryTransferService {
|
||||
delivery.setPackageCount(outbound.getPackageCount()); delivery.setPalletCount(outbound.getPalletCount());
|
||||
delivery.setIdempotencyKey(request.idempotencyKey().trim()); delivery.setCreatedBy(operatorId);
|
||||
delivery.setCreatedAt(now); delivery.setUpdatedAt(now); deliveryOrders.insert(delivery);
|
||||
bindTailOrders(tenantId, items, outbound.getOutboundNo(), delivery.getDeliveryNo(), request.remark(), now);
|
||||
bindTailOrders(customerId, items, outbound.getOutboundNo(), delivery.getDeliveryNo(), request.remark(), now);
|
||||
return result(outbound, delivery, false);
|
||||
}
|
||||
|
||||
public PageResult<DeliveryResponses.OutboundRow> outboundPage(String tenantId, DeliveryRequests.Query query) {
|
||||
public PageResult<DeliveryResponses.OutboundRow> outboundPage(String customerId, DeliveryRequests.Query query) {
|
||||
Page<OutboundOrder> page = outboundOrders.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<OutboundOrder>().eq(OutboundOrder::getTenantId, tenantId)
|
||||
new LambdaQueryWrapper<OutboundOrder>().eq(OutboundOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.documentNo()), OutboundOrder::getOutboundNo, query.documentNo())
|
||||
.eq(StringUtils.hasText(query.warehouseId()), OutboundOrder::getOverseasWarehouseId, query.warehouseId())
|
||||
.eq(StringUtils.hasText(query.status()), OutboundOrder::getStatus, query.status())
|
||||
@@ -131,13 +131,13 @@ public class DeliveryTransferService {
|
||||
return page(page, page.getRecords().stream().map(this::outboundRow).toList());
|
||||
}
|
||||
|
||||
public DeliveryResponses.OutboundDetail outboundDetail(String tenantId, String outboundNo,
|
||||
public DeliveryResponses.OutboundDetail outboundDetail(String customerId, String outboundNo,
|
||||
DeliveryRequests.PalletQuery query) {
|
||||
OutboundOrder outbound = outboundOrders.findByNo(tenantId, outboundNo);
|
||||
OutboundOrder outbound = outboundOrders.findByNo(customerId, outboundNo);
|
||||
if (outbound == null) throw notFound("Outbound order not found");
|
||||
DeliveryOrder delivery = requireDelivery(tenantId, outbound.getId());
|
||||
DeliveryOrder delivery = requireDelivery(customerId, outbound.getId());
|
||||
Page<OutboundPalletItem> page = outboundPallets.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<OutboundPalletItem>().eq(OutboundPalletItem::getTenantId, tenantId)
|
||||
new LambdaQueryWrapper<OutboundPalletItem>().eq(OutboundPalletItem::getCustomerId, customerId)
|
||||
.eq(OutboundPalletItem::getOutboundOrderId, outbound.getId())
|
||||
.eq(StringUtils.hasText(query.destinationWarehouseCode()),
|
||||
OutboundPalletItem::getDestinationWarehouseCode, query.destinationWarehouseCode())
|
||||
@@ -146,9 +146,9 @@ public class DeliveryTransferService {
|
||||
page(page, page.getRecords().stream().map(this::palletRow).toList()));
|
||||
}
|
||||
|
||||
public PageResult<DeliveryResponses.DeliveryRow> deliveryPage(String tenantId, DeliveryRequests.Query query) {
|
||||
public PageResult<DeliveryResponses.DeliveryRow> deliveryPage(String customerId, DeliveryRequests.Query query) {
|
||||
Page<DeliveryOrder> page = deliveryOrders.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<DeliveryOrder>().eq(DeliveryOrder::getTenantId, tenantId)
|
||||
new LambdaQueryWrapper<DeliveryOrder>().eq(DeliveryOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.documentNo()), DeliveryOrder::getDeliveryNo, query.documentNo())
|
||||
.eq(StringUtils.hasText(query.warehouseId()), DeliveryOrder::getOverseasWarehouseId, query.warehouseId())
|
||||
.eq(StringUtils.hasText(query.status()), DeliveryOrder::getStatus, query.status())
|
||||
@@ -156,16 +156,16 @@ public class DeliveryTransferService {
|
||||
return page(page, page.getRecords().stream().map(this::deliveryRow).toList());
|
||||
}
|
||||
|
||||
public DeliveryResponses.OutboundDetail deliveryDetail(String tenantId, String deliveryNo,
|
||||
public DeliveryResponses.OutboundDetail deliveryDetail(String customerId, String deliveryNo,
|
||||
DeliveryRequests.PalletQuery query) {
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(tenantId, deliveryNo);
|
||||
DeliveryOrder delivery = deliveryOrders.findByNo(customerId, deliveryNo);
|
||||
if (delivery == null) throw notFound("Delivery order not found");
|
||||
OutboundOrder outbound = outboundOrders.selectById(delivery.getOutboundOrderId());
|
||||
if (outbound == null || !tenantId.equals(outbound.getTenantId())) throw notFound("Outbound order not found");
|
||||
return outboundDetail(tenantId, outbound.getOutboundNo(), query);
|
||||
if (outbound == null || !customerId.equals(outbound.getCustomerId())) throw notFound("Outbound order not found");
|
||||
return outboundDetail(customerId, outbound.getOutboundNo(), query);
|
||||
}
|
||||
|
||||
private void bindTailOrders(String tenantId, List<DispatchItem> items, String outboundNo, String deliveryNo,
|
||||
private void bindTailOrders(String customerId, List<DispatchItem> items, String outboundNo, String deliveryNo,
|
||||
String remark, LocalDateTime now) {
|
||||
Set<Long> orderIds = new LinkedHashSet<>();
|
||||
items.stream().map(DispatchItem::getOrderId).filter(Objects::nonNull).forEach(orderIds::add);
|
||||
@@ -182,7 +182,7 @@ public class DeliveryTransferService {
|
||||
}
|
||||
if (orderIds.isEmpty()) return;
|
||||
for (LastlegOrder order : tailOrders.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getTenantId, tenantId).in(LastlegOrder::getId, orderIds))) {
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, orderIds))) {
|
||||
String from = order.getCurrentStatus(); order.setOutboundOrderNo(outboundNo);
|
||||
order.setDeliveryOrderNo(deliveryNo); order.setCurrentStatus("WAIT_OUTBOUND"); order.setUpdatedAt(now);
|
||||
tailOrders.updateById(order);
|
||||
@@ -194,8 +194,8 @@ public class DeliveryTransferService {
|
||||
}
|
||||
}
|
||||
|
||||
private DeliveryOrder requireDelivery(String tenantId, Long outboundId) {
|
||||
DeliveryOrder value = deliveryOrders.lockByOutbound(tenantId, outboundId);
|
||||
private DeliveryOrder requireDelivery(String customerId, Long outboundId) {
|
||||
DeliveryOrder value = deliveryOrders.lockByOutbound(customerId, outboundId);
|
||||
if (value == null) throw new IllegalStateException("Delivery document is missing for outbound order");
|
||||
return value;
|
||||
}
|
||||
|
||||
+17
-17
@@ -40,20 +40,20 @@ public class DispatchController {
|
||||
@Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch")
|
||||
public ApiResponse<List<DispatchPalletCandidate>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@RequestParam(required = false) String orderNo,
|
||||
@RequestParam(required = false) String fbaWarehouseCode,
|
||||
@RequestParam(required = false) String sourceStage) {
|
||||
return ApiResponse.success(service.candidates(scope(user, tenantId), orderNo, fbaWarehouseCode, sourceStage));
|
||||
return ApiResponse.success(service.candidates(scope(user, customerId), orderNo, fbaWarehouseCode, sourceStage));
|
||||
}
|
||||
|
||||
@PostMapping("/pre-arrange")
|
||||
@Operation(summary = "Create a pending-departure dispatch and lock all selected pallets")
|
||||
public ApiResponse<DispatchResponse> preArrange(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DispatchRequests.PreArrange request) {
|
||||
String scopedTenant = scope(user, tenantId);
|
||||
String scopedTenant = scope(user, customerId);
|
||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
return ApiResponse.success(service.preArrange(scopedTenant, operatorId, request));
|
||||
}
|
||||
@@ -62,37 +62,37 @@ public class DispatchController {
|
||||
@Operation(summary = "Page dispatch orders")
|
||||
public ApiResponse<PageResult<DispatchResponse>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid DispatchRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Get dispatch details and all pallet destinations")
|
||||
public ApiResponse<DispatchResponse> detail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, tenantId), dispatchNo));
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), dispatchNo));
|
||||
}
|
||||
|
||||
@PutMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
|
||||
public ApiResponse<DispatchResponse> update(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo,
|
||||
@Valid @RequestBody DispatchRequests.Update request) {
|
||||
return ApiResponse.success(service.update(scope(user, tenantId), dispatchNo, request));
|
||||
return ApiResponse.success(service.update(scope(user, customerId), dispatchNo, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{dispatchNo}")
|
||||
@Operation(summary = "Cancel a pending dispatch and release its pallet locks")
|
||||
public ApiResponse<Void> cancel(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo) {
|
||||
service.cancel(scope(user, tenantId), dispatchNo);
|
||||
service.cancel(scope(user, customerId), dispatchNo);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
@@ -100,24 +100,24 @@ public class DispatchController {
|
||||
@Operation(summary = "Ship all PDA-picked pallets and mark the dispatch departed")
|
||||
public ApiResponse<DispatchPdaResponses.Departed> depart(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable String dispatchNo,
|
||||
@Valid @RequestBody DispatchPdaRequests.Depart request) {
|
||||
String scopedTenant = scope(user, tenantId);
|
||||
String scopedTenant = scope(user, customerId);
|
||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
return ApiResponse.success(pdaWorkflow.depart(scopedTenant, dispatchNo, operatorId, request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ public class DispatchItem {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private Long dispatchOrderId;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long planId;
|
||||
private Long planPalletId;
|
||||
private Long warehousePalletId;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class DispatchOrder {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String dispatchNo;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseName;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class DispatchPalletLock {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String sourcePalletKey;
|
||||
/** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */
|
||||
private String activeKey;
|
||||
|
||||
+8
-8
@@ -17,7 +17,7 @@ public interface DispatchCandidateMapper {
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
|
||||
@@ -25,22 +25,22 @@ public interface DispatchCandidateMapper {
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE ip.tenant_id=#{tenantId} " +
|
||||
"WHERE ip.customer_id=#{customerId} " +
|
||||
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
|
||||
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
|
||||
"<if test='sourceStage != null and sourceStage != \"\"'> AND " +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " +
|
||||
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||
"WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
|
||||
"WHERE d.tenant_id=ip.tenant_id AND d.status='ACTIVE' AND (" +
|
||||
"WHERE d.customer_id=ip.customer_id AND d.status='ACTIVE' AND (" +
|
||||
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
||||
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id)))) " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.plan_id,p.sequence_no" +
|
||||
"</script>")
|
||||
List<DispatchPalletCandidate> available(@Param("tenantId") String tenantId,
|
||||
List<DispatchPalletCandidate> available(@Param("customerId") String customerId,
|
||||
@Param("orderNo") String orderNo,
|
||||
@Param("fbaWarehouseCode") String fbaWarehouseCode,
|
||||
@Param("sourceStage") String sourceStage);
|
||||
@@ -53,7 +53,7 @@ public interface DispatchCandidateMapper {
|
||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.tenant_id=#{tenantId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
|
||||
@@ -61,12 +61,12 @@ public interface DispatchCandidateMapper {
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||
"WHERE ip.tenant_id=#{tenantId} AND p.id IN " +
|
||||
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
||||
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||
"ORDER BY p.id FOR UPDATE" +
|
||||
"</script>")
|
||||
List<DispatchPalletCandidate> lockSelected(@Param("tenantId") String tenantId,
|
||||
List<DispatchPalletCandidate> lockSelected(@Param("customerId") String customerId,
|
||||
@Param("planPalletIds") List<Long> planPalletIds);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchItemMapper extends BaseMapper<DispatchItem> {
|
||||
@Select("SELECT * FROM ll_dispatch_item WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} ORDER BY id")
|
||||
List<DispatchItem> findByDispatchOrder(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_dispatch_item WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} ORDER BY id")
|
||||
List<DispatchItem> findByDispatchOrder(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId);
|
||||
|
||||
@Update("UPDATE ll_dispatch_item i JOIN ll_inbound_plan_pallet p ON p.id=i.plan_pallet_id " +
|
||||
"JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.tenant_id=i.tenant_id " +
|
||||
"JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=i.customer_id " +
|
||||
"SET i.warehouse_pallet_id=p.warehouse_pallet_id,i.source_stage='RECEIVED' " +
|
||||
"WHERE i.tenant_id=#{tenantId} AND i.dispatch_order_id=#{dispatchOrderId} " +
|
||||
"WHERE i.customer_id=#{customerId} AND i.dispatch_order_id=#{dispatchOrderId} " +
|
||||
"AND i.warehouse_pallet_id IS NULL AND p.warehouse_pallet_id IS NOT NULL")
|
||||
int bindReceivedPallets(@Param("tenantId") String tenantId,
|
||||
int bindReceivedPallets(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId);
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE tenant_id=#{tenantId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findByDispatchNo(@Param("tenantId") String tenantId, @Param("dispatchNo") String dispatchNo);
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
||||
DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE tenant_id=#{tenantId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||
DispatchOrder lockById(@Param("tenantId") String tenantId, @Param("id") Long id);
|
||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||
DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock> {
|
||||
@Select("SELECT * FROM ll_dispatch_pallet_lock WHERE tenant_id=#{tenantId} AND source_pallet_key=#{sourcePalletKey} " +
|
||||
@Select("SELECT * FROM ll_dispatch_pallet_lock WHERE customer_id=#{customerId} AND source_pallet_key=#{sourcePalletKey} " +
|
||||
"AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
||||
DispatchPalletLock lockActive(@Param("tenantId") String tenantId,
|
||||
DispatchPalletLock lockActive(@Param("customerId") String customerId,
|
||||
@Param("sourcePalletKey") String sourcePalletKey);
|
||||
}
|
||||
|
||||
+11
-11
@@ -20,38 +20,38 @@ public class DispatchPdaController {
|
||||
@PostMapping("/push")
|
||||
public ApiResponse<DispatchPdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DispatchPdaRequests.Push request) {
|
||||
return ApiResponse.success(service.push(scope(user, tenantId), dispatchNo, request));
|
||||
return ApiResponse.success(service.push(scope(user, customerId), dispatchNo, request));
|
||||
}
|
||||
@GetMapping
|
||||
public ApiResponse<DispatchPdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(service.task(scope(user, tenantId), dispatchNo));
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.task(scope(user, customerId), dispatchNo));
|
||||
}
|
||||
@PostMapping("/items/{itemId}/pick")
|
||||
public ApiResponse<DispatchPdaResponses.Picked> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo, @PathVariable long itemId,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DispatchPdaRequests.Pick request) {
|
||||
return ApiResponse.success(service.pick(scope(user, tenantId), dispatchNo, itemId, operator(user), request));
|
||||
return ApiResponse.success(service.pick(scope(user, customerId), dispatchNo, itemId, operator(user), request));
|
||||
}
|
||||
@PostMapping("/complete")
|
||||
public ApiResponse<DispatchPdaResponses.PickingCompleted> completePicking(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody DispatchPdaRequests.CompletePicking request) {
|
||||
return ApiResponse.success(service.completePicking(scope(user, tenantId), dispatchNo, request));
|
||||
return ApiResponse.success(service.completePicking(scope(user, customerId), dispatchNo, request));
|
||||
}
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant.trim();
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_dispatch_pda_operation")
|
||||
public class DispatchPdaOperation {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long taskId;
|
||||
private String operationType;
|
||||
private String idempotencyKey;
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_dispatch_pda_pick")
|
||||
public class DispatchPdaPick {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long taskId;
|
||||
private Long dispatchItemId;
|
||||
private Long inventoryPositionId;
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_dispatch_pda_pick_order")
|
||||
public class DispatchPdaPickOrder {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long pickId;
|
||||
private Long receiptId;
|
||||
private Long orderId;
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_dispatch_pda_task")
|
||||
public class DispatchPdaTask {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long dispatchOrderId;
|
||||
private String taskNo;
|
||||
private String status;
|
||||
|
||||
+2
-2
@@ -6,6 +6,6 @@ import org.apache.ibatis.annotations.*;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPdaOperationMapper extends BaseMapper<DispatchPdaOperation> {
|
||||
@Select("SELECT * FROM ll_dispatch_pda_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DispatchPdaOperation findKey(@Param("tenantId") String tenantId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_dispatch_pda_operation WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DispatchPdaOperation findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
}
|
||||
|
||||
+4
-4
@@ -7,8 +7,8 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPdaPickMapper extends BaseMapper<DispatchPdaPick> {
|
||||
@Select("SELECT * FROM ll_dispatch_pda_pick WHERE tenant_id=#{tenantId} AND task_id=#{taskId} ORDER BY id")
|
||||
List<DispatchPdaPick> listTask(@Param("tenantId") String tenantId, @Param("taskId") Long taskId);
|
||||
@Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick WHERE tenant_id=#{tenantId} AND task_id=#{taskId} AND dispatch_item_id=#{itemId}")
|
||||
long totalItem(@Param("tenantId") String tenantId, @Param("taskId") Long taskId, @Param("itemId") Long itemId);
|
||||
@Select("SELECT * FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId} AND task_id=#{taskId} ORDER BY id")
|
||||
List<DispatchPdaPick> listTask(@Param("customerId") String customerId, @Param("taskId") Long taskId);
|
||||
@Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick WHERE customer_id=#{customerId} AND task_id=#{taskId} AND dispatch_item_id=#{itemId}")
|
||||
long totalItem(@Param("customerId") String customerId, @Param("taskId") Long taskId, @Param("itemId") Long itemId);
|
||||
}
|
||||
|
||||
+9
-9
@@ -12,20 +12,20 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface DispatchPdaPickOrderMapper extends BaseMapper<DispatchPdaPickOrder> {
|
||||
@Select("SELECT r.* FROM ll_inbound_stock_receipt r " +
|
||||
"WHERE r.tenant_id=#{tenantId} AND r.inventory_position_id=#{positionId} " +
|
||||
"WHERE r.customer_id=#{customerId} AND r.inventory_position_id=#{positionId} " +
|
||||
"AND r.received_packages > (SELECT COALESCE(SUM(a.packages),0) FROM ll_dispatch_pda_pick_order a " +
|
||||
"WHERE a.tenant_id=r.tenant_id AND a.receipt_id=r.id) ORDER BY r.received_at,r.id")
|
||||
List<InboundStockReceipt> availableReceipts(@Param("tenantId") String tenantId,
|
||||
"WHERE a.customer_id=r.customer_id AND a.receipt_id=r.id) ORDER BY r.received_at,r.id")
|
||||
List<InboundStockReceipt> availableReceipts(@Param("customerId") String customerId,
|
||||
@Param("positionId") Long positionId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick_order " +
|
||||
"WHERE tenant_id=#{tenantId} AND receipt_id=#{receiptId}")
|
||||
long attributedPackages(@Param("tenantId") String tenantId, @Param("receiptId") Long receiptId);
|
||||
"WHERE customer_id=#{customerId} AND receipt_id=#{receiptId}")
|
||||
long attributedPackages(@Param("customerId") String customerId, @Param("receiptId") Long receiptId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(product_quantity),0) FROM ll_dispatch_pda_pick_order " +
|
||||
"WHERE tenant_id=#{tenantId} AND receipt_id=#{receiptId}")
|
||||
long attributedQuantity(@Param("tenantId") String tenantId, @Param("receiptId") Long receiptId);
|
||||
"WHERE customer_id=#{customerId} AND receipt_id=#{receiptId}")
|
||||
long attributedQuantity(@Param("customerId") String customerId, @Param("receiptId") Long receiptId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pda_pick_order WHERE tenant_id=#{tenantId} AND pick_id=#{pickId} ORDER BY id")
|
||||
List<DispatchPdaPickOrder> listPick(@Param("tenantId") String tenantId, @Param("pickId") Long pickId);
|
||||
@Select("SELECT * FROM ll_dispatch_pda_pick_order WHERE customer_id=#{customerId} AND pick_id=#{pickId} ORDER BY id")
|
||||
List<DispatchPdaPickOrder> listPick(@Param("customerId") String customerId, @Param("pickId") Long pickId);
|
||||
}
|
||||
|
||||
+4
-4
@@ -6,9 +6,9 @@ import org.apache.ibatis.annotations.*;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchPdaTaskMapper extends BaseMapper<DispatchPdaTask> {
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
DispatchPdaTask lockByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1 FOR UPDATE")
|
||||
DispatchPdaTask lockByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE tenant_id=#{tenantId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
|
||||
DispatchPdaTask findByDispatch(@Param("tenantId") String tenantId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
@Select("SELECT * FROM ll_dispatch_pda_task WHERE customer_id=#{customerId} AND dispatch_order_id=#{dispatchOrderId} LIMIT 1")
|
||||
DispatchPdaTask findByDispatch(@Param("customerId") String customerId, @Param("dispatchOrderId") Long dispatchOrderId);
|
||||
}
|
||||
|
||||
+58
-58
@@ -45,25 +45,25 @@ public class DispatchPdaWorkflowService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Task push(String tenantId, String dispatchNo, DispatchPdaRequests.Push request) {
|
||||
DispatchOrder order = requireOrder(tenantId, dispatchNo);
|
||||
public DispatchPdaResponses.Task push(String customerId, String dispatchNo, DispatchPdaRequests.Push request) {
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
requirePdaStatus(order);
|
||||
requireVehicle(order);
|
||||
String warehouseId = warehouse(request.warehouseId());
|
||||
requireDispatchWarehouse(order, warehouseId);
|
||||
DispatchPdaOperation replay = replay(tenantId, "PUSH", request.idempotencyKey(), request);
|
||||
DispatchPdaOperation replay = replay(customerId, "PUSH", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, DispatchPdaResponses.Task.class);
|
||||
items.bindReceivedPallets(tenantId, order.getId());
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(tenantId, order.getId());
|
||||
items.bindReceivedPallets(customerId, order.getId());
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
|
||||
if (dispatchItems.isEmpty()) throw conflict("Dispatch has no pallets to push");
|
||||
if (dispatchItems.stream().anyMatch(item -> item.getWarehousePalletId() == null)) {
|
||||
throw conflict("All pre-arranged pallets must be received before PDA push");
|
||||
}
|
||||
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId());
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (task == null) {
|
||||
task = new DispatchPdaTask();
|
||||
task.setId(IdWorker.getId()); task.setTenantId(tenantId); task.setDispatchOrderId(order.getId());
|
||||
task.setId(IdWorker.getId()); task.setCustomerId(customerId); task.setDispatchOrderId(order.getId());
|
||||
task.setTaskNo("DPDA" + task.getId()); task.setStatus("PDA_PENDING");
|
||||
task.setWarehouseId(warehouseId);
|
||||
task.setOperatorId(trim(request.operatorId())); task.setOperatorName(trim(request.operatorName()));
|
||||
@@ -71,91 +71,91 @@ public class DispatchPdaWorkflowService {
|
||||
}
|
||||
if ("PENDING_DEPARTURE".equals(order.getStatus())) { order.setStatus("PICKING"); order.setUpdatedAt(now); orders.updateById(order); }
|
||||
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA task warehouse does not match selected warehouse");
|
||||
DispatchPdaResponses.Task result = task(tenantId, order, task, dispatchItems);
|
||||
save(tenantId, task.getId(), "PUSH", request.idempotencyKey(), request, result);
|
||||
DispatchPdaResponses.Task result = task(customerId, order, task, dispatchItems);
|
||||
save(customerId, task.getId(), "PUSH", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public DispatchPdaResponses.Task task(String tenantId, String dispatchNo) {
|
||||
DispatchOrder order = requireOrder(tenantId, dispatchNo);
|
||||
DispatchPdaTask task = tasks.findByDispatch(tenantId, order.getId());
|
||||
public DispatchPdaResponses.Task task(String customerId, String dispatchNo) {
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
DispatchPdaTask task = tasks.findByDispatch(customerId, order.getId());
|
||||
if (task == null) throw notFound("Dispatch PDA task not found");
|
||||
return task(tenantId, order, task, items.findByDispatchOrder(tenantId, order.getId()));
|
||||
return task(customerId, order, task, items.findByDispatchOrder(customerId, order.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Picked pick(String tenantId, String dispatchNo, long itemId,
|
||||
public DispatchPdaResponses.Picked pick(String customerId, String dispatchNo, long itemId,
|
||||
String operatorId, DispatchPdaRequests.Pick request) {
|
||||
DispatchOrder order = requireOrder(tenantId, dispatchNo);
|
||||
DispatchOrder order = requireOrder(customerId, dispatchNo);
|
||||
requirePdaStatus(order);
|
||||
requireVehicle(order);
|
||||
String warehouseId = warehouse(request.warehouseId());
|
||||
requireDispatchWarehouse(order, warehouseId);
|
||||
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId());
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
|
||||
throw conflict("Dispatch PDA task is not available for picking");
|
||||
}
|
||||
if (!warehouseId.equals(task.getWarehouseId())) throw conflict("PDA pick warehouse does not match task warehouse");
|
||||
DispatchItem item = items.selectById(itemId);
|
||||
if (item == null || !tenantId.equals(item.getTenantId()) || !order.getId().equals(item.getDispatchOrderId())) {
|
||||
if (item == null || !customerId.equals(item.getCustomerId()) || !order.getId().equals(item.getDispatchOrderId())) {
|
||||
throw notFound("Dispatch pallet item not found");
|
||||
}
|
||||
if (!warehouseId.equals(item.getOverseasWarehouseId())) throw conflict("Pallet does not belong to selected warehouse");
|
||||
DispatchPdaOperation replay = replay(tenantId, "PICK", request.idempotencyKey(), request);
|
||||
DispatchPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, DispatchPdaResponses.Picked.class);
|
||||
InventoryPosition position = positions.selectById(request.positionId());
|
||||
if (position == null || !tenantId.equals(position.getTenantId())
|
||||
if (position == null || !customerId.equals(position.getCustomerId())
|
||||
|| item.getWarehousePalletId() == null || !item.getWarehousePalletId().equals(position.getPalletId())) {
|
||||
throw conflict("Inventory position does not belong to this dispatch pallet");
|
||||
}
|
||||
long picked = picks.totalItem(tenantId, task.getId(), itemId);
|
||||
long picked = picks.totalItem(customerId, task.getId(), itemId);
|
||||
if (picked + request.packages() > item.getPackageCount()) throw conflict("Picked packages exceed dispatch pallet packages");
|
||||
String keyBase = "DISPATCH:" + order.getDispatchNo() + ":" + itemId + ":" + request.idempotencyKey();
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(tenantId, request.positionId(),
|
||||
InventoryCommandRequests.Move move = new InventoryCommandRequests.Move(customerId, request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":ALLOCATE", request.remark());
|
||||
inventory.allocate(tenantId, operatorId, move);
|
||||
inventory.pick(tenantId, operatorId, new InventoryCommandRequests.Move(tenantId, request.positionId(),
|
||||
inventory.allocate(customerId, operatorId, move);
|
||||
inventory.pick(customerId, operatorId, new InventoryCommandRequests.Move(customerId, request.positionId(),
|
||||
request.packages(), order.getDispatchNo(), order.getDispatchNo(), null,
|
||||
keyBase + ":PICK", request.remark()));
|
||||
DispatchPdaPick pick = new DispatchPdaPick();
|
||||
pick.setId(IdWorker.getId()); pick.setTenantId(tenantId); pick.setTaskId(task.getId());
|
||||
pick.setId(IdWorker.getId()); pick.setCustomerId(customerId); pick.setTaskId(task.getId());
|
||||
pick.setDispatchItemId(itemId); pick.setInventoryPositionId(request.positionId()); pick.setPackages(request.packages());
|
||||
pick.setProductQuantity(request.productQuantity()); pick.setIdempotencyKey(request.idempotencyKey());
|
||||
pick.setPickedAt(LocalDateTime.now()); pick.setCreatedAt(pick.getPickedAt()); picks.insert(pick);
|
||||
pickAttribution.attribute(tenantId, pick);
|
||||
pickAttribution.attribute(customerId, pick);
|
||||
long total = picked + request.packages();
|
||||
order.setPickedPackageCount((int) (n(order.getPickedPackageCount()) + request.packages()));
|
||||
task.setStatus("PICKING"); task.setUpdatedAt(LocalDateTime.now());
|
||||
if (allPicked(tenantId, task.getId(), items.findByDispatchOrder(tenantId, order.getId()))) {
|
||||
if (allPicked(customerId, task.getId(), items.findByDispatchOrder(customerId, order.getId()))) {
|
||||
task.setStatus("COMPLETED"); task.setCompletedAt(LocalDateTime.now()); order.setStatus("READY_TO_DEPART");
|
||||
}
|
||||
order.setUpdatedAt(LocalDateTime.now());
|
||||
orders.updateById(order);
|
||||
tasks.updateById(task);
|
||||
DispatchPdaResponses.Picked result = new DispatchPdaResponses.Picked(itemId, request.positionId(), request.packages(), total, false, false);
|
||||
save(tenantId, task.getId(), "PICK", request.idempotencyKey(), request, result);
|
||||
save(customerId, task.getId(), "PICK", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchPdaResponses.PickingCompleted completePicking(String tenantId, String dispatchNo,
|
||||
public DispatchPdaResponses.PickingCompleted completePicking(String customerId, String dispatchNo,
|
||||
DispatchPdaRequests.CompletePicking request) {
|
||||
DispatchOrder found = requireOrder(tenantId, dispatchNo);
|
||||
DispatchOrder order = orders.lockById(tenantId, found.getId());
|
||||
DispatchOrder found = requireOrder(customerId, dispatchNo);
|
||||
DispatchOrder order = orders.lockById(customerId, found.getId());
|
||||
if (order == null) throw notFound("Dispatch order not found");
|
||||
DispatchPdaOperation replay = replay(tenantId, "COMPLETE_PICKING", request.idempotencyKey(), request);
|
||||
DispatchPdaOperation replay = replay(customerId, "COMPLETE_PICKING", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, DispatchPdaResponses.PickingCompleted.class).withReplay();
|
||||
requireVehicle(order);
|
||||
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId());
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
if (task == null || !("PDA_PENDING".equals(task.getStatus()) || "PICKING".equals(task.getStatus()))) {
|
||||
throw conflict("Dispatch PDA task is not available for completion");
|
||||
}
|
||||
List<DispatchItem> source = items.findByDispatchOrder(tenantId, order.getId());
|
||||
List<DispatchItem> source = items.findByDispatchOrder(customerId, order.getId());
|
||||
long planned = source.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull)
|
||||
.mapToLong(Integer::longValue).sum();
|
||||
long picked = picks.listTask(tenantId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
long picked = picks.listTask(customerId, task.getId()).stream().mapToLong(p -> n(p.getPackages())).sum();
|
||||
if (picked <= 0 || picked > planned) throw conflict("PDA completion requires a valid picked package count");
|
||||
boolean partial = picked < planned;
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -166,47 +166,47 @@ public class DispatchPdaWorkflowService {
|
||||
order.setUpdatedAt(now); orders.updateById(order);
|
||||
DispatchPdaResponses.PickingCompleted result = new DispatchPdaResponses.PickingCompleted(order.getDispatchNo(),
|
||||
order.getStatus(), planned, picked, planned - picked, partial, false);
|
||||
save(tenantId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result);
|
||||
save(customerId, task.getId(), "COMPLETE_PICKING", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Final warehouse exit: only fully PDA-picked dispatches can consume picked inventory. */
|
||||
@Transactional
|
||||
public DispatchPdaResponses.Departed depart(String tenantId, String dispatchNo, String operatorId,
|
||||
public DispatchPdaResponses.Departed depart(String customerId, String dispatchNo, String operatorId,
|
||||
DispatchPdaRequests.Depart request) {
|
||||
DispatchOrder found = requireOrder(tenantId, dispatchNo);
|
||||
DispatchOrder order = orders.lockById(tenantId, found.getId());
|
||||
DispatchOrder found = requireOrder(customerId, dispatchNo);
|
||||
DispatchOrder order = orders.lockById(customerId, found.getId());
|
||||
if (order == null) throw notFound("Dispatch order not found");
|
||||
DispatchPdaOperation replay = replay(tenantId, "DEPART", request.idempotencyKey(), request);
|
||||
DispatchPdaOperation replay = replay(customerId, "DEPART", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, DispatchPdaResponses.Departed.class).withReplay();
|
||||
requireVehicle(order);
|
||||
OutboundOrder outbound = outboundOrders.lockByDispatch(tenantId, order.getId());
|
||||
OutboundOrder outbound = outboundOrders.lockByDispatch(customerId, order.getId());
|
||||
if (outbound == null || !"WAIT_OUTBOUND".equals(outbound.getStatus())) {
|
||||
throw conflict("Dispatch must be transferred to a pending outbound order before departure");
|
||||
}
|
||||
DispatchPdaTask task = tasks.lockByDispatch(tenantId, order.getId());
|
||||
DispatchPdaTask task = tasks.lockByDispatch(customerId, order.getId());
|
||||
if (task == null || !"COMPLETED".equals(task.getStatus())
|
||||
|| !("READY_TO_DEPART".equals(order.getStatus()) || "PARTIAL_READY_TO_DEPART".equals(order.getStatus()))) {
|
||||
throw conflict("Dispatch must complete PDA picking before departure");
|
||||
}
|
||||
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(tenantId, order.getId());
|
||||
List<DispatchItem> dispatchItems = items.findByDispatchOrder(customerId, order.getId());
|
||||
boolean partial = "PARTIAL_READY_TO_DEPART".equals(order.getStatus());
|
||||
if (!partial && !allPicked(tenantId, task.getId(), dispatchItems)) {
|
||||
if (!partial && !allPicked(customerId, task.getId(), dispatchItems)) {
|
||||
throw conflict("Every dispatch pallet must be fully picked before departure");
|
||||
}
|
||||
List<DispatchPdaPick> dispatchPicks = picks.listTask(tenantId, task.getId());
|
||||
List<DispatchPdaPick> dispatchPicks = picks.listTask(customerId, task.getId());
|
||||
if (dispatchPicks.isEmpty()) throw conflict("Dispatch has no PDA picks to ship");
|
||||
long shipped = 0;
|
||||
for (DispatchPdaPick pick : dispatchPicks) {
|
||||
String key = "DISPATCH:" + order.getDispatchNo() + ":PICK:" + pick.getId() + ":SHIP";
|
||||
inventory.ship(tenantId, operatorId, new InventoryCommandRequests.Move(tenantId,
|
||||
inventory.ship(customerId, operatorId, new InventoryCommandRequests.Move(customerId,
|
||||
pick.getInventoryPositionId(), n(pick.getPackages()),
|
||||
order.getDispatchNo(), order.getDispatchNo(), null, key, request.remark()));
|
||||
shipped += n(pick.getPackages());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
pickAttribution.depart(tenantId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now);
|
||||
pickAttribution.depart(customerId, outbound.getOutboundNo(), order.getDispatchNo(), dispatchPicks, now);
|
||||
outbound.setStatus("OUTBOUNDED");
|
||||
outbound.setUpdatedAt(now);
|
||||
outboundOrders.updateById(outbound);
|
||||
@@ -214,33 +214,33 @@ public class DispatchPdaWorkflowService {
|
||||
order.setStatus(partial ? "PARTIAL_DEPARTED" : "DEPARTED");
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
releasePalletLocks(tenantId, order.getId(), now);
|
||||
releasePalletLocks(customerId, order.getId(), now);
|
||||
DispatchPdaResponses.Departed result = new DispatchPdaResponses.Departed(order.getDispatchNo(),
|
||||
order.getStatus(), shipped, false);
|
||||
save(tenantId, task.getId(), "DEPART", request.idempotencyKey(), request, result);
|
||||
save(customerId, task.getId(), "DEPART", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private DispatchPdaResponses.Task task(String tenantId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) {
|
||||
private DispatchPdaResponses.Task task(String customerId, DispatchOrder order, DispatchPdaTask task, List<DispatchItem> source) {
|
||||
List<DispatchPdaResponses.Item> values = new ArrayList<>();
|
||||
for (DispatchItem item : source) {
|
||||
List<InventoryPosition> palletPositions = item.getWarehousePalletId() == null ? List.of()
|
||||
: positions.listPallet(tenantId, item.getWarehousePalletId());
|
||||
: positions.listPallet(customerId, item.getWarehousePalletId());
|
||||
values.add(new DispatchPdaResponses.Item(item.getId(), item.getPalletNo(), item.getDestinationWarehouseCode(),
|
||||
item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), item.getPackageCount(),
|
||||
picks.totalItem(tenantId, task.getId(), item.getId()), palletPositions.stream().map(p ->
|
||||
picks.totalItem(customerId, task.getId(), item.getId()), palletPositions.stream().map(p ->
|
||||
new DispatchPdaResponses.Position(p.getId(), p.getBalanceId(), n(p.getAvailablePackages()),
|
||||
n(p.getAllocatedPackages()), n(p.getPickedPackages()))).toList()));
|
||||
}
|
||||
return new DispatchPdaResponses.Task(task.getId(), task.getTaskNo(), order.getDispatchNo(), order.getVehiclePlate(), task.getWarehouseId(), task.getStatus(), values);
|
||||
}
|
||||
|
||||
private boolean allPicked(String tenantId, Long taskId, List<DispatchItem> source) {
|
||||
return source.stream().allMatch(item -> picks.totalItem(tenantId, taskId, item.getId()) == item.getPackageCount());
|
||||
private boolean allPicked(String customerId, Long taskId, List<DispatchItem> source) {
|
||||
return source.stream().allMatch(item -> picks.totalItem(customerId, taskId, item.getId()) == item.getPackageCount());
|
||||
}
|
||||
private void releasePalletLocks(String tenantId, Long dispatchOrderId, LocalDateTime now) {
|
||||
private void releasePalletLocks(String customerId, Long dispatchOrderId, LocalDateTime now) {
|
||||
List<DispatchPalletLock> activeLocks = palletLocks.selectList(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getTenantId, tenantId)
|
||||
.eq(DispatchPalletLock::getCustomerId, customerId)
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatchOrderId)
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
for (DispatchPalletLock lock : activeLocks) {
|
||||
@@ -251,7 +251,7 @@ public class DispatchPdaWorkflowService {
|
||||
palletLocks.updateById(lock);
|
||||
}
|
||||
}
|
||||
private DispatchOrder requireOrder(String tenantId, String dispatchNo) { DispatchOrder order = orders.findByDispatchNo(tenantId, dispatchNo); if (order == null) throw notFound("Dispatch order not found"); return order; }
|
||||
private DispatchOrder requireOrder(String customerId, String dispatchNo) { DispatchOrder order = orders.findByDispatchNo(customerId, dispatchNo); if (order == null) throw notFound("Dispatch order not found"); return order; }
|
||||
private void requirePdaStatus(DispatchOrder order) {
|
||||
if (!("PENDING_DEPARTURE".equals(order.getStatus()) || "PICKING".equals(order.getStatus())
|
||||
|| "READY_TO_DEPART".equals(order.getStatus()))) throw conflict("Dispatch has already departed and cannot be changed by PDA");
|
||||
@@ -263,8 +263,8 @@ public class DispatchPdaWorkflowService {
|
||||
throw conflict("Selected warehouse does not match dispatch warehouse");
|
||||
}
|
||||
}
|
||||
private DispatchPdaOperation replay(String tenantId, String type, String key, Object request) { DispatchPdaOperation op = operations.findKey(tenantId, required(key, "Idempotency key is required")); if (op == null) return null; if (!type.equals(op.getOperationType()) || !hash(request).equals(op.getRequestHash())) throw conflict("Idempotency key was reused with a different request"); if (!"COMPLETED".equals(op.getStatus())) throw conflict("PDA operation is still processing"); return op; }
|
||||
private void save(String tenantId, Long taskId, String type, String key, Object request, Object result) { DispatchPdaOperation op = new DispatchPdaOperation(); op.setId(IdWorker.getId()); op.setTenantId(tenantId); op.setTaskId(taskId); op.setOperationType(type); op.setIdempotencyKey(required(key,"Idempotency key is required")); op.setRequestHash(hash(request)); op.setResultJson(json(result)); op.setStatus("COMPLETED"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op); }
|
||||
private DispatchPdaOperation replay(String customerId, String type, String key, Object request) { DispatchPdaOperation op = operations.findKey(customerId, required(key, "Idempotency key is required")); if (op == null) return null; if (!type.equals(op.getOperationType()) || !hash(request).equals(op.getRequestHash())) throw conflict("Idempotency key was reused with a different request"); if (!"COMPLETED".equals(op.getStatus())) throw conflict("PDA operation is still processing"); return op; }
|
||||
private void save(String customerId, Long taskId, String type, String key, Object request, Object result) { DispatchPdaOperation op = new DispatchPdaOperation(); op.setId(IdWorker.getId()); op.setCustomerId(customerId); op.setTaskId(taskId); op.setOperationType(type); op.setIdempotencyKey(required(key,"Idempotency key is required")); op.setRequestHash(hash(request)); op.setResultJson(json(result)); op.setStatus("COMPLETED"); op.setCreatedAt(LocalDateTime.now()); op.setUpdatedAt(op.getCreatedAt()); operations.insert(op); }
|
||||
private <T> T read(DispatchPdaOperation op, Class<T> type) { try { return objectMapper.readValue(op.getResultJson(), type); } catch (Exception e) { throw new IllegalStateException("Cannot read PDA replay", e); } }
|
||||
private String hash(Object value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json(value).getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw new IllegalStateException(e); } }
|
||||
private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw new IllegalStateException(e); } }
|
||||
|
||||
+9
-9
@@ -26,18 +26,18 @@ public class DispatchPickOrderAttributionService {
|
||||
@Resource private LastlegOrderStatusHistoryMapper history;
|
||||
|
||||
/** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */
|
||||
public List<OrderPackages> attribute(String tenantId, DispatchPdaPick pick) {
|
||||
public List<OrderPackages> attribute(String customerId, DispatchPdaPick pick) {
|
||||
long remainingPackages = value(pick.getPackages());
|
||||
long totalPackages = remainingPackages;
|
||||
long totalQuantity = value(pick.getProductQuantity());
|
||||
long allocatedPackages = 0;
|
||||
long allocatedQuantity = 0;
|
||||
Map<Long, Long> pickedByOrder = new LinkedHashMap<>();
|
||||
for (InboundStockReceipt receipt : attributions.availableReceipts(tenantId,
|
||||
for (InboundStockReceipt receipt : attributions.availableReceipts(customerId,
|
||||
pick.getInventoryPositionId())) {
|
||||
if (remainingPackages == 0) break;
|
||||
long receiptPackages = value(receipt.getReceivedPackages())
|
||||
- attributions.attributedPackages(tenantId, receipt.getId());
|
||||
- attributions.attributedPackages(customerId, receipt.getId());
|
||||
if (receiptPackages <= 0) continue;
|
||||
long packages = Math.min(remainingPackages, receiptPackages);
|
||||
long nextPackages = allocatedPackages + packages;
|
||||
@@ -45,7 +45,7 @@ public class DispatchPickOrderAttributionService {
|
||||
long quantity = nextQuantity - allocatedQuantity;
|
||||
DispatchPdaPickOrder row = new DispatchPdaPickOrder();
|
||||
row.setId(IdWorker.getId());
|
||||
row.setTenantId(tenantId);
|
||||
row.setCustomerId(customerId);
|
||||
row.setPickId(pick.getId());
|
||||
row.setReceiptId(receipt.getId());
|
||||
row.setOrderId(receipt.getOrderId());
|
||||
@@ -60,7 +60,7 @@ public class DispatchPickOrderAttributionService {
|
||||
}
|
||||
if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders");
|
||||
for (Map.Entry<Long, Long> entry : pickedByOrder.entrySet()) {
|
||||
if (orders.increasePicked(tenantId, entry.getKey(), entry.getValue()) != 1) {
|
||||
if (orders.increasePicked(customerId, entry.getKey(), entry.getValue()) != 1) {
|
||||
throw conflict("Picked packages exceed tail-leg order remaining packages");
|
||||
}
|
||||
}
|
||||
@@ -68,17 +68,17 @@ public class DispatchPickOrderAttributionService {
|
||||
}
|
||||
|
||||
/** Call once for each departing PDA pick; repeated dispatch departure is protected by its outer idempotency key. */
|
||||
public List<OrderPackages> depart(String tenantId, String outboundNo, String dispatchNo, List<DispatchPdaPick> picks,
|
||||
public List<OrderPackages> depart(String customerId, String outboundNo, String dispatchNo, List<DispatchPdaPick> picks,
|
||||
LocalDateTime departedAt) {
|
||||
Map<Long, Long> shippedByOrder = new LinkedHashMap<>();
|
||||
for (DispatchPdaPick pick : picks) {
|
||||
for (DispatchPdaPickOrder row : attributions.listPick(tenantId, pick.getId())) {
|
||||
for (DispatchPdaPickOrder row : attributions.listPick(customerId, pick.getId())) {
|
||||
shippedByOrder.merge(row.getOrderId(), value(row.getPackages()), Long::sum);
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Long, Long> entry : shippedByOrder.entrySet()) {
|
||||
LastlegOrder before = orders.lockById(tenantId, entry.getKey());
|
||||
if (before == null || orders.transferPickedToShipped(tenantId, entry.getKey(), entry.getValue(), outboundNo) != 1) {
|
||||
LastlegOrder before = orders.lockById(customerId, entry.getKey());
|
||||
if (before == null || orders.transferPickedToShipped(customerId, entry.getKey(), entry.getValue(), outboundNo) != 1) {
|
||||
throw conflict("Tail-leg order picked packages are inconsistent");
|
||||
}
|
||||
long shipped = value(before.getShippedPackageCount()) + entry.getValue();
|
||||
|
||||
@@ -43,21 +43,21 @@ public class DispatchService {
|
||||
@Resource private DispatchPalletLockMapper locks;
|
||||
@Resource private DispatchCandidateMapper candidates;
|
||||
|
||||
public List<DispatchPalletCandidate> candidates(String tenantId, String orderNo,
|
||||
public List<DispatchPalletCandidate> candidates(String customerId, String orderNo,
|
||||
String fbaWarehouseCode, String sourceStage) {
|
||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
||||
}
|
||||
return candidates.available(tenantId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
|
||||
return candidates.available(customerId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchResponse preArrange(String tenantId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||
List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList();
|
||||
if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty");
|
||||
}
|
||||
List<DispatchPalletCandidate> selected = candidates.lockSelected(tenantId, palletIds);
|
||||
List<DispatchPalletCandidate> selected = candidates.lockSelected(customerId, palletIds);
|
||||
if (selected.size() != palletIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
||||
}
|
||||
@@ -71,9 +71,9 @@ public class DispatchService {
|
||||
}
|
||||
for (DispatchPalletCandidate candidate : selected) {
|
||||
boolean planLocked = candidate.planPalletId() != null
|
||||
&& locks.lockActive(tenantId, "PLAN:" + candidate.planPalletId()) != null;
|
||||
&& locks.lockActive(customerId, "PLAN:" + candidate.planPalletId()) != null;
|
||||
boolean receivedLocked = candidate.warehousePalletId() != null
|
||||
&& locks.lockActive(tenantId, "RECEIVED:" + candidate.warehousePalletId()) != null;
|
||||
&& locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId()) != null;
|
||||
if (planLocked || receivedLocked) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
"Pallet is already locked by another active dispatch: " + candidate.palletNo());
|
||||
@@ -83,7 +83,7 @@ public class DispatchService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
DispatchOrder dispatch = new DispatchOrder();
|
||||
dispatch.setId(IdWorker.getId());
|
||||
dispatch.setTenantId(tenantId);
|
||||
dispatch.setCustomerId(customerId);
|
||||
dispatch.setDispatchNo("PC" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + dispatch.getId());
|
||||
dispatch.setOverseasWarehouseId(selected.get(0).overseasWarehouseId());
|
||||
dispatch.setOverseasWarehouseName(selected.get(0).overseasWarehouseName());
|
||||
@@ -106,7 +106,7 @@ public class DispatchService {
|
||||
for (DispatchPalletCandidate candidate : selected) {
|
||||
DispatchPalletLock lock = new DispatchPalletLock();
|
||||
lock.setId(IdWorker.getId());
|
||||
lock.setTenantId(tenantId);
|
||||
lock.setCustomerId(customerId);
|
||||
lock.setSourcePalletKey(candidate.sourcePalletKey());
|
||||
lock.setActiveKey(candidate.sourcePalletKey());
|
||||
lock.setPlanId(candidate.planId());
|
||||
@@ -126,7 +126,7 @@ public class DispatchService {
|
||||
DispatchItem item = new DispatchItem();
|
||||
item.setId(IdWorker.getId());
|
||||
item.setDispatchOrderId(dispatch.getId());
|
||||
item.setTenantId(tenantId);
|
||||
item.setCustomerId(customerId);
|
||||
item.setPlanId(candidate.planId());
|
||||
item.setPlanPalletId(candidate.planPalletId());
|
||||
item.setWarehousePalletId(candidate.warehousePalletId());
|
||||
@@ -147,12 +147,12 @@ public class DispatchService {
|
||||
item.setCreatedAt(now);
|
||||
items.insert(item);
|
||||
}
|
||||
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId()));
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
public PageResult<DispatchResponse> page(String tenantId, DispatchRequests.Query query) {
|
||||
public PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query) {
|
||||
Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()),
|
||||
new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getTenantId, tenantId)
|
||||
new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
|
||||
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
||||
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
|
||||
@@ -160,18 +160,18 @@ public class DispatchService {
|
||||
query.fbaAppointmentNo())
|
||||
.orderByDesc(DispatchOrder::getCreatedAt));
|
||||
List<DispatchResponse> records = page.getRecords().stream()
|
||||
.map(order -> response(order, items.findByDispatchOrder(tenantId, order.getId()))).toList();
|
||||
.map(order -> response(order, items.findByDispatchOrder(customerId, order.getId()))).toList();
|
||||
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public DispatchResponse detail(String tenantId, String dispatchNo) {
|
||||
DispatchOrder dispatch = find(tenantId, dispatchNo);
|
||||
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId()));
|
||||
public DispatchResponse detail(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = find(customerId, dispatchNo);
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public DispatchResponse update(String tenantId, String dispatchNo, DispatchRequests.Update request) {
|
||||
DispatchOrder dispatch = orders.lockById(tenantId, find(tenantId, dispatchNo).getId());
|
||||
public DispatchResponse update(String customerId, String dispatchNo, DispatchRequests.Update request) {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
// Vehicle plate is intentionally editable through all dispatch states.
|
||||
dispatch.setVehiclePlate(trimToNull(request.vehiclePlate()));
|
||||
@@ -193,12 +193,12 @@ public class DispatchService {
|
||||
}
|
||||
dispatch.setUpdatedAt(LocalDateTime.now());
|
||||
orders.updateById(dispatch);
|
||||
return response(dispatch, items.findByDispatchOrder(tenantId, dispatch.getId()));
|
||||
return response(dispatch, items.findByDispatchOrder(customerId, dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void cancel(String tenantId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.lockById(tenantId, find(tenantId, dispatchNo).getId());
|
||||
public void cancel(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
if (!(PENDING_DEPARTURE.equals(dispatch.getStatus()))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Dispatched order cannot be cancelled");
|
||||
@@ -208,7 +208,7 @@ public class DispatchService {
|
||||
dispatch.setUpdatedAt(now);
|
||||
orders.updateById(dispatch);
|
||||
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||
.eq(DispatchPalletLock::getTenantId, tenantId)
|
||||
.eq(DispatchPalletLock::getCustomerId, customerId)
|
||||
.eq(DispatchPalletLock::getDispatchOrderId, dispatch.getId())
|
||||
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||
for (DispatchPalletLock lock : activeLocks) {
|
||||
@@ -220,8 +220,8 @@ public class DispatchService {
|
||||
}
|
||||
}
|
||||
|
||||
private DispatchOrder find(String tenantId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.findByDispatchNo(tenantId, dispatchNo);
|
||||
private DispatchOrder find(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = orders.findByDispatchNo(customerId, dispatchNo);
|
||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
return dispatch;
|
||||
}
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.dropship.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/v1/lastleg/dropship")
|
||||
public class DropshipOrderController {
|
||||
@Resource private DropshipOrderService service;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@PostMapping("/receive")
|
||||
public ApiResponse<Integer> receive(@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody DropshipRequests.Receive request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(service.receive(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.dropship.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.List;
|
||||
|
||||
public final class DropshipRequests {
|
||||
private DropshipRequests() {}
|
||||
public record Receive(@NotBlank String customerId, @NotNull Long orderId, @NotNull Long warehouseId,
|
||||
@NotBlank String idempotencyKey, @NotEmpty List<@Valid Pallet> pallets) {}
|
||||
public record Pallet(@NotBlank String palletNo, @NotEmpty List<@Valid SkuPackages> skuPackages) {}
|
||||
public record SkuPackages(@NotBlank String skuCode, String productName, @Positive long packageCount,
|
||||
@Positive long productQuantity,
|
||||
@NotEmpty List<@NotBlank String> cartonMarks) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_carton")
|
||||
public class DropshipCarton {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long profileId;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
private String skuCode;
|
||||
private String cartonMark;
|
||||
private Integer cartonSequence;
|
||||
private String status;
|
||||
private String outboundOrderNo;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_carton_ledger")
|
||||
public class DropshipCartonLedger {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long cartonId;
|
||||
private String cartonMark;
|
||||
private String skuCode;
|
||||
private String operationType;
|
||||
private Long beforePackages;
|
||||
private Long deltaPackages;
|
||||
private Long afterPackages;
|
||||
private String businessOrderNo;
|
||||
private String idempotencyKey;
|
||||
private String remark;
|
||||
private LocalDateTime occurredAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_order_profile")
|
||||
public class DropshipOrderProfile {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
/** Immutable pallet number snapshot supplied by the first-leg handover event. */
|
||||
private String sourcePalletNo;
|
||||
private Long inventoryPositionId;
|
||||
private String status;
|
||||
private String idempotencyKey;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_sku_inventory")
|
||||
public class DropshipSkuInventory {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long profileId;
|
||||
private String skuCode;
|
||||
private String productName;
|
||||
private Long packageCount;
|
||||
private Long productQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
public interface DropshipCartonLedgerMapper extends BaseMapper<DropshipCartonLedger> {
|
||||
@Select("SELECT * FROM ll_dropship_carton_ledger WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DropshipCartonLedger findIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND carton_mark=#{cartonMark} LIMIT 1")
|
||||
DropshipCarton find(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("cartonMark") String cartonMark);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProfile> {
|
||||
@Select("SELECT * FROM ll_dropship_order_profile WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY source_pallet_no")
|
||||
List<DropshipOrderProfile> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
|
||||
public interface DropshipSkuInventoryMapper extends BaseMapper<DropshipSkuInventory> {}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.baiyue.lastleg.dropship.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.entity.*;
|
||||
import com.baiyue.lastleg.dropship.mapper.*;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class DropshipOrderService {
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private DropshipSkuInventoryMapper skuInventory;
|
||||
@Resource private DropshipCartonMapper cartons;
|
||||
@Resource private DropshipCartonLedgerMapper ledger;
|
||||
@Resource private WarehouseResourceGateway warehouse;
|
||||
|
||||
@Transactional
|
||||
public int receive(DropshipRequests.Receive request) {
|
||||
LastlegOrder order = orders.selectById(request.orderId());
|
||||
if (order == null || !request.customerId().equals(order.getCustomerId())) throw conflict("Tail order not found");
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) throw conflict("Order is not a one-click dropship order");
|
||||
long totalPackages = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::packageCount).sum();
|
||||
long totalQuantity = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::productQuantity).sum();
|
||||
if (order.getPackageCount() != null && order.getPackageCount().longValue() != totalPackages) {
|
||||
throw conflict("SKU package total does not equal order package count");
|
||||
}
|
||||
if (order.getProductQuantity() != null && order.getProductQuantity().longValue() != totalQuantity) {
|
||||
throw conflict("SKU product quantity total does not equal order product quantity");
|
||||
}
|
||||
List<DropshipOrderProfile> existing = profiles.listOrder(request.customerId(), request.orderId());
|
||||
if (!existing.isEmpty()) {
|
||||
Set<String> existingPallets = existing.stream().map(DropshipOrderProfile::getSourcePalletNo)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
Set<String> requestedPallets = request.pallets().stream().map(value -> value.palletNo().trim())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (!existingPallets.equals(requestedPallets)) throw conflict("Dropship order pallet snapshot conflicts with existing data");
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
validateMarks(request);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (DropshipRequests.Pallet inputPallet : request.pallets()) {
|
||||
WarehouseResourceGateway.WarehousePalletReference pallet = warehouse.ensureDropshipPallet(
|
||||
request.customerId(), request.warehouseId(), request.orderId() + ":" + inputPallet.palletNo().trim());
|
||||
DropshipOrderProfile profile = profile(request, order, inputPallet, pallet, now);
|
||||
profiles.insert(profile);
|
||||
int sequence = 0;
|
||||
for (DropshipRequests.SkuPackages input : inputPallet.skuPackages()) {
|
||||
DropshipSkuInventory inventory = new DropshipSkuInventory();
|
||||
inventory.setId(IdWorker.getId()); inventory.setCustomerId(request.customerId()); inventory.setOrderId(order.getId());
|
||||
inventory.setProfileId(profile.getId()); inventory.setSkuCode(input.skuCode()); inventory.setProductName(input.productName());
|
||||
inventory.setPackageCount(input.packageCount()); inventory.setProductQuantity(input.productQuantity());
|
||||
inventory.setCreatedAt(now); inventory.setUpdatedAt(now);
|
||||
skuInventory.insert(inventory);
|
||||
for (String mark : input.cartonMarks()) {
|
||||
DropshipCarton carton = new DropshipCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
|
||||
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(pallet.palletId()); carton.setPalletNo(pallet.palletNo());
|
||||
carton.setSkuCode(input.skuCode()); carton.setCartonMark(mark.trim()); carton.setCartonSequence(++sequence);
|
||||
carton.setStatus("AVAILABLE"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
ledger.insert(stockInFact(request, order, carton, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
private void validateMarks(DropshipRequests.Receive request) {
|
||||
Set<String> marks = new HashSet<>();
|
||||
Set<String> palletNos = new HashSet<>();
|
||||
for (DropshipRequests.Pallet pallet : request.pallets()) {
|
||||
if (!palletNos.add(pallet.palletNo().trim())) throw conflict("Duplicate pallet number: " + pallet.palletNo());
|
||||
Set<String> skus = new HashSet<>();
|
||||
for (DropshipRequests.SkuPackages item : pallet.skuPackages()) {
|
||||
if (item.packageCount() != item.cartonMarks().size()) {
|
||||
throw conflict("SKU packageCount must equal cartonMarks count: " + item.skuCode());
|
||||
}
|
||||
if (!skus.add(item.skuCode().trim())) throw conflict("Duplicate SKU on pallet: " + item.skuCode());
|
||||
for (String mark : item.cartonMarks()) {
|
||||
if (!marks.add(mark.trim())) throw conflict("Duplicate carton mark: " + mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DropshipOrderProfile profile(DropshipRequests.Receive request, LastlegOrder order,
|
||||
DropshipRequests.Pallet input,
|
||||
WarehouseResourceGateway.WarehousePalletReference pallet, LocalDateTime now) {
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile();
|
||||
profile.setId(IdWorker.getId()); profile.setCustomerId(request.customerId()); profile.setOrderId(order.getId());
|
||||
profile.setWarehousePalletId(pallet.palletId()); profile.setPalletNo(pallet.palletNo());
|
||||
profile.setSourcePalletNo(input.palletNo().trim());
|
||||
profile.setStatus("RECEIVED"); profile.setVersion(0L);
|
||||
profile.setIdempotencyKey(request.idempotencyKey() + ":" + input.palletNo().trim());
|
||||
profile.setCreatedAt(now); profile.setUpdatedAt(now);
|
||||
return profile;
|
||||
}
|
||||
|
||||
private DropshipCartonLedger stockInFact(DropshipRequests.Receive request, LastlegOrder order,
|
||||
DropshipCarton carton, LocalDateTime now) {
|
||||
DropshipCartonLedger fact = new DropshipCartonLedger();
|
||||
fact.setId(IdWorker.getId()); fact.setCustomerId(request.customerId()); fact.setOrderId(order.getId());
|
||||
fact.setCartonId(carton.getId()); fact.setCartonMark(carton.getCartonMark()); fact.setSkuCode(carton.getSkuCode());
|
||||
fact.setOperationType("STOCK_IN"); fact.setBeforePackages(0L); fact.setDeltaPackages(1L); fact.setAfterPackages(1L);
|
||||
fact.setBusinessOrderNo(order.getOrderNo());
|
||||
fact.setIdempotencyKey(request.idempotencyKey() + ":" + carton.getCartonMark());
|
||||
fact.setOccurredAt(now); fact.setCreatedAt(now);
|
||||
return fact;
|
||||
}
|
||||
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
package com.baiyue.lastleg.entity;
|
||||
import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime;
|
||||
@Data @TableName("ll_loading_event_inbox") public class LastlegEventInbox { @TableId private String eventId; private String tenantId; private String eventType; private Integer eventVersion; private String payloadHash; private String payloadJson; private String status; private LocalDateTime receivedAt; private LocalDateTime processedAt; private String lastError; }
|
||||
@Data @TableName("ll_loading_event_inbox") public class LastlegEventInbox { @TableId private String eventId; private String customerId; private String eventType; private Integer eventVersion; private String payloadHash; private String payloadJson; private String status; private LocalDateTime receivedAt; private LocalDateTime processedAt; private String lastError; }
|
||||
|
||||
@@ -13,7 +13,7 @@ public class LastlegOrder {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String orderNo;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String orderType;
|
||||
private String inboundCode;
|
||||
private String sourceRegion;
|
||||
|
||||
+10
-10
@@ -28,15 +28,15 @@ public class InboundPlanController {
|
||||
@Operation(summary = "Convert the entire container to one inbound plan")
|
||||
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ConvertInboundPlanRequest request) {
|
||||
return ApiResponse.success(service.convert(scope(user, request.tenantId()), request.orderId()));
|
||||
return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{planNo}")
|
||||
@Operation(summary = "Get an inbound plan with lines, pallets and allocations")
|
||||
public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(service.detailByPlanNo(scope(user, tenantId), planNo));
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.detailByPlanNo(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
@PostMapping("/fba-appointments/batch")
|
||||
@@ -44,9 +44,9 @@ public class InboundPlanController {
|
||||
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody BatchFbaAppointmentRequest request) {
|
||||
String tenantId = scope(user, request.tenantId());
|
||||
String customerId = scope(user, request.customerId());
|
||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
return ApiResponse.success(appointmentService.batchCreate(tenantId, operatorId, request));
|
||||
return ApiResponse.success(appointmentService.batchCreate(customerId, operatorId, request));
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/{appointmentNo}")
|
||||
@@ -54,20 +54,20 @@ public class InboundPlanController {
|
||||
public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String appointmentNo,
|
||||
@RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(appointmentService.detail(scope(user, tenantId), appointmentNo));
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(appointmentService.detail(scope(user, customerId), appointmentNo));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public record BatchFbaAppointmentRequest(
|
||||
String tenantId,
|
||||
String customerId,
|
||||
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
|
||||
@NotBlank @Size(max = 64) String appointmentNo,
|
||||
@NotNull LocalDateTime appointmentAt,
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record ConvertInboundPlanRequest(
|
||||
@NotNull Long orderId,
|
||||
String tenantId
|
||||
String customerId
|
||||
) {
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import java.util.List;
|
||||
|
||||
public record FbaAppointmentResponse(
|
||||
Long id,
|
||||
String tenantId,
|
||||
String customerId,
|
||||
String appointmentNo,
|
||||
LocalDateTime appointmentAt,
|
||||
String appointmentStatus,
|
||||
|
||||
@@ -7,7 +7,7 @@ import java.util.List;
|
||||
public record InboundPlanResponse(
|
||||
Long id,
|
||||
String planNo,
|
||||
String tenantId,
|
||||
String customerId,
|
||||
String containerNo,
|
||||
String status,
|
||||
int lineCount,
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
public class InboundFbaAppointment {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String appointmentNo;
|
||||
private LocalDateTime appointmentAt;
|
||||
private String appointmentStatus;
|
||||
|
||||
@@ -14,7 +14,7 @@ public class InboundPlan {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String planNo;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String containerNo;
|
||||
private String status;
|
||||
private Integer lineCount;
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ public interface InboundFbaAppointmentQueryMapper {
|
||||
"SELECT l.id AS lineId,l.plan_id AS planId,l.order_id AS orderId,l.inbound_code AS inboundCode," +
|
||||
"l.fba_warehouse_code AS fbaWarehouseCode " +
|
||||
"FROM ll_inbound_plan_line l JOIN ll_inbound_plan p ON p.id=l.plan_id " +
|
||||
"WHERE p.tenant_id=#{tenantId} AND l.id IN " +
|
||||
"WHERE p.customer_id=#{customerId} AND l.id IN " +
|
||||
"<foreach collection='lineIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||
"ORDER BY l.id FOR UPDATE" +
|
||||
"</script>")
|
||||
List<FbaAppointmentLineSnapshot> lockLines(@Param("tenantId") String tenantId,
|
||||
List<FbaAppointmentLineSnapshot> lockLines(@Param("customerId") String customerId,
|
||||
@Param("lineIds") List<Long> lineIds);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPlanMapper extends BaseMapper<InboundPlan> {
|
||||
@Select("SELECT * FROM ll_inbound_plan WHERE tenant_id=#{tenantId} AND container_no=#{containerNo} LIMIT 1")
|
||||
InboundPlan findByContainer(@Param("tenantId") String tenantId, @Param("containerNo") String containerNo);
|
||||
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND container_no=#{containerNo} LIMIT 1")
|
||||
InboundPlan findByContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
|
||||
}
|
||||
|
||||
+8
-8
@@ -12,10 +12,10 @@ import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPlanOrderMapper {
|
||||
@Select("SELECT container_no FROM ll_order WHERE tenant_id=#{tenantId} AND id=#{orderId} LIMIT 1")
|
||||
String findContainerNo(@Param("tenantId") String tenantId, @Param("orderId") Long orderId);
|
||||
@Select("SELECT container_no FROM ll_order WHERE customer_id=#{customerId} AND id=#{orderId} LIMIT 1")
|
||||
String findContainerNo(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
|
||||
@Select("SELECT o.id AS orderId,o.tenant_id AS tenantId,o.container_no AS containerNo," +
|
||||
@Select("SELECT o.id AS orderId,o.customer_id AS customerId,o.container_no AS containerNo," +
|
||||
"o.inbound_code AS inboundCode,o.current_status AS currentStatus,o.inbound_order_no AS inboundOrderNo," +
|
||||
"COALESCE(o.volume_cbm,0) AS volumeCbm,COALESCE(o.package_count,0) AS packageCount," +
|
||||
"NULLIF(o.fba_warehouse_code,'') AS fbaWarehouseCode," +
|
||||
@@ -29,17 +29,17 @@ public interface InboundPlanOrderMapper {
|
||||
"<> COALESCE(NULLIF(o.transfer_warehouse_id,''),NULLIF(o.transfer_warehouse_code,'')) THEN 1 ELSE 0 END) " +
|
||||
"AS destinationMismatchCount " +
|
||||
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id " +
|
||||
"WHERE o.tenant_id=#{tenantId} AND o.container_no=#{containerNo} " +
|
||||
"GROUP BY o.id,o.tenant_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," +
|
||||
"WHERE o.customer_id=#{customerId} AND o.container_no=#{containerNo} " +
|
||||
"GROUP BY o.id,o.customer_id,o.container_no,o.inbound_code,o.current_status,o.inbound_order_no,o.volume_cbm,o.package_count,o.fba_warehouse_code," +
|
||||
"o.transfer_warehouse_id,o.transfer_warehouse_code,o.transfer_warehouse_name " +
|
||||
"ORDER BY o.id FOR UPDATE")
|
||||
List<SourceOrderSnapshot> lockContainerOrders(@Param("tenantId") String tenantId,
|
||||
List<SourceOrderSnapshot> lockContainerOrders(@Param("customerId") String customerId,
|
||||
@Param("containerNo") String containerNo);
|
||||
|
||||
@Update("UPDATE ll_order SET inbound_order_no=#{planNo},current_status='INBOUND_PLANNED'," +
|
||||
"pallet_merge_flag='MERGED',pallet_count=#{palletCount},updated_at=#{updatedAt} " +
|
||||
"WHERE tenant_id=#{tenantId} AND id=#{orderId}")
|
||||
int markPlanned(@Param("tenantId") String tenantId, @Param("orderId") Long orderId,
|
||||
"WHERE customer_id=#{customerId} AND id=#{orderId}")
|
||||
int markPlanned(@Param("customerId") String customerId, @Param("orderId") Long orderId,
|
||||
@Param("planNo") String planNo, @Param("palletCount") int palletCount,
|
||||
@Param("updatedAt") LocalDateTime updatedAt);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import java.math.BigDecimal;
|
||||
|
||||
public record SourceOrderSnapshot(
|
||||
Long orderId,
|
||||
String tenantId,
|
||||
String customerId,
|
||||
String containerNo,
|
||||
String inboundCode,
|
||||
String currentStatus,
|
||||
@@ -18,12 +18,12 @@ public record SourceOrderSnapshot(
|
||||
int destinationCount,
|
||||
int destinationMismatchCount
|
||||
) {
|
||||
public SourceOrderSnapshot(Long orderId, String tenantId, String containerNo, String inboundCode,
|
||||
public SourceOrderSnapshot(Long orderId, String customerId, String containerNo, String inboundCode,
|
||||
String currentStatus, String inboundOrderNo, BigDecimal volumeCbm,
|
||||
int packageCount, String destinationWarehouseId,
|
||||
String destinationWarehouseCode, String destinationWarehouseName,
|
||||
int destinationCount, int destinationMismatchCount) {
|
||||
this(orderId, tenantId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
this(orderId, customerId, containerNo, inboundCode, currentStatus, inboundOrderNo, volumeCbm,
|
||||
packageCount, null, destinationWarehouseId, destinationWarehouseCode,
|
||||
destinationWarehouseName, destinationCount, destinationMismatchCount);
|
||||
}
|
||||
|
||||
+5
-5
@@ -24,19 +24,19 @@ public class InboundPalletAssignmentController {
|
||||
@PostMapping("/{planPalletId}/assign")
|
||||
public ApiResponse<Void> assign(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo, @PathVariable long planPalletId,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.Assign request) {
|
||||
service.assign(scope(user, tenantId), planNo, planPalletId, request);
|
||||
service.assign(scope(user, customerId), planNo, planPalletId, request);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+17
-17
@@ -28,66 +28,66 @@ public class InboundPdaController {
|
||||
@PostMapping("/push")
|
||||
public ApiResponse<PdaResponses.Task> push(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.Push request) {
|
||||
return ApiResponse.success(service.push(scope(user, tenantId), planNo, request));
|
||||
return ApiResponse.success(service.push(scope(user, customerId), planNo, request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PdaResponses.Task> task(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(service.task(scope(user, tenantId), planNo));
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.task(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
@PostMapping("/allocations/{allocationId}/count")
|
||||
public ApiResponse<PdaResponses.Operation> count(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@PathVariable long allocationId,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.PackageOperation request) {
|
||||
return ApiResponse.success(service.count(scope(user, tenantId), planNo, allocationId, request));
|
||||
return ApiResponse.success(service.count(scope(user, customerId), planNo, allocationId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/allocations/{allocationId}/pick")
|
||||
public ApiResponse<PdaResponses.Operation> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@PathVariable long allocationId,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.PackageOperation request) {
|
||||
return ApiResponse.success(service.pick(scope(user, tenantId), planNo, allocationId, request));
|
||||
return ApiResponse.success(service.pick(scope(user, customerId), planNo, allocationId, request));
|
||||
}
|
||||
|
||||
@GetMapping("/allocations/{allocationId}/merge-candidates")
|
||||
public ApiResponse<List<PdaResponses.MergeCandidate>> candidates(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String planNo,
|
||||
@PathVariable long allocationId, @RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(service.mergeCandidates(scope(user, tenantId), planNo, allocationId));
|
||||
@PathVariable long allocationId, @RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.mergeCandidates(scope(user, customerId), planNo, allocationId));
|
||||
}
|
||||
|
||||
@PostMapping("/allocations/{allocationId}/merge")
|
||||
public ApiResponse<PdaResponses.Merge> merge(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@PathVariable long allocationId,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid @RequestBody PdaRequests.ConfirmMerge request) {
|
||||
return ApiResponse.success(service.merge(scope(user, tenantId), planNo, allocationId, request));
|
||||
return ApiResponse.success(service.merge(scope(user, customerId), planNo, allocationId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/complete")
|
||||
public ApiResponse<PdaResponses.Task> complete(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String tenantId) {
|
||||
return ApiResponse.success(service.complete(scope(user, tenantId), planNo));
|
||||
@RequestParam(required = false) String customerId) {
|
||||
return ApiResponse.success(service.complete(scope(user, customerId), planNo));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InboundPalletMerge {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long taskId;
|
||||
private String mergeGroupNo;
|
||||
private Long sourcePalletId;
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
public class InboundPdaOperation {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long taskId;
|
||||
private Long allocationId;
|
||||
private String action;
|
||||
|
||||
@@ -13,7 +13,7 @@ public class InboundPdaTask {
|
||||
@TableId(type = IdType.INPUT)
|
||||
private Long id;
|
||||
private Long planId;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String taskNo;
|
||||
private String status;
|
||||
private String operatorId;
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InboundStockReceipt {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long planId;
|
||||
private Long taskId;
|
||||
private Long allocationId;
|
||||
|
||||
+20
-6
@@ -27,12 +27,24 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long reserve(String tenantId, long warehouseId, long palletId, long locationId,
|
||||
public WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId) {
|
||||
WarehousePalletResponse response = client.post().uri("/internal/v1/warehouse-pallets/ensure-dropship")
|
||||
.header("X-Internal-Service-Token", internalToken)
|
||||
.body(Map.of("customerId", Long.parseLong(customerId), "warehouseId", warehouseId, "businessId", businessId))
|
||||
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw new IllegalStateException("Warehouse rejected dropship pallet: HTTP " + result.getStatusCode());
|
||||
}).body(WarehousePalletResponse.class);
|
||||
if (response == null || response.data() == null) throw new IllegalStateException("Warehouse returned an empty pallet response");
|
||||
return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().locationId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long reserve(String customerId, long warehouseId, long palletId, long locationId,
|
||||
String businessType, String businessId, String reservationKey) {
|
||||
ReserveEnvelope response = client.post()
|
||||
.uri("/internal/v1/warehouse-resource-locks")
|
||||
.header("X-Internal-Service-Token", internalToken)
|
||||
.body(Map.of("tenantId", Long.parseLong(tenantId), "warehouseId", warehouseId,
|
||||
.body(Map.of("customerId", Long.parseLong(customerId), "warehouseId", warehouseId,
|
||||
"palletId", palletId, "locationId", locationId, "businessType", businessType,
|
||||
"businessId", businessId, "reservationKey", reservationKey))
|
||||
.retrieve()
|
||||
@@ -45,11 +57,11 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrap(String tenantId, long resourceLockId) {
|
||||
public void scrap(String customerId, long resourceLockId) {
|
||||
client.post()
|
||||
.uri("/internal/v1/warehouse-resource-locks/{lockId}/scrap", resourceLockId)
|
||||
.header("X-Internal-Service-Token", internalToken)
|
||||
.body(Map.of("tenantId", Long.parseLong(tenantId)))
|
||||
.body(Map.of("customerId", Long.parseLong(customerId)))
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {
|
||||
throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode());
|
||||
@@ -58,11 +70,11 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrapPallet(String tenantId, long palletId) {
|
||||
public void scrapPallet(String customerId, long palletId) {
|
||||
client.post()
|
||||
.uri("/internal/v1/warehouse-resource-locks/pallets/{palletId}/scrap", palletId)
|
||||
.header("X-Internal-Service-Token", internalToken)
|
||||
.body(Map.of("tenantId", Long.parseLong(tenantId)))
|
||||
.body(Map.of("customerId", Long.parseLong(customerId)))
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, response) -> {
|
||||
throw new IllegalStateException("Warehouse rejected pallet scrap: HTTP " + response.getStatusCode());
|
||||
@@ -70,6 +82,8 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
.toBodilessEntity();
|
||||
}
|
||||
|
||||
private record WarehousePalletResponse(WarehousePalletData data) {}
|
||||
private record WarehousePalletData(long id, String palletCode, long locationId) {}
|
||||
private record ReserveEnvelope(ReserveData data) {}
|
||||
private record ReserveData(long lockId) {}
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,8 +1,11 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.gateway;
|
||||
|
||||
public interface WarehouseResourceGateway {
|
||||
long reserve(String tenantId, long warehouseId, long palletId, long locationId,
|
||||
WarehousePalletReference ensureDropshipPallet(String customerId, long warehouseId, String businessId);
|
||||
long reserve(String customerId, long warehouseId, long palletId, long locationId,
|
||||
String businessType, String businessId, String reservationKey);
|
||||
void scrap(String tenantId, long resourceLockId);
|
||||
void scrapPallet(String tenantId, long palletId);
|
||||
void scrap(String customerId, long resourceLockId);
|
||||
void scrapPallet(String customerId, long palletId);
|
||||
|
||||
record WarehousePalletReference(long palletId, String palletNo, long locationId) {}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,6 +8,6 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPdaOperationMapper extends BaseMapper<InboundPdaOperation> {
|
||||
@Select("SELECT * FROM ll_inbound_pda_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1")
|
||||
InboundPdaOperation findByKey(@Param("tenantId") String tenantId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_inbound_pda_operation WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
InboundPdaOperation findByKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
}
|
||||
|
||||
+4
-4
@@ -8,9 +8,9 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InboundPdaTaskMapper extends BaseMapper<InboundPdaTask> {
|
||||
@Select("SELECT * FROM ll_inbound_pda_task WHERE tenant_id=#{tenantId} AND plan_id=#{planId} LIMIT 1 FOR UPDATE")
|
||||
InboundPdaTask lockByPlan(@Param("tenantId") String tenantId, @Param("planId") Long planId);
|
||||
@Select("SELECT * FROM ll_inbound_pda_task WHERE customer_id=#{customerId} AND plan_id=#{planId} LIMIT 1 FOR UPDATE")
|
||||
InboundPdaTask lockByPlan(@Param("customerId") String customerId, @Param("planId") Long planId);
|
||||
|
||||
@Select("SELECT * FROM ll_inbound_pda_task WHERE tenant_id=#{tenantId} AND plan_id=#{planId} LIMIT 1")
|
||||
InboundPdaTask findByPlan(@Param("tenantId") String tenantId, @Param("planId") Long planId);
|
||||
@Select("SELECT * FROM ll_inbound_pda_task WHERE customer_id=#{customerId} AND plan_id=#{planId} LIMIT 1")
|
||||
InboundPdaTask findByPlan(@Param("customerId") String customerId, @Param("planId") Long planId);
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
public interface InboundStockReceiptMapper extends BaseMapper<InboundStockReceipt> {
|
||||
@Select("SELECT * FROM ll_inbound_stock_receipt WHERE tenant_id=#{tenantId} AND task_id=#{taskId} " +
|
||||
@Select("SELECT * FROM ll_inbound_stock_receipt WHERE customer_id=#{customerId} AND task_id=#{taskId} " +
|
||||
"ORDER BY allocation_id")
|
||||
List<InboundStockReceipt> listTask(@Param("tenantId") String tenantId, @Param("taskId") Long taskId);
|
||||
List<InboundStockReceipt> listTask(@Param("customerId") String customerId, @Param("taskId") Long taskId);
|
||||
}
|
||||
|
||||
+5
-5
@@ -30,14 +30,14 @@ public class PdaWorkflowRepository {
|
||||
@Resource private InboundPdaOperationMapper operations;
|
||||
@Resource private InboundPalletMergeMapper merges;
|
||||
|
||||
public InboundPlan lockPlan(String tenantId, String planNo) {
|
||||
public InboundPlan lockPlan(String customerId, String planNo) {
|
||||
return plans.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getTenantId, tenantId).eq(InboundPlan::getPlanNo, planNo).last("FOR UPDATE"));
|
||||
.eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo).last("FOR UPDATE"));
|
||||
}
|
||||
|
||||
public InboundPdaTask lockTask(String tenantId, Long planId) { return tasks.lockByPlan(tenantId, planId); }
|
||||
public InboundPdaTask task(String tenantId, Long planId) { return tasks.findByPlan(tenantId, planId); }
|
||||
public InboundPdaOperation operation(String tenantId, String key) { return operations.findByKey(tenantId, key); }
|
||||
public InboundPdaTask lockTask(String customerId, Long planId) { return tasks.lockByPlan(customerId, planId); }
|
||||
public InboundPdaTask task(String customerId, Long planId) { return tasks.findByPlan(customerId, planId); }
|
||||
public InboundPdaOperation operation(String customerId, String key) { return operations.findByKey(customerId, key); }
|
||||
|
||||
public InboundPlanPalletAllocation lockAllocation(Long planId, Long allocationId) {
|
||||
return allocations.selectOne(new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
|
||||
+44
-44
@@ -55,22 +55,22 @@ public class InboundPdaWorkflowService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Task push(String tenantId, String planNo, PdaRequests.Push request) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaOperation replay = replay(tenantId, "PUSH", request.idempotencyKey(), request);
|
||||
public PdaResponses.Task push(String customerId, String planNo, PdaRequests.Push request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "PUSH", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Task.class);
|
||||
|
||||
List<InboundPlanPallet> pallets = repository.pallets(plan.getId());
|
||||
if (pallets.isEmpty() || pallets.stream().anyMatch(this::isNotAssigned)) {
|
||||
throw conflict("Every planned pallet must have an assigned and locked warehouse pallet/location");
|
||||
}
|
||||
InboundPdaTask task = repository.lockTask(tenantId, plan.getId());
|
||||
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (task == null) {
|
||||
task = new InboundPdaTask();
|
||||
task.setId(IdWorker.getId());
|
||||
task.setPlanId(plan.getId());
|
||||
task.setTenantId(tenantId);
|
||||
task.setCustomerId(customerId);
|
||||
task.setTaskNo("PDA" + task.getId());
|
||||
task.setStatus("PDA_PENDING");
|
||||
task.setOperatorId(request.operatorId());
|
||||
@@ -85,13 +85,13 @@ public class InboundPdaWorkflowService {
|
||||
plan.setUpdatedAt(now);
|
||||
repository.update(plan);
|
||||
PdaResponses.Task result = task(task, planNo);
|
||||
saveOperation(tenantId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result);
|
||||
saveOperation(customerId, task.getId(), null, "PUSH", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void assign(String tenantId, String planNo, long planPalletId, PdaRequests.Assign request) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
public void assign(String customerId, String planNo, long planPalletId, PdaRequests.Assign request) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPlanPallet pallet = requirePallet(plan.getId(), planPalletId);
|
||||
validateDestinationWarehouse(pallet, request.warehouseId());
|
||||
if (pallet.getWarehouseResourceLockId() != null) {
|
||||
@@ -100,7 +100,7 @@ public class InboundPdaWorkflowService {
|
||||
throw conflict("Planned pallet is already assigned to another warehouse resource");
|
||||
}
|
||||
String businessId = planNo + ":" + planPalletId;
|
||||
long lockId = warehouse.reserve(tenantId, request.warehouseId(), request.palletId(), request.locationId(),
|
||||
long lockId = warehouse.reserve(customerId, request.warehouseId(), request.palletId(), request.locationId(),
|
||||
"INBOUND_PLAN_PALLET", businessId, request.idempotencyKey());
|
||||
pallet.setWarehousePalletId(request.palletId());
|
||||
pallet.setWarehouseLocationId(request.locationId());
|
||||
@@ -117,20 +117,20 @@ public class InboundPdaWorkflowService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Task task(String tenantId, String planNo) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaTask task = repository.task(tenantId, plan.getId());
|
||||
public PdaResponses.Task task(String customerId, String planNo) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaTask task = repository.task(customerId, plan.getId());
|
||||
if (task == null) throw notFound("PDA task not found");
|
||||
return task(task, planNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Operation count(String tenantId, String planNo, long allocationId,
|
||||
public PdaResponses.Operation count(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaOperation replay = replay(tenantId, "COUNT", request.idempotencyKey(), request);
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "COUNT", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(tenantId, plan);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
|
||||
int picked = value(allocation.getPickedPackageCount());
|
||||
if (request.packageCount() < picked) {
|
||||
@@ -147,17 +147,17 @@ public class InboundPdaWorkflowService {
|
||||
startPicking(task, plan, request.operatorId(), request.operatorName());
|
||||
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
|
||||
PdaResponses.Operation result = operation("COUNTED", allocation, line, List.of(), null, null, null);
|
||||
saveOperation(tenantId, task.getId(), allocationId, "COUNT", request.idempotencyKey(), request, result);
|
||||
saveOperation(customerId, task.getId(), allocationId, "COUNT", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Operation pick(String tenantId, String planNo, long allocationId,
|
||||
public PdaResponses.Operation pick(String customerId, String planNo, long allocationId,
|
||||
PdaRequests.PackageOperation request) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaOperation replay = replay(tenantId, "PICK", request.idempotencyKey(), request);
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "PICK", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Operation.class);
|
||||
InboundPdaTask task = requireActiveTask(tenantId, plan);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
|
||||
InboundPlanLine line = requireLine(plan.getId(), allocation.getLineId());
|
||||
int picked = value(allocation.getPickedPackageCount());
|
||||
@@ -169,7 +169,7 @@ public class InboundPdaWorkflowService {
|
||||
List<PdaResponses.MergeCandidate> candidates = candidates(plan, allocation, mergeCount);
|
||||
PdaResponses.Operation result = operation("MERGE_CONFIRMATION_REQUIRED", allocation, line,
|
||||
candidates, plannedRemaining, request.packageCount(), mergeCount);
|
||||
saveOperation(tenantId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
|
||||
saveOperation(customerId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
if (request.packageCount() > countedRemaining) {
|
||||
@@ -189,25 +189,25 @@ public class InboundPdaWorkflowService {
|
||||
repository.update(pallet);
|
||||
startPicking(task, plan, request.operatorId(), request.operatorName());
|
||||
PdaResponses.Operation result = operation("PICKED", allocation, line, List.of(), null, null, null);
|
||||
saveOperation(tenantId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
|
||||
saveOperation(customerId, task.getId(), allocationId, "PICK", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public List<PdaResponses.MergeCandidate> mergeCandidates(String tenantId, String planNo,
|
||||
public List<PdaResponses.MergeCandidate> mergeCandidates(String customerId, String planNo,
|
||||
long allocationId) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPlanPalletAllocation allocation = requireAllocation(plan.getId(), allocationId);
|
||||
return candidates(plan, allocation, 1);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Merge merge(String tenantId, String planNo, long targetAllocationId,
|
||||
public PdaResponses.Merge merge(String customerId, String planNo, long targetAllocationId,
|
||||
PdaRequests.ConfirmMerge request) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaOperation replay = replay(tenantId, "MERGE", request.idempotencyKey(), request);
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaOperation replay = replay(customerId, "MERGE", request.idempotencyKey(), request);
|
||||
if (replay != null) return read(replay, PdaResponses.Merge.class);
|
||||
InboundPdaTask task = requireActiveTask(tenantId, plan);
|
||||
InboundPdaTask task = requireActiveTask(customerId, plan);
|
||||
if (request.sourceAllocationId() == targetAllocationId) throw conflict("Source and target allocations differ");
|
||||
InboundPlanPalletAllocation target = requireAllocation(plan.getId(), targetAllocationId);
|
||||
InboundPlanPalletAllocation source = requireAllocation(plan.getId(), request.sourceAllocationId());
|
||||
@@ -240,7 +240,7 @@ public class InboundPdaWorkflowService {
|
||||
if (sourcePallet.getWarehouseResourceLockId() == null) {
|
||||
throw conflict("Source pallet has no warehouse resource lock");
|
||||
}
|
||||
warehouse.scrap(tenantId, sourcePallet.getWarehouseResourceLockId());
|
||||
warehouse.scrap(customerId, sourcePallet.getWarehouseResourceLockId());
|
||||
sourcePallet.setAssignmentStatus("SCRAPPED");
|
||||
sourcePallet.setWarehouseLocationId(null);
|
||||
sourcePallet.setWarehouseResourceLockId(null);
|
||||
@@ -251,7 +251,7 @@ public class InboundPdaWorkflowService {
|
||||
String groupNo = "PMG" + IdWorker.getId();
|
||||
InboundPalletMerge merge = new InboundPalletMerge();
|
||||
merge.setId(IdWorker.getId());
|
||||
merge.setTenantId(tenantId);
|
||||
merge.setCustomerId(customerId);
|
||||
merge.setTaskId(task.getId());
|
||||
merge.setMergeGroupNo(groupNo);
|
||||
merge.setSourcePalletId(sourcePallet.getId());
|
||||
@@ -270,14 +270,14 @@ public class InboundPdaWorkflowService {
|
||||
repository.insert(merge);
|
||||
PdaResponses.Merge result = new PdaResponses.Merge("MERGED", groupNo, sourceLine.getOrderId(),
|
||||
targetLine.getOrderId(), request.packageCount(), movedVolume, scrapped);
|
||||
saveOperation(tenantId, task.getId(), targetAllocationId, "MERGE", request.idempotencyKey(), request, result);
|
||||
saveOperation(customerId, task.getId(), targetAllocationId, "MERGE", request.idempotencyKey(), request, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PdaResponses.Task complete(String tenantId, String planNo) {
|
||||
InboundPlan plan = requirePlan(tenantId, planNo);
|
||||
InboundPdaTask task = repository.lockTask(tenantId, plan.getId());
|
||||
public PdaResponses.Task complete(String customerId, String planNo) {
|
||||
InboundPlan plan = requirePlan(customerId, planNo);
|
||||
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
|
||||
if (task == null) throw conflict("Push the inbound plan to PDA before operating");
|
||||
if ("COMPLETED".equals(task.getStatus())) return task(task, planNo);
|
||||
Map<Long, Integer> pickedByLine = new HashMap<>();
|
||||
@@ -293,7 +293,7 @@ public class InboundPdaWorkflowService {
|
||||
}
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
stockReceipts.stockIn(tenantId, plan, task, lines, allocations, repository.pallets(plan.getId()), now);
|
||||
stockReceipts.stockIn(customerId, plan, task, lines, allocations, repository.pallets(plan.getId()), now);
|
||||
task.setStatus("COMPLETED");
|
||||
task.setCompletedAt(now);
|
||||
task.setUpdatedAt(now);
|
||||
@@ -419,14 +419,14 @@ public class InboundPdaWorkflowService {
|
||||
|| !("ASSIGNED".equals(pallet.getAssignmentStatus()) || "LOCKED".equals(pallet.getAssignmentStatus()));
|
||||
}
|
||||
|
||||
private InboundPlan requirePlan(String tenantId, String planNo) {
|
||||
InboundPlan plan = repository.lockPlan(tenantId, planNo);
|
||||
private InboundPlan requirePlan(String customerId, String planNo) {
|
||||
InboundPlan plan = repository.lockPlan(customerId, planNo);
|
||||
if (plan == null) throw notFound("Inbound plan not found");
|
||||
return plan;
|
||||
}
|
||||
|
||||
private InboundPdaTask requireActiveTask(String tenantId, InboundPlan plan) {
|
||||
InboundPdaTask task = repository.lockTask(tenantId, plan.getId());
|
||||
private InboundPdaTask requireActiveTask(String customerId, InboundPlan plan) {
|
||||
InboundPdaTask task = repository.lockTask(customerId, plan.getId());
|
||||
if (task == null) throw conflict("Push the inbound plan to PDA before operating");
|
||||
if ("COMPLETED".equals(task.getStatus())) throw conflict("PDA task is already completed");
|
||||
return task;
|
||||
@@ -470,8 +470,8 @@ public class InboundPdaWorkflowService {
|
||||
return volume(line.getVolumeCbm()).divide(BigDecimal.valueOf(required), 8, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private InboundPdaOperation replay(String tenantId, String action, String key, Object request) {
|
||||
InboundPdaOperation operation = repository.operation(tenantId, key);
|
||||
private InboundPdaOperation replay(String customerId, String action, String key, Object request) {
|
||||
InboundPdaOperation operation = repository.operation(customerId, key);
|
||||
if (operation == null) return null;
|
||||
if (!action.equals(operation.getAction()) || !hash(request).equals(operation.getRequestHash())) {
|
||||
throw conflict("Idempotency key was already used with a different request");
|
||||
@@ -479,11 +479,11 @@ public class InboundPdaWorkflowService {
|
||||
return operation;
|
||||
}
|
||||
|
||||
private void saveOperation(String tenantId, Long taskId, Long allocationId, String action,
|
||||
private void saveOperation(String customerId, Long taskId, Long allocationId, String action,
|
||||
String key, Object request, Object result) {
|
||||
InboundPdaOperation operation = new InboundPdaOperation();
|
||||
operation.setId(IdWorker.getId());
|
||||
operation.setTenantId(tenantId);
|
||||
operation.setCustomerId(customerId);
|
||||
operation.setTaskId(taskId);
|
||||
operation.setAllocationId(allocationId);
|
||||
operation.setAction(action);
|
||||
|
||||
+7
-7
@@ -32,14 +32,14 @@ public class InboundStockReceiptService {
|
||||
@Resource private InboundStockReceiptMapper receipts;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
|
||||
public void stockIn(String tenantId, InboundPlan plan, InboundPdaTask task,
|
||||
public void stockIn(String customerId, InboundPlan plan, InboundPdaTask task,
|
||||
List<InboundPlanLine> lines, List<InboundPlanPalletAllocation> allocations,
|
||||
List<InboundPlanPallet> pallets, LocalDateTime receivedAt) {
|
||||
if (!receipts.listTask(tenantId, task.getId()).isEmpty()) return;
|
||||
if (!receipts.listTask(customerId, task.getId()).isEmpty()) return;
|
||||
|
||||
Map<Long, LastlegOrder> orderById = orders.selectBatchIds(lines.stream()
|
||||
.map(InboundPlanLine::getOrderId).collect(Collectors.toSet())).stream()
|
||||
.filter(order -> tenantId.equals(order.getTenantId()))
|
||||
.filter(order -> customerId.equals(order.getCustomerId()))
|
||||
.collect(Collectors.toMap(LastlegOrder::getId, order -> order));
|
||||
Map<Long, InboundPlanPallet> palletById = pallets.stream()
|
||||
.collect(Collectors.toMap(InboundPlanPallet::getId, pallet -> pallet));
|
||||
@@ -59,13 +59,13 @@ public class InboundStockReceiptService {
|
||||
throw conflict("PDA picked packages must equal the inbound order package total");
|
||||
}
|
||||
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
||||
persist(tenantId, plan, task, line, allocation,
|
||||
persist(customerId, plan, task, line, allocation,
|
||||
requirePallet(palletById, allocation), order, receivedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persist(String tenantId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line,
|
||||
private void persist(String customerId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line,
|
||||
InboundPlanPalletAllocation allocation, InboundPlanPallet pallet,
|
||||
LastlegOrder order, LocalDateTime receivedAt) {
|
||||
long packages = value(allocation.getPickedPackageCount());
|
||||
@@ -77,7 +77,7 @@ public class InboundStockReceiptService {
|
||||
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId();
|
||||
String overseasWarehouseId = required(order.getOverseasWarehouseId(),
|
||||
"Actual overseas warehouse is required for stock-in");
|
||||
InventoryCommandResponse result = inventory.stockIn(tenantId, task.getOperatorId(),
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
|
||||
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
|
||||
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
|
||||
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
|
||||
@@ -88,7 +88,7 @@ public class InboundStockReceiptService {
|
||||
|
||||
InboundStockReceipt receipt = new InboundStockReceipt();
|
||||
receipt.setId(IdWorker.getId());
|
||||
receipt.setTenantId(tenantId);
|
||||
receipt.setCustomerId(customerId);
|
||||
receipt.setPlanId(plan.getId());
|
||||
receipt.setTaskId(task.getId());
|
||||
receipt.setAllocationId(allocation.getId());
|
||||
|
||||
+11
-11
@@ -32,7 +32,7 @@ public class InboundFbaAppointmentService {
|
||||
@Resource private InboundFbaAppointmentQueryMapper queryMapper;
|
||||
|
||||
@Transactional
|
||||
public FbaAppointmentResponse batchCreate(String tenantId, String operatorId,
|
||||
public FbaAppointmentResponse batchCreate(String customerId, String operatorId,
|
||||
BatchFbaAppointmentRequest request) {
|
||||
List<Long> lineIds = List.copyOf(request.lineIds());
|
||||
if (new HashSet<>(lineIds).size() != lineIds.size()) {
|
||||
@@ -42,12 +42,12 @@ public class InboundFbaAppointmentService {
|
||||
String status = normalizeStatus(request.appointmentStatus());
|
||||
String remark = trimToNull(request.appointmentRemark());
|
||||
|
||||
InboundFbaAppointment existing = find(tenantId, appointmentNo);
|
||||
InboundFbaAppointment existing = find(customerId, appointmentNo);
|
||||
if (existing != null) {
|
||||
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
|
||||
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(tenantId, lineIds);
|
||||
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(customerId, lineIds);
|
||||
if (lines.size() != lineIds.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"One or more selected inbound lines do not exist in this tenant");
|
||||
@@ -55,7 +55,7 @@ public class InboundFbaAppointmentService {
|
||||
String fbaWarehouseCode = commonFbaWarehouse(lines);
|
||||
|
||||
// The row locks serialize concurrent submissions selecting the same inbound lines.
|
||||
existing = find(tenantId, appointmentNo);
|
||||
existing = find(customerId, appointmentNo);
|
||||
if (existing != null) {
|
||||
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
@@ -63,7 +63,7 @@ public class InboundFbaAppointmentService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundFbaAppointment appointment = new InboundFbaAppointment();
|
||||
appointment.setId(IdWorker.getId());
|
||||
appointment.setTenantId(tenantId);
|
||||
appointment.setCustomerId(customerId);
|
||||
appointment.setAppointmentNo(appointmentNo);
|
||||
appointment.setAppointmentAt(request.appointmentAt());
|
||||
appointment.setAppointmentStatus(status);
|
||||
@@ -76,7 +76,7 @@ public class InboundFbaAppointmentService {
|
||||
try {
|
||||
appointmentMapper.insert(appointment);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InboundFbaAppointment concurrent = find(tenantId, appointmentNo);
|
||||
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
|
||||
if (concurrent != null) {
|
||||
return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark);
|
||||
}
|
||||
@@ -99,8 +99,8 @@ public class InboundFbaAppointmentService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public FbaAppointmentResponse detail(String tenantId, String appointmentNo) {
|
||||
InboundFbaAppointment appointment = find(tenantId,
|
||||
public FbaAppointmentResponse detail(String customerId, String appointmentNo) {
|
||||
InboundFbaAppointment appointment = find(customerId,
|
||||
requiredTrimmed(appointmentNo, "Appointment number is required"));
|
||||
if (appointment == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
|
||||
@@ -134,9 +134,9 @@ public class InboundFbaAppointmentService {
|
||||
return warehouses.get(0);
|
||||
}
|
||||
|
||||
private InboundFbaAppointment find(String tenantId, String appointmentNo) {
|
||||
private InboundFbaAppointment find(String customerId, String appointmentNo) {
|
||||
return appointmentMapper.selectOne(new LambdaQueryWrapper<InboundFbaAppointment>()
|
||||
.eq(InboundFbaAppointment::getTenantId, tenantId)
|
||||
.eq(InboundFbaAppointment::getCustomerId, customerId)
|
||||
.eq(InboundFbaAppointment::getAppointmentNo, appointmentNo));
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ public class InboundFbaAppointmentService {
|
||||
|
||||
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
|
||||
List<InboundFbaAppointmentItem> items) {
|
||||
return new FbaAppointmentResponse(appointment.getId(), appointment.getTenantId(),
|
||||
return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
|
||||
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getAppointmentStatus(),
|
||||
appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
|
||||
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
|
||||
|
||||
+14
-14
@@ -36,20 +36,20 @@ public class InboundPlanService {
|
||||
@Resource private InboundPlanOrderMapper orderMapper;
|
||||
|
||||
@Transactional
|
||||
public InboundPlanResponse convert(String tenantId, Long selectedOrderId) {
|
||||
String containerNo = trimToNull(orderMapper.findContainerNo(tenantId, selectedOrderId));
|
||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||
String containerNo = trimToNull(orderMapper.findContainerNo(customerId, selectedOrderId));
|
||||
if (containerNo == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Order does not exist in this tenant or container number is empty");
|
||||
}
|
||||
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(tenantId, containerNo);
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockContainerOrders(customerId, containerNo);
|
||||
if (orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Container orders not found");
|
||||
}
|
||||
InboundPlan existing = planMapper.findByContainer(tenantId, containerNo);
|
||||
InboundPlan existing = planMapper.findByContainer(customerId, containerNo);
|
||||
if (existing != null) {
|
||||
return detail(tenantId, existing);
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
|
||||
@@ -57,7 +57,7 @@ public class InboundPlanService {
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(IdWorker.getId());
|
||||
plan.setPlanNo("LIP" + plan.getId());
|
||||
plan.setTenantId(tenantId);
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setContainerNo(containerNo);
|
||||
plan.setStatus("PENDING_ASSIGNMENT");
|
||||
plan.setLineCount(orders.size());
|
||||
@@ -88,23 +88,23 @@ public class InboundPlanService {
|
||||
InboundPlanLine line = lineByOrder.get(order.orderId());
|
||||
line.setPlannedPalletCount(count);
|
||||
lineMapper.updateById(line);
|
||||
if (orderMapper.markPlanned(tenantId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
|
||||
if (orderMapper.markPlanned(customerId, order.orderId(), plan.getPlanNo(), count, now) != 1) {
|
||||
throw new IllegalStateException("Failed to update order " + order.orderId());
|
||||
}
|
||||
orderMapper.insertHistory(order.orderId(), order.currentStatus(),
|
||||
"Container converted to inbound plan " + plan.getPlanNo(), now);
|
||||
}
|
||||
return detail(tenantId, plan);
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public InboundPlanResponse detailByPlanNo(String tenantId, String planNo) {
|
||||
public InboundPlanResponse detailByPlanNo(String customerId, String planNo) {
|
||||
InboundPlan plan = planMapper.selectOne(new LambdaQueryWrapper<InboundPlan>()
|
||||
.eq(InboundPlan::getTenantId, tenantId).eq(InboundPlan::getPlanNo, planNo));
|
||||
.eq(InboundPlan::getCustomerId, customerId).eq(InboundPlan::getPlanNo, planNo));
|
||||
if (plan == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
return detail(tenantId, plan);
|
||||
return detail(customerId, plan);
|
||||
}
|
||||
|
||||
private Map<Long, InboundPlanLine> createLines(InboundPlan plan, List<SourceOrderSnapshot> orders) {
|
||||
@@ -195,8 +195,8 @@ public class InboundPlanService {
|
||||
}
|
||||
}
|
||||
|
||||
private InboundPlanResponse detail(String tenantId, InboundPlan plan) {
|
||||
if (!tenantId.equals(plan.getTenantId())) {
|
||||
private InboundPlanResponse detail(String customerId, InboundPlan plan) {
|
||||
if (!customerId.equals(plan.getCustomerId())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inbound plan not found");
|
||||
}
|
||||
List<InboundPlanLine> lines = lineMapper.selectList(new LambdaQueryWrapper<InboundPlanLine>()
|
||||
@@ -207,7 +207,7 @@ public class InboundPlanService {
|
||||
new LambdaQueryWrapper<InboundPlanPalletAllocation>()
|
||||
.eq(InboundPlanPalletAllocation::getPlanId, plan.getId())
|
||||
.orderByAsc(InboundPlanPalletAllocation::getId));
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getTenantId(), plan.getContainerNo(),
|
||||
return new InboundPlanResponse(plan.getId(), plan.getPlanNo(), plan.getCustomerId(), plan.getContainerNo(),
|
||||
plan.getStatus(), plan.getLineCount(), plan.getPalletCount(), plan.getTotalVolumeCbm(),
|
||||
lines.stream().map(line -> new InboundPlanResponse.Line(line.getId(), line.getOrderId(), line.getInboundCode(), line.getFbaWarehouseCode(),
|
||||
line.getDestinationWarehouseId(), line.getDestinationWarehouseCode(), line.getDestinationWarehouseName(),
|
||||
|
||||
+11
-11
@@ -27,66 +27,66 @@ public class InventoryCommandController {
|
||||
@Operation(summary = "Put inbound-order packages into an overseas warehouse pallet")
|
||||
public ApiResponse<InventoryCommandResponse> stockIn(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.StockIn request) {
|
||||
return ApiResponse.success(service.stockIn(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.stockIn(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/hold")
|
||||
@Operation(summary = "Move available inbound-order packages to hold")
|
||||
public ApiResponse<InventoryCommandResponse> hold(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.hold(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.hold(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/release")
|
||||
@Operation(summary = "Release held inbound-order packages")
|
||||
public ApiResponse<InventoryCommandResponse> release(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.release(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.release(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/allocate")
|
||||
@Operation(summary = "Allocate available inbound-order packages to an outbound order")
|
||||
public ApiResponse<InventoryCommandResponse> allocate(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.allocate(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.allocate(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/unallocate")
|
||||
@Operation(summary = "Cancel an outbound allocation")
|
||||
public ApiResponse<InventoryCommandResponse> unallocate(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.unallocate(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.unallocate(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/pick")
|
||||
@Operation(summary = "Confirm picked inbound-order packages")
|
||||
public ApiResponse<InventoryCommandResponse> pick(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.pick(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.pick(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/ship")
|
||||
@Operation(summary = "Ship picked inbound-order packages and report whether the pallet is empty")
|
||||
public ApiResponse<InventoryCommandResponse> ship(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Move request) {
|
||||
return ApiResponse.success(service.ship(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.ship(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/adjust")
|
||||
@Operation(summary = "Adjust inbound-order package inventory after a physical count")
|
||||
public ApiResponse<InventoryCommandResponse> adjust(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody InventoryCommandRequests.Adjustment request) {
|
||||
return ApiResponse.success(service.adjust(scope(user, request.tenantId()), operator(user), request));
|
||||
return ApiResponse.success(service.adjust(scope(user, request.customerId()), operator(user), request));
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform request must specify target tenant");
|
||||
}
|
||||
return requestedTenant.trim();
|
||||
return requestedCustomer.trim();
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@ public final class InventoryCommandRequests {
|
||||
}
|
||||
|
||||
public record StockIn(
|
||||
String tenantId,
|
||||
String customerId,
|
||||
String customerName,
|
||||
@NotBlank String overseasWarehouseId,
|
||||
String overseasWarehouseCode,
|
||||
@@ -35,7 +35,7 @@ public final class InventoryCommandRequests {
|
||||
}
|
||||
|
||||
public record Move(
|
||||
String tenantId,
|
||||
String customerId,
|
||||
@NotNull Long positionId,
|
||||
@Positive long packages,
|
||||
String businessOrderNo,
|
||||
@@ -47,7 +47,7 @@ public final class InventoryCommandRequests {
|
||||
}
|
||||
|
||||
public record Adjustment(
|
||||
String tenantId,
|
||||
String customerId,
|
||||
@NotNull Long positionId,
|
||||
long packageDelta,
|
||||
@NotBlank String reason,
|
||||
|
||||
+6
-6
@@ -8,15 +8,15 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface InventoryCommandMapper {
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND id=#{positionId} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockPosition(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND id=#{positionId} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockPosition(@Param("customerId") String customerId,
|
||||
@Param("positionId") Long positionId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} AND id=#{balanceId} LIMIT 1 FOR UPDATE")
|
||||
InventoryBalance lockBalance(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} LIMIT 1 FOR UPDATE")
|
||||
InventoryBalance lockBalance(@Param("customerId") String customerId,
|
||||
@Param("balanceId") Long balanceId);
|
||||
|
||||
@Select("SELECT COALESCE(SUM(available_packages+hold_packages+allocated_packages+picked_packages+receiving_packages),0) " +
|
||||
"FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND pallet_id=#{palletId}")
|
||||
long palletPackages(@Param("tenantId") String tenantId, @Param("palletId") Long palletId);
|
||||
"FROM ll_inventory_position WHERE customer_id=#{customerId} AND pallet_id=#{palletId}")
|
||||
long palletPackages(@Param("customerId") String customerId, @Param("palletId") Long palletId);
|
||||
}
|
||||
|
||||
+46
-46
@@ -39,11 +39,11 @@ public class InventoryCommandService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse stockIn(String tenantId, String operatorId,
|
||||
public InventoryCommandResponse stockIn(String customerId, String operatorId,
|
||||
InventoryCommandRequests.StockIn request) {
|
||||
return execute(tenantId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> {
|
||||
InventoryBalance balance = balance(tenantId, request);
|
||||
InventoryPosition position = position(tenantId, balance, request);
|
||||
return execute(customerId, operatorId, "STOCK_IN", request.idempotencyKey(), request, operation -> {
|
||||
InventoryBalance balance = balance(customerId, request);
|
||||
InventoryPosition position = position(customerId, balance, request);
|
||||
long before = value(position.getAvailablePackages());
|
||||
position.setAvailablePackages(before + request.packages());
|
||||
position.setStatus("OCCUPIED");
|
||||
@@ -54,7 +54,7 @@ public class InventoryCommandService {
|
||||
balance.setAvailablePackages(value(balance.getAvailablePackages()) + request.packages());
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(tenantId, operatorId, operation.getId(), balance, position, "STOCK_IN",
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, "STOCK_IN",
|
||||
"OUTSIDE", "AVAILABLE", before, request.packages(), before + request.packages(),
|
||||
request.businessOrderNo(), request.inboundOrderNo(), null, request.sourceEventId(),
|
||||
request.idempotencyKey(), request.remark());
|
||||
@@ -63,34 +63,34 @@ public class InventoryCommandService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse hold(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(tenantId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request);
|
||||
public InventoryCommandResponse hold(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "HOLD", Bucket.AVAILABLE, Bucket.HOLD, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse release(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(tenantId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request);
|
||||
public InventoryCommandResponse release(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "RELEASE", Bucket.HOLD, Bucket.AVAILABLE, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse allocate(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(tenantId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request);
|
||||
public InventoryCommandResponse allocate(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "ALLOCATE", Bucket.AVAILABLE, Bucket.ALLOCATED, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse unallocate(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(tenantId, operatorId, "UNALLOCATE", Bucket.ALLOCATED, Bucket.AVAILABLE, request);
|
||||
public InventoryCommandResponse unallocate(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "UNALLOCATE", Bucket.ALLOCATED, Bucket.AVAILABLE, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse pick(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(tenantId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request);
|
||||
public InventoryCommandResponse pick(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return move(customerId, operatorId, "PICK", Bucket.ALLOCATED, Bucket.PICKED, request);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse ship(String tenantId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return execute(tenantId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(tenantId, request.positionId());
|
||||
public InventoryCommandResponse ship(String customerId, String operatorId, InventoryCommandRequests.Move request) {
|
||||
return execute(customerId, operatorId, "SHIP", request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
requireAtLeast(value(position.getPickedPackages()), request.packages(), "picked");
|
||||
@@ -105,9 +105,9 @@ public class InventoryCommandService {
|
||||
balance.setPickedPackages(value(balance.getPickedPackages()) - request.packages());
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
boolean palletEmpty = commands.palletPackages(tenantId, position.getPalletId()) == 0;
|
||||
if (palletEmpty) warehouseGateway.scrapPallet(tenantId, position.getPalletId());
|
||||
ledger(tenantId, operatorId, operation.getId(), balance, position, "SHIP",
|
||||
boolean palletEmpty = commands.palletPackages(customerId, position.getPalletId()) == 0;
|
||||
if (palletEmpty) warehouseGateway.scrapPallet(customerId, position.getPalletId());
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, "SHIP",
|
||||
"PICKED", "SHIPPED", before, -request.packages(), before - request.packages(),
|
||||
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
|
||||
request.idempotencyKey(), request.remark());
|
||||
@@ -116,13 +116,13 @@ public class InventoryCommandService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InventoryCommandResponse adjust(String tenantId, String operatorId,
|
||||
public InventoryCommandResponse adjust(String customerId, String operatorId,
|
||||
InventoryCommandRequests.Adjustment request) {
|
||||
if (request.packageDelta() == 0) {
|
||||
throw badRequest("Adjustment package delta cannot be zero");
|
||||
}
|
||||
return execute(tenantId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(tenantId, request.positionId());
|
||||
return execute(customerId, operatorId, "ADJUST", request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
long before = value(position.getAvailablePackages());
|
||||
@@ -139,7 +139,7 @@ public class InventoryCommandService {
|
||||
balance.setAvailablePackages(balanceAfter);
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(tenantId, operatorId, operation.getId(), balance, position, "ADJUST",
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, "ADJUST",
|
||||
"AVAILABLE", "AVAILABLE", before, request.packageDelta(), after,
|
||||
request.businessOrderNo(), null, null, null, request.idempotencyKey(),
|
||||
request.reason() + remarkSuffix(request.remark()));
|
||||
@@ -147,10 +147,10 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
private InventoryCommandResponse move(String tenantId, String operatorId, String operationType,
|
||||
private InventoryCommandResponse move(String customerId, String operatorId, String operationType,
|
||||
Bucket from, Bucket to, InventoryCommandRequests.Move request) {
|
||||
return execute(tenantId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(tenantId, request.positionId());
|
||||
return execute(customerId, operatorId, operationType, request.idempotencyKey(), request, operation -> {
|
||||
Locked locked = locked(customerId, request.positionId());
|
||||
InventoryPosition position = locked.position();
|
||||
InventoryBalance balance = locked.balance();
|
||||
requireAtLeast(packages(position, from), request.packages(), from.name().toLowerCase());
|
||||
@@ -166,7 +166,7 @@ public class InventoryCommandService {
|
||||
packages(balance, to, packages(balance, to) + request.packages());
|
||||
touch(balance);
|
||||
balances.updateById(balance);
|
||||
ledger(tenantId, operatorId, operation.getId(), balance, position, operationType,
|
||||
ledger(customerId, operatorId, operation.getId(), balance, position, operationType,
|
||||
from.name(), to.name(), beforeTarget, request.packages(), beforeTarget + request.packages(),
|
||||
request.businessOrderNo(), null, request.outboundOrderNo(), request.sourceEventId(),
|
||||
request.idempotencyKey(), request.remark());
|
||||
@@ -174,13 +174,13 @@ public class InventoryCommandService {
|
||||
});
|
||||
}
|
||||
|
||||
private InventoryBalance balance(String tenantId, InventoryCommandRequests.StockIn request) {
|
||||
InventoryBalance balance = balances.lockDimension(tenantId, request.overseasWarehouseId(),
|
||||
private InventoryBalance balance(String customerId, InventoryCommandRequests.StockIn request) {
|
||||
InventoryBalance balance = balances.lockDimension(customerId, request.overseasWarehouseId(),
|
||||
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
|
||||
if (balance != null) return balance;
|
||||
balance = new InventoryBalance();
|
||||
balance.setId(IdWorker.getId());
|
||||
balance.setTenantId(tenantId);
|
||||
balance.setCustomerId(customerId);
|
||||
balance.setCustomerName(trim(request.customerName()));
|
||||
balance.setOverseasWarehouseId(request.overseasWarehouseId().trim());
|
||||
balance.setOverseasWarehouseCode(trim(request.overseasWarehouseCode()));
|
||||
@@ -199,21 +199,21 @@ public class InventoryCommandService {
|
||||
balances.insert(balance);
|
||||
return balance;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryBalance concurrent = balances.lockDimension(tenantId, request.overseasWarehouseId(),
|
||||
InventoryBalance concurrent = balances.lockDimension(customerId, request.overseasWarehouseId(),
|
||||
request.destinationWarehouseCode(), request.inboundOrderId(), request.inboundCode());
|
||||
if (concurrent != null) return concurrent;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private InventoryPosition position(String tenantId, InventoryBalance balance,
|
||||
private InventoryPosition position(String customerId, InventoryBalance balance,
|
||||
InventoryCommandRequests.StockIn request) {
|
||||
String batchNo = trim(request.batchNo()) == null ? "DEFAULT" : request.batchNo().trim();
|
||||
InventoryPosition position = positions.lockPosition(tenantId, balance.getId(), request.palletId(), batchNo);
|
||||
InventoryPosition position = positions.lockPosition(customerId, balance.getId(), request.palletId(), batchNo);
|
||||
if (position != null) return position;
|
||||
position = new InventoryPosition();
|
||||
position.setId(IdWorker.getId());
|
||||
position.setTenantId(tenantId);
|
||||
position.setCustomerId(customerId);
|
||||
position.setBalanceId(balance.getId());
|
||||
position.setOverseasWarehouseId(balance.getOverseasWarehouseId());
|
||||
position.setDestinationWarehouseCode(balance.getDestinationWarehouseCode());
|
||||
@@ -237,30 +237,30 @@ public class InventoryCommandService {
|
||||
positions.insert(position);
|
||||
return position;
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryPosition concurrent = positions.lockPosition(tenantId, balance.getId(), request.palletId(), batchNo);
|
||||
InventoryPosition concurrent = positions.lockPosition(customerId, balance.getId(), request.palletId(), batchNo);
|
||||
if (concurrent != null) return concurrent;
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
private Locked locked(String tenantId, Long positionId) {
|
||||
InventoryPosition position = commands.lockPosition(tenantId, positionId);
|
||||
private Locked locked(String customerId, Long positionId) {
|
||||
InventoryPosition position = commands.lockPosition(customerId, positionId);
|
||||
if (position == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory position not found");
|
||||
InventoryBalance balance = commands.lockBalance(tenantId, position.getBalanceId());
|
||||
InventoryBalance balance = commands.lockBalance(customerId, position.getBalanceId());
|
||||
if (balance == null) throw new IllegalStateException("Inventory position balance is missing");
|
||||
return new Locked(balance, position);
|
||||
}
|
||||
|
||||
private InventoryCommandResponse execute(String tenantId, String operatorId, String operationType,
|
||||
private InventoryCommandResponse execute(String customerId, String operatorId, String operationType,
|
||||
String key, Object request, Work work) {
|
||||
String normalizedKey = required(key, "Idempotency key is required");
|
||||
String requestHash = hash(request);
|
||||
InventoryOperation existing = operations.findIdempotency(tenantId, normalizedKey);
|
||||
InventoryOperation existing = operations.findIdempotency(customerId, normalizedKey);
|
||||
if (existing != null) return replay(existing, operationType, requestHash);
|
||||
|
||||
InventoryOperation operation = new InventoryOperation();
|
||||
operation.setId(IdWorker.getId());
|
||||
operation.setTenantId(tenantId);
|
||||
operation.setCustomerId(customerId);
|
||||
operation.setIdempotencyKey(normalizedKey);
|
||||
operation.setOperationType(operationType);
|
||||
operation.setRequestHash(requestHash);
|
||||
@@ -270,7 +270,7 @@ public class InventoryCommandService {
|
||||
try {
|
||||
operations.insert(operation);
|
||||
} catch (DuplicateKeyException exception) {
|
||||
InventoryOperation concurrent = operations.findIdempotency(tenantId, normalizedKey);
|
||||
InventoryOperation concurrent = operations.findIdempotency(customerId, normalizedKey);
|
||||
if (concurrent != null) return replay(concurrent, operationType, requestHash);
|
||||
throw exception;
|
||||
}
|
||||
@@ -297,14 +297,14 @@ public class InventoryCommandService {
|
||||
}
|
||||
}
|
||||
|
||||
private void ledger(String tenantId, String operatorId, Long operationId, InventoryBalance balance,
|
||||
private void ledger(String customerId, String operatorId, Long operationId, InventoryBalance balance,
|
||||
InventoryPosition position, String businessType, String from, String to,
|
||||
long beforePackages, long deltaPackages, long afterPackages,
|
||||
String businessOrderNo, String inboundOrderNo, String outboundOrderNo,
|
||||
String sourceEventId, String idempotencyKey, String remark) {
|
||||
InventoryLedger ledger = new InventoryLedger();
|
||||
ledger.setId(IdWorker.getId());
|
||||
ledger.setTenantId(tenantId);
|
||||
ledger.setCustomerId(customerId);
|
||||
ledger.setBalanceId(balance.getId());
|
||||
ledger.setPositionId(position.getId());
|
||||
ledger.setOverseasWarehouseId(balance.getOverseasWarehouseId());
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InventoryBalance {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String customerName;
|
||||
private String overseasWarehouseId;
|
||||
private String overseasWarehouseCode;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InventoryLedger {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long balanceId;
|
||||
private Long positionId;
|
||||
private String overseasWarehouseId;
|
||||
@@ -22,6 +22,9 @@ public class InventoryLedger {
|
||||
private Long palletId;
|
||||
private String palletNo;
|
||||
private String batchNo;
|
||||
private String skuCode;
|
||||
private String cartonMark;
|
||||
private Long cartonPackages;
|
||||
private String warehouseLocationCode;
|
||||
private String businessType;
|
||||
private Long businessOrderId;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InventoryOperation {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String idempotencyKey;
|
||||
private String operationType;
|
||||
private String requestHash;
|
||||
|
||||
@@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
public class InventoryPosition {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long balanceId;
|
||||
private String overseasWarehouseId;
|
||||
private String destinationWarehouseCode;
|
||||
|
||||
+8
-8
@@ -9,32 +9,32 @@ import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} " +
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{overseasWarehouseId} " +
|
||||
"AND destination_warehouse_code=#{destinationWarehouseCode} " +
|
||||
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1")
|
||||
InventoryBalance findDimension(@Param("tenantId") String tenantId,
|
||||
InventoryBalance findDimension(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId,
|
||||
@Param("destinationWarehouseCode") String destinationWarehouseCode,
|
||||
@Param("inboundOrderId") Long inboundOrderId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} " +
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} " +
|
||||
"AND overseas_warehouse_id=#{overseasWarehouseId} " +
|
||||
"AND destination_warehouse_code=#{destinationWarehouseCode} " +
|
||||
"AND inbound_order_id=#{inboundOrderId} AND inbound_code=#{inboundCode} LIMIT 1 FOR UPDATE")
|
||||
InventoryBalance lockDimension(@Param("tenantId") String tenantId,
|
||||
InventoryBalance lockDimension(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId,
|
||||
@Param("destinationWarehouseCode") String destinationWarehouseCode,
|
||||
@Param("inboundOrderId") Long inboundOrderId,
|
||||
@Param("inboundCode") String inboundCode);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId}" +
|
||||
@Select("<script>SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId}" +
|
||||
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" +
|
||||
"<if test='destinationWarehouseCode != null and destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{destinationWarehouseCode}</if>" +
|
||||
"<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" +
|
||||
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryBalance> listTenant(@Param("tenantId") String tenantId,
|
||||
List<InventoryBalance> listTenant(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId,
|
||||
@Param("destinationWarehouseCode") String destinationWarehouseCode,
|
||||
@Param("inboundCode") String inboundCode,
|
||||
@@ -45,8 +45,8 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
||||
"COALESCE(SUM(available_packages),0) available_packages,COALESCE(SUM(hold_packages),0) hold_packages," +
|
||||
"COALESCE(SUM(allocated_packages),0) allocated_packages,COALESCE(SUM(picked_packages),0) picked_packages," +
|
||||
"COALESCE(SUM(in_transit_packages),0) in_transit_packages,COALESCE(SUM(receiving_packages),0) receiving_packages," +
|
||||
"COALESCE(SUM(procurement_packages),0) procurement_packages FROM ll_inventory_balance WHERE tenant_id=#{tenantId}" +
|
||||
"COALESCE(SUM(procurement_packages),0) procurement_packages FROM ll_inventory_balance WHERE customer_id=#{customerId}" +
|
||||
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>")
|
||||
InventorySummary summarize(@Param("tenantId") String tenantId,
|
||||
InventorySummary summarize(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId);
|
||||
}
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@ import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public interface InventoryLedgerMapper extends BaseMapper<InventoryLedger> {
|
||||
@Select("SELECT * FROM ll_inventory_ledger WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
|
||||
InventoryLedger findIdempotency(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_inventory_ledger WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
|
||||
InventoryLedger findIdempotency(@Param("customerId") String customerId,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_ledger WHERE tenant_id=#{tenantId}" +
|
||||
@Select("<script>SELECT * FROM ll_inventory_ledger WHERE customer_id=#{customerId}" +
|
||||
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if>" +
|
||||
"<if test='inboundCode != null and inboundCode != \"\"'> AND inbound_code=#{inboundCode}</if>" +
|
||||
"<if test='businessType != null and businessType != \"\"'> AND business_type=#{businessType}</if>" +
|
||||
@@ -21,7 +21,7 @@ public interface InventoryLedgerMapper extends BaseMapper<InventoryLedger> {
|
||||
"<if test='startedAt != null'> AND occurred_at >=#{startedAt}</if>" +
|
||||
"<if test='endedAt != null'> AND occurred_at <#{endedAt}</if>" +
|
||||
" ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryLedger> listTenant(@Param("tenantId") String tenantId,
|
||||
List<InventoryLedger> listTenant(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId,
|
||||
@Param("inboundCode") String inboundCode,
|
||||
@Param("businessType") String businessType,
|
||||
|
||||
+4
-4
@@ -6,11 +6,11 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
public interface InventoryOperationMapper extends BaseMapper<InventoryOperation> {
|
||||
@Select("SELECT * FROM ll_inventory_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
|
||||
InventoryOperation findIdempotency(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_inventory_operation WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1")
|
||||
InventoryOperation findIdempotency(@Param("customerId") String customerId,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_operation WHERE tenant_id=#{tenantId} AND idempotency_key=#{idempotencyKey} LIMIT 1 FOR UPDATE")
|
||||
InventoryOperation lockIdempotency(@Param("tenantId") String tenantId,
|
||||
@Select("SELECT * FROM ll_inventory_operation WHERE customer_id=#{customerId} AND idempotency_key=#{idempotencyKey} LIMIT 1 FOR UPDATE")
|
||||
InventoryOperation lockIdempotency(@Param("customerId") String customerId,
|
||||
@Param("idempotencyKey") String idempotencyKey);
|
||||
}
|
||||
|
||||
+6
-6
@@ -9,18 +9,18 @@ import java.util.List;
|
||||
|
||||
public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
|
||||
@Select("SELECT p.* FROM ll_inventory_position p JOIN ll_inventory_balance b ON b.id=p.balance_id " +
|
||||
"WHERE b.tenant_id=#{tenantId} AND p.balance_id=#{balanceId} ORDER BY p.pallet_no,p.batch_no")
|
||||
List<InventoryPosition> listBalance(@Param("tenantId") String tenantId,
|
||||
"WHERE b.customer_id=#{customerId} AND p.balance_id=#{balanceId} ORDER BY p.pallet_no,p.batch_no")
|
||||
List<InventoryPosition> listBalance(@Param("customerId") String customerId,
|
||||
@Param("balanceId") Long balanceId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND pallet_id=#{palletId} " +
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND pallet_id=#{palletId} " +
|
||||
"ORDER BY balance_id,batch_no")
|
||||
List<InventoryPosition> listPallet(@Param("tenantId") String tenantId,
|
||||
List<InventoryPosition> listPallet(@Param("customerId") String customerId,
|
||||
@Param("palletId") Long palletId);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND balance_id=#{balanceId} " +
|
||||
@Select("SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId} " +
|
||||
"AND pallet_id=#{palletId} AND batch_no=#{batchNo} LIMIT 1 FOR UPDATE")
|
||||
InventoryPosition lockPosition(@Param("tenantId") String tenantId,
|
||||
InventoryPosition lockPosition(@Param("customerId") String customerId,
|
||||
@Param("balanceId") Long balanceId,
|
||||
@Param("palletId") Long palletId,
|
||||
@Param("batchNo") String batchNo);
|
||||
|
||||
+16
-16
@@ -38,55 +38,55 @@ public class InventoryQueryController {
|
||||
@Operation(summary = "Get inbound-order package inventory totals")
|
||||
public ApiResponse<InventoryDashboardResponse> summary(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return ApiResponse.success(service.dashboard(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.dashboard(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances")
|
||||
@Operation(summary = "Page inventory by customer, warehouse and inbound order")
|
||||
public ApiResponse<PageResult<InventoryBalanceView>> balances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return ApiResponse.success(service.balances(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.balances(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/balances/{balanceId}/positions")
|
||||
@Operation(summary = "List pallet and batch positions for one inbound-order balance")
|
||||
public ApiResponse<List<InventoryPositionView>> positions(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@PathVariable long balanceId,
|
||||
@Valid InventoryQueryRequests.PositionQuery query) {
|
||||
return ApiResponse.success(service.positions(scope(user, tenantId), balanceId, query));
|
||||
return ApiResponse.success(service.positions(scope(user, customerId), balanceId, query));
|
||||
}
|
||||
|
||||
@GetMapping(BASE_PATH + "/ledger")
|
||||
@Operation(summary = "Page append-only inventory movements")
|
||||
public ApiResponse<PageResult<InventoryLedgerView>> ledgers(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return ApiResponse.success(service.ledgers(scope(user, tenantId), query));
|
||||
return ApiResponse.success(service.ledgers(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/balances/export", produces = "text/csv")
|
||||
@Operation(summary = "Export filtered inbound-order inventory as UTF-8 CSV")
|
||||
public ResponseEntity<byte[]> exportBalances(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.BalanceQuery query) {
|
||||
return csv(service.exportBalances(scope(user, tenantId), query), "inventory-balances.csv");
|
||||
return csv(service.exportBalances(scope(user, customerId), query), "inventory-balances.csv");
|
||||
}
|
||||
|
||||
@GetMapping(value = BASE_PATH + "/ledger/export", produces = "text/csv")
|
||||
@Operation(summary = "Export filtered inventory movements as UTF-8 CSV")
|
||||
public ResponseEntity<byte[]> exportLedgers(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InventoryQueryRequests.LedgerQuery query) {
|
||||
return csv(service.exportLedgers(scope(user, tenantId), query), "inventory-ledger.csv");
|
||||
return csv(service.exportLedgers(scope(user, customerId), query), "inventory-ledger.csv");
|
||||
}
|
||||
|
||||
private static ResponseEntity<byte[]> csv(byte[] body, String filename) {
|
||||
@@ -96,16 +96,16 @@ public class InventoryQueryController {
|
||||
.body(body);
|
||||
}
|
||||
|
||||
private String scope(AuthenticatedUser user, String requestedTenant) {
|
||||
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
|
||||
if (requestedTenant == null || requestedTenant.isBlank() || "0".equals(requestedTenant)) {
|
||||
if (requestedCustomer == null || requestedCustomer.isBlank() || "0".equals(requestedCustomer)) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Platform queries must specify a customer tenant");
|
||||
"Platform queries must specify a customer");
|
||||
}
|
||||
return requestedTenant;
|
||||
return requestedCustomer;
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
+15
-15
@@ -11,14 +11,14 @@ import org.apache.ibatis.annotations.Select;
|
||||
import java.util.List;
|
||||
|
||||
public interface InventoryQueryMapper {
|
||||
String BALANCE_FILTER = " WHERE tenant_id=#{tenantId}" +
|
||||
String BALANCE_FILTER = " WHERE customer_id=#{customerId}" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.tenant_id=#{tenantId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<choose>" +
|
||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages > 0</when>" +
|
||||
@@ -29,14 +29,14 @@ public interface InventoryQueryMapper {
|
||||
"<when test='q.inventoryStatus == \"PROCUREMENT\"'> AND procurement_packages > 0</when>" +
|
||||
"</choose>";
|
||||
|
||||
String JOINED_BALANCE_FILTER = " WHERE b.tenant_id=#{tenantId}" +
|
||||
String JOINED_BALANCE_FILTER = " WHERE b.customer_id=#{customerId}" +
|
||||
"<if test='q.customerName != null and q.customerName != \"\"'> AND b.customer_name LIKE CONCAT('%',#{q.customerName},'%')</if>" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND b.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.overseasWarehouseCode != null and q.overseasWarehouseCode != \"\"'> AND b.overseas_warehouse_code=#{q.overseasWarehouseCode}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND b.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
|
||||
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.tenant_id=#{tenantId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
|
||||
"<choose>" +
|
||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages > 0</when>" +
|
||||
@@ -49,13 +49,13 @@ public interface InventoryQueryMapper {
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_balance" + BALANCE_FILTER +
|
||||
" ORDER BY updated_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryBalance> balances(@Param("tenantId") String tenantId,
|
||||
List<InventoryBalance> balances(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_inventory_balance" + BALANCE_FILTER + "</script>")
|
||||
long countBalances(@Param("tenantId") String tenantId,
|
||||
long countBalances(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) dimension_count," +
|
||||
@@ -67,16 +67,16 @@ public interface InventoryQueryMapper {
|
||||
"COALESCE(SUM(receiving_packages),0) receiving_packages," +
|
||||
"COALESCE(SUM(procurement_packages),0) procurement_packages " +
|
||||
"FROM ll_inventory_balance" + BALANCE_FILTER + "</script>")
|
||||
InventoryAggregateProjection aggregate(@Param("tenantId") String tenantId,
|
||||
InventoryAggregateProjection aggregate(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query);
|
||||
|
||||
@Select("<script>SELECT COUNT(DISTINCT p.pallet_id) FROM ll_inventory_position p " +
|
||||
"JOIN ll_inventory_balance b ON b.id=p.balance_id" +
|
||||
JOINED_BALANCE_FILTER + "</script>")
|
||||
long countPallets(@Param("tenantId") String tenantId,
|
||||
long countPallets(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.BalanceQuery query);
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_position WHERE tenant_id=#{tenantId} AND balance_id=#{balanceId}" +
|
||||
@Select("<script>SELECT * FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id=#{balanceId}" +
|
||||
"<if test='q.palletNo != null and q.palletNo != \"\"'> AND pallet_no LIKE CONCAT('%',#{q.palletNo},'%')</if>" +
|
||||
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND batch_no LIKE CONCAT('%',#{q.batchNo},'%')</if>" +
|
||||
"<choose>" +
|
||||
@@ -86,14 +86,14 @@ public interface InventoryQueryMapper {
|
||||
"<when test='q.inventoryStatus == \"PICKED\"'> AND picked_packages > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"RECEIVING\"'> AND receiving_packages > 0</when>" +
|
||||
"</choose> ORDER BY pallet_no,batch_no,id</script>")
|
||||
List<InventoryPosition> positions(@Param("tenantId") String tenantId,
|
||||
List<InventoryPosition> positions(@Param("customerId") String customerId,
|
||||
@Param("balanceId") long balanceId,
|
||||
@Param("q") InventoryQueryRequests.PositionQuery query);
|
||||
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE tenant_id=#{tenantId} AND id=#{balanceId} LIMIT 1")
|
||||
InventoryBalance ownedBalance(@Param("tenantId") String tenantId, @Param("balanceId") long balanceId);
|
||||
@Select("SELECT * FROM ll_inventory_balance WHERE customer_id=#{customerId} AND id=#{balanceId} LIMIT 1")
|
||||
InventoryBalance ownedBalance(@Param("customerId") String customerId, @Param("balanceId") long balanceId);
|
||||
|
||||
String LEDGER_FILTER = " WHERE tenant_id=#{tenantId}" +
|
||||
String LEDGER_FILTER = " WHERE customer_id=#{customerId}" +
|
||||
"<if test='q.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.businessType != null and q.businessType != \"\"'> AND business_type=#{q.businessType}</if>" +
|
||||
@@ -108,12 +108,12 @@ public interface InventoryQueryMapper {
|
||||
|
||||
@Select("<script>SELECT * FROM ll_inventory_ledger" + LEDGER_FILTER +
|
||||
" ORDER BY occurred_at DESC,id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InventoryLedger> ledgers(@Param("tenantId") String tenantId,
|
||||
List<InventoryLedger> ledgers(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.LedgerQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM ll_inventory_ledger" + LEDGER_FILTER + "</script>")
|
||||
long countLedgers(@Param("tenantId") String tenantId,
|
||||
long countLedgers(@Param("customerId") String customerId,
|
||||
@Param("q") InventoryQueryRequests.LedgerQuery query);
|
||||
}
|
||||
|
||||
+19
-19
@@ -31,9 +31,9 @@ public class InventoryQueryService {
|
||||
|
||||
@Resource private InventoryQueryMapper mapper;
|
||||
|
||||
public InventoryDashboardResponse dashboard(String tenantId, InventoryQueryRequests.BalanceQuery query) {
|
||||
public InventoryDashboardResponse dashboard(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||
InventoryAggregateProjection value = mapper.aggregate(tenantId, query);
|
||||
InventoryAggregateProjection value = mapper.aggregate(customerId, query);
|
||||
long availablePackages = number(value.getAvailablePackages());
|
||||
long holdPackages = number(value.getHoldPackages());
|
||||
long allocatedPackages = number(value.getAllocatedPackages());
|
||||
@@ -41,46 +41,46 @@ public class InventoryQueryService {
|
||||
long inTransitPackages = number(value.getInTransitPackages());
|
||||
long receivingPackages = number(value.getReceivingPackages());
|
||||
long procurementPackages = number(value.getProcurementPackages());
|
||||
return new InventoryDashboardResponse(number(value.getDimensionCount()), mapper.countPallets(tenantId, query),
|
||||
return new InventoryDashboardResponse(number(value.getDimensionCount()), mapper.countPallets(customerId, query),
|
||||
availablePackages + holdPackages + allocatedPackages + pickedPackages + inTransitPackages
|
||||
+ receivingPackages + procurementPackages,
|
||||
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
|
||||
receivingPackages, procurementPackages);
|
||||
}
|
||||
|
||||
public PageResult<InventoryBalanceView> balances(String tenantId, InventoryQueryRequests.BalanceQuery query) {
|
||||
public PageResult<InventoryBalanceView> balances(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||
long total = mapper.countBalances(tenantId, query);
|
||||
long total = mapper.countBalances(customerId, query);
|
||||
List<InventoryBalanceView> records = total == 0 ? List.of() : mapper.balances(
|
||||
tenantId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList();
|
||||
customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList();
|
||||
return page(records, query.page(), query.size(), total);
|
||||
}
|
||||
|
||||
public List<InventoryPositionView> positions(String tenantId, long balanceId,
|
||||
public List<InventoryPositionView> positions(String customerId, long balanceId,
|
||||
InventoryQueryRequests.PositionQuery query) {
|
||||
validateStatus(query.inventoryStatus(), POSITION_STATUSES);
|
||||
InventoryBalance balance = mapper.ownedBalance(tenantId, balanceId);
|
||||
InventoryBalance balance = mapper.ownedBalance(customerId, balanceId);
|
||||
if (balance == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Inventory balance not found");
|
||||
}
|
||||
return mapper.positions(tenantId, balanceId, query).stream().map(value -> positionView(value, balance)).toList();
|
||||
return mapper.positions(customerId, balanceId, query).stream().map(value -> positionView(value, balance)).toList();
|
||||
}
|
||||
|
||||
public PageResult<InventoryLedgerView> ledgers(String tenantId, InventoryQueryRequests.LedgerQuery query) {
|
||||
public PageResult<InventoryLedgerView> ledgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
|
||||
validateDateRange(query);
|
||||
long total = mapper.countLedgers(tenantId, query);
|
||||
long total = mapper.countLedgers(customerId, query);
|
||||
List<InventoryLedgerView> records = total == 0 ? List.of() : mapper.ledgers(
|
||||
tenantId, query, offset(query.page(), query.size()), query.size()).stream().map(this::ledgerView).toList();
|
||||
customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::ledgerView).toList();
|
||||
return page(records, query.page(), query.size(), total);
|
||||
}
|
||||
|
||||
public byte[] exportBalances(String tenantId, InventoryQueryRequests.BalanceQuery query) {
|
||||
public byte[] exportBalances(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||
List<String> rows = new ArrayList<>();
|
||||
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
|
||||
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,totalPackages,availablePackages," +
|
||||
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages");
|
||||
mapper.balances(tenantId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
|
||||
mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
|
||||
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
|
||||
csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()),
|
||||
csv(v.inboundOrderNo()), csv(v.inboundCode()),
|
||||
@@ -90,13 +90,13 @@ public class InventoryQueryService {
|
||||
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public byte[] exportLedgers(String tenantId, InventoryQueryRequests.LedgerQuery query) {
|
||||
public byte[] exportLedgers(String customerId, InventoryQueryRequests.LedgerQuery query) {
|
||||
validateDateRange(query);
|
||||
List<String> rows = new ArrayList<>();
|
||||
rows.add("occurredAt,customerId,overseasWarehouseId,destinationWarehouseCode,inboundOrderId,inboundOrderNo," +
|
||||
"inboundCode,businessType,businessOrderNo,outboundOrderNo,palletNo,batchNo,statusFrom,statusTo," +
|
||||
"beforePackages,deltaPackages,afterPackages,operatorId,remark");
|
||||
mapper.ledgers(tenantId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add(
|
||||
mapper.ledgers(customerId, query, 0, EXPORT_LIMIT).stream().map(this::ledgerView).forEach(v -> rows.add(
|
||||
String.join(",", csv(v.occurredAt()), csv(v.customerId()), csv(v.overseasWarehouseId()),
|
||||
csv(v.destinationWarehouseCode()), csv(v.inboundOrderId()), csv(v.inboundOrderNo()), csv(v.inboundCode()),
|
||||
csv(v.businessType()), csv(v.businessOrderNo()), csv(v.outboundOrderNo()), csv(v.palletNo()), csv(v.batchNo()),
|
||||
@@ -113,7 +113,7 @@ public class InventoryQueryService {
|
||||
long inTransitPackages = number(value.getInTransitPackages());
|
||||
long receivingPackages = number(value.getReceivingPackages());
|
||||
long procurementPackages = number(value.getProcurementPackages());
|
||||
return new InventoryBalanceView(value.getId(), value.getTenantId(), value.getCustomerName(), value.getOverseasWarehouseId(),
|
||||
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
|
||||
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
|
||||
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
|
||||
value.getInboundOrderNo(), value.getInboundCode(), availablePackages + holdPackages + allocatedPackages + pickedPackages
|
||||
@@ -128,7 +128,7 @@ public class InventoryQueryService {
|
||||
long allocated = number(value.getAllocatedPackages());
|
||||
long picked = number(value.getPickedPackages());
|
||||
long receiving = number(value.getReceivingPackages());
|
||||
return new InventoryPositionView(value.getId(), value.getBalanceId(), value.getTenantId(),
|
||||
return new InventoryPositionView(value.getId(), value.getBalanceId(), value.getCustomerId(),
|
||||
value.getOverseasWarehouseId(), value.getDestinationWarehouseCode(), balance.getInboundOrderId(),
|
||||
balance.getInboundOrderNo(), balance.getInboundCode(),
|
||||
value.getPalletId(), value.getPalletNo(), value.getBatchNo(), value.getWarehouseLocationId(),
|
||||
@@ -138,7 +138,7 @@ public class InventoryQueryService {
|
||||
}
|
||||
|
||||
private InventoryLedgerView ledgerView(InventoryLedger value) {
|
||||
return new InventoryLedgerView(value.getId(), value.getTenantId(), value.getOverseasWarehouseId(),
|
||||
return new InventoryLedgerView(value.getId(), value.getCustomerId(), value.getOverseasWarehouseId(),
|
||||
value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(),
|
||||
value.getPalletNo(), value.getBatchNo(),
|
||||
value.getWarehouseLocationCode(), value.getBusinessType(), value.getBusinessOrderNo(),
|
||||
|
||||
@@ -9,30 +9,30 @@ import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
@Select("SELECT * FROM ll_order WHERE tenant_id=#{tenantId} AND source_region=#{sourceRegion} " +
|
||||
@Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND source_region=#{sourceRegion} " +
|
||||
"AND source_inbound_id=#{sourceInboundId} LIMIT 1")
|
||||
LastlegOrder findSource(@Param("tenantId") String tenantId,
|
||||
LastlegOrder findSource(@Param("customerId") String customerId,
|
||||
@Param("sourceRegion") String sourceRegion,
|
||||
@Param("sourceInboundId") String sourceInboundId);
|
||||
|
||||
@Select("SELECT COUNT(*) FROM ll_order WHERE tenant_id=#{tenantId} AND container_no=#{containerNo}")
|
||||
long countContainer(@Param("tenantId") String tenantId, @Param("containerNo") String containerNo);
|
||||
@Select("SELECT COUNT(*) FROM ll_order WHERE customer_id=#{customerId} AND container_no=#{containerNo}")
|
||||
long countContainer(@Param("customerId") String customerId, @Param("containerNo") String containerNo);
|
||||
|
||||
@Select("SELECT * FROM ll_order WHERE tenant_id=#{tenantId} AND id=#{id} FOR UPDATE")
|
||||
LastlegOrder lockById(@Param("tenantId") String tenantId, @Param("id") Long id);
|
||||
@Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND id=#{id} FOR UPDATE")
|
||||
LastlegOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
||||
|
||||
@Update("UPDATE ll_order SET picked_package_count=picked_package_count+#{packages},updated_at=NOW() " +
|
||||
"WHERE tenant_id=#{tenantId} AND id=#{id} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id} " +
|
||||
"AND package_count-shipped_package_count-picked_package_count >= #{packages}")
|
||||
int increasePicked(@Param("tenantId") String tenantId, @Param("id") Long id,
|
||||
int increasePicked(@Param("customerId") String customerId, @Param("id") Long id,
|
||||
@Param("packages") long packages);
|
||||
|
||||
@Update("UPDATE ll_order SET picked_package_count=picked_package_count-#{packages}," +
|
||||
"shipped_package_count=shipped_package_count+#{packages},outbound_order_no=#{outboundNo}," +
|
||||
"current_status=CASE WHEN shipped_package_count+#{packages}=package_count " +
|
||||
"THEN 'COMPLETED' ELSE 'PARTIALLY_SHIPPED' END,updated_at=NOW() " +
|
||||
"WHERE tenant_id=#{tenantId} AND id=#{id} AND picked_package_count >= #{packages} " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{id} AND picked_package_count >= #{packages} " +
|
||||
"AND shipped_package_count+#{packages} <= package_count")
|
||||
int transferPickedToShipped(@Param("tenantId") String tenantId, @Param("id") Long id,
|
||||
int transferPickedToShipped(@Param("customerId") String customerId, @Param("id") Long id,
|
||||
@Param("packages") long packages, @Param("outboundNo") String outboundNo);
|
||||
}
|
||||
|
||||
+8
-8
@@ -20,24 +20,24 @@ public class RelabelApplicationController {
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId, @Valid @RequestBody RelabelRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user, tenantId), operator(user), request));
|
||||
@RequestParam(required = false) String customerId, @Valid @RequestBody RelabelRequests.Create request) {
|
||||
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
||||
}
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<RelabelResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId, @Valid RelabelRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, tenantId), query));
|
||||
@RequestParam(required = false) String customerId, @Valid RelabelRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
@GetMapping("/{relabelNo}")
|
||||
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId, @PathVariable String relabelNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, tenantId), relabelNo));
|
||||
@RequestParam(required = false) String customerId, @PathVariable String relabelNo) {
|
||||
return ApiResponse.success(service.detail(scope(user, customerId), relabelNo));
|
||||
}
|
||||
@PutMapping("/{relabelNo}/status")
|
||||
public ApiResponse<RelabelResponses.Detail> status(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String tenantId, @PathVariable String relabelNo,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String relabelNo,
|
||||
@Valid @RequestBody RelabelRequests.ChangeStatus request) {
|
||||
return ApiResponse.success(service.changeStatus(scope(user, tenantId), relabelNo, operator(user), request));
|
||||
return ApiResponse.success(service.changeStatus(scope(user, customerId), relabelNo, operator(user), request));
|
||||
}
|
||||
private String scope(AuthenticatedUser user, String requested) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_relabel_application")
|
||||
public class RelabelApplication {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String relabelNo;
|
||||
private Long lastlegOrderId;
|
||||
private String customerOrderNo;
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
public class RelabelItem {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long applicationId;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private String skuCode;
|
||||
private String productImageFileId;
|
||||
private String productImageFileName;
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.time.LocalDateTime;
|
||||
@TableName("ll_relabel_status_history")
|
||||
public class RelabelStatusHistory {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String tenantId;
|
||||
private String customerId;
|
||||
private Long applicationId;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
|
||||
+6
-6
@@ -8,12 +8,12 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface RelabelApplicationMapper extends BaseMapper<RelabelApplication> {
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND relabel_no=#{relabelNo} LIMIT 1")
|
||||
RelabelApplication findNo(@Param("tenantId") String tenantId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1")
|
||||
RelabelApplication findNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND idempotency_key=#{key} LIMIT 1")
|
||||
RelabelApplication findKey(@Param("tenantId") String tenantId, @Param("key") String key);
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
RelabelApplication findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE tenant_id=#{tenantId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockNo(@Param("tenantId") String tenantId, @Param("relabelNo") String relabelNo);
|
||||
@Select("SELECT * FROM ll_relabel_application WHERE customer_id=#{customerId} AND relabel_no=#{relabelNo} LIMIT 1 FOR UPDATE")
|
||||
RelabelApplication lockNo(@Param("customerId") String customerId, @Param("relabelNo") String relabelNo);
|
||||
}
|
||||
|
||||
+26
-26
@@ -49,7 +49,7 @@ public class RelabelApplicationService {
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail create(String tenantId, String operatorId, RelabelRequests.Create request) {
|
||||
public RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request) {
|
||||
int total = 0;
|
||||
int notified = 0;
|
||||
for (RelabelRequests.Item item : request.items()) {
|
||||
@@ -60,10 +60,10 @@ public class RelabelApplicationService {
|
||||
notified = Math.addExact(notified, item.notifiedPackageCount());
|
||||
}
|
||||
String requestHash = hash(request);
|
||||
RelabelApplication replay = applications.findKey(tenantId, request.idempotencyKey().trim());
|
||||
RelabelApplication replay = applications.findKey(customerId, request.idempotencyKey().trim());
|
||||
if (replay != null) {
|
||||
if (!requestHash.equals(replay.getRequestHash())) throw conflict("Idempotency key was reused with different relabel data");
|
||||
return detail(tenantId, replay.getRelabelNo());
|
||||
return detail(customerId, replay.getRelabelNo());
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long applicationId = IdWorker.getId();
|
||||
@@ -71,7 +71,7 @@ public class RelabelApplicationService {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setOrderNo(orderNoGenerator.next());
|
||||
order.setTenantId(tenantId);
|
||||
order.setCustomerId(customerId);
|
||||
order.setOrderType("RELABEL_APPLICATION");
|
||||
order.setInboundCode(relabelNo);
|
||||
order.setSourceRegion("CUSTOMER");
|
||||
@@ -95,7 +95,7 @@ public class RelabelApplicationService {
|
||||
|
||||
RelabelApplication application = new RelabelApplication();
|
||||
application.setId(applicationId);
|
||||
application.setTenantId(tenantId);
|
||||
application.setCustomerId(customerId);
|
||||
application.setRelabelNo(relabelNo);
|
||||
application.setLastlegOrderId(order.getId());
|
||||
application.setCustomerOrderNo(request.customerOrderNo().trim());
|
||||
@@ -115,19 +115,19 @@ public class RelabelApplicationService {
|
||||
application.setUpdatedAt(now);
|
||||
applications.insert(application);
|
||||
|
||||
for (RelabelRequests.Item source : request.items()) insertItem(tenantId, applicationId, source, now);
|
||||
insertHistory(tenantId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
for (RelabelRequests.Item source : request.items()) insertItem(customerId, applicationId, source, now);
|
||||
insertHistory(customerId, applicationId, null, PENDING_PROCESSING, operatorId, "Relabel application submitted", now);
|
||||
insertOrderHistory(order.getId(), null, PENDING_PROCESSING, "Relabel application submitted", now);
|
||||
return detail(tenantId, relabelNo);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<RelabelResponses.Summary> page(String tenantId, RelabelRequests.Query query) {
|
||||
public PageResult<RelabelResponses.Summary> page(String customerId, RelabelRequests.Query query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw badRequest("createdFrom must not be after createdTo");
|
||||
}
|
||||
LambdaQueryWrapper<RelabelApplication> wrapper = new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(RelabelApplication::getTenantId, tenantId)
|
||||
.eq(RelabelApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
|
||||
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
|
||||
.eq(StringUtils.hasText(query.status()), RelabelApplication::getStatus, query.status())
|
||||
@@ -135,7 +135,7 @@ public class RelabelApplicationService {
|
||||
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
|
||||
.apply(StringUtils.hasText(query.skuCode()),
|
||||
"EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.tenant_id=ll_relabel_application.tenant_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))",
|
||||
"EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))",
|
||||
query.skuCode())
|
||||
.orderByDesc(RelabelApplication::getCreatedAt);
|
||||
Page<RelabelApplication> page = applications.selectPage(new Page<>(query.page(), query.size()), wrapper);
|
||||
@@ -144,11 +144,11 @@ public class RelabelApplicationService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RelabelResponses.Detail detail(String tenantId, String relabelNo) {
|
||||
RelabelApplication application = require(tenantId, relabelNo);
|
||||
public RelabelResponses.Detail detail(String customerId, String relabelNo) {
|
||||
RelabelApplication application = require(customerId, relabelNo);
|
||||
List<RelabelItem> detailItems = itemList(application.getId());
|
||||
List<RelabelStatusHistory> statusHistory = histories.selectList(new LambdaQueryWrapper<RelabelStatusHistory>()
|
||||
.eq(RelabelStatusHistory::getTenantId, tenantId)
|
||||
.eq(RelabelStatusHistory::getCustomerId, customerId)
|
||||
.eq(RelabelStatusHistory::getApplicationId, application.getId())
|
||||
.orderByAsc(RelabelStatusHistory::getCreatedAt));
|
||||
return new RelabelResponses.Detail(summary(application, detailItems), application.getRemark(),
|
||||
@@ -158,13 +158,13 @@ public class RelabelApplicationService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail changeStatus(String tenantId, String relabelNo, String operatorId,
|
||||
public RelabelResponses.Detail changeStatus(String customerId, String relabelNo, String operatorId,
|
||||
RelabelRequests.ChangeStatus request) {
|
||||
RelabelApplication application = applications.lockNo(tenantId, relabelNo);
|
||||
RelabelApplication application = applications.lockNo(customerId, relabelNo);
|
||||
if (application == null) throw notFound("Relabel application not found");
|
||||
String next = request.status().trim().toUpperCase();
|
||||
if (!STATUSES.contains(next)) throw badRequest("Unsupported relabel status");
|
||||
if (next.equals(application.getStatus())) return detail(tenantId, relabelNo);
|
||||
if (next.equals(application.getStatus())) return detail(customerId, relabelNo);
|
||||
validateTransition(application.getStatus(), next);
|
||||
String before = application.getStatus();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -172,20 +172,20 @@ public class RelabelApplicationService {
|
||||
application.setUpdatedAt(now);
|
||||
applications.updateById(application);
|
||||
LastlegOrder order = orders.selectById(application.getLastlegOrderId());
|
||||
if (order == null || !tenantId.equals(order.getTenantId())) throw conflict("Linked tail-leg order is missing");
|
||||
if (order == null || !customerId.equals(order.getCustomerId())) throw conflict("Linked tail-leg order is missing");
|
||||
String orderBefore = order.getCurrentStatus();
|
||||
order.setCurrentStatus(next);
|
||||
order.setRelabelStatus(next);
|
||||
order.setUpdatedAt(now);
|
||||
orders.updateById(order);
|
||||
insertHistory(tenantId, application.getId(), before, next, operatorId, request.remark(), now);
|
||||
insertHistory(customerId, application.getId(), before, next, operatorId, request.remark(), now);
|
||||
insertOrderHistory(order.getId(), orderBefore, next, request.remark(), now);
|
||||
return detail(tenantId, relabelNo);
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
private void insertItem(String tenantId, long applicationId, RelabelRequests.Item source, LocalDateTime now) {
|
||||
private void insertItem(String customerId, long applicationId, RelabelRequests.Item source, LocalDateTime now) {
|
||||
RelabelItem item = new RelabelItem();
|
||||
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setTenantId(tenantId);
|
||||
item.setId(IdWorker.getId()); item.setApplicationId(applicationId); item.setCustomerId(customerId);
|
||||
item.setSkuCode(source.skuCode().trim()); item.setProductImageFileId(trim(source.productImageFileId()));
|
||||
item.setProductImageFileName(trim(source.productImageFileName())); item.setProductImageUrl(trim(source.productImageUrl()));
|
||||
item.setProductNameZh(source.productNameZh().trim()); item.setTotalPackageCount(source.totalPackageCount());
|
||||
@@ -219,14 +219,14 @@ public class RelabelApplicationService {
|
||||
return items.selectList(new LambdaQueryWrapper<RelabelItem>().eq(RelabelItem::getApplicationId, applicationId)
|
||||
.orderByAsc(RelabelItem::getId));
|
||||
}
|
||||
private RelabelApplication require(String tenantId, String relabelNo) {
|
||||
RelabelApplication value = applications.findNo(tenantId, relabelNo);
|
||||
private RelabelApplication require(String customerId, String relabelNo) {
|
||||
RelabelApplication value = applications.findNo(customerId, relabelNo);
|
||||
if (value == null) throw notFound("Relabel application not found");
|
||||
return value;
|
||||
}
|
||||
private void insertHistory(String tenantId, Long id, String from, String to, String operator,
|
||||
private void insertHistory(String customerId, Long id, String from, String to, String operator,
|
||||
String remark, LocalDateTime now) {
|
||||
RelabelStatusHistory value = new RelabelStatusHistory(); value.setId(IdWorker.getId()); value.setTenantId(tenantId);
|
||||
RelabelStatusHistory value = new RelabelStatusHistory(); value.setId(IdWorker.getId()); value.setCustomerId(customerId);
|
||||
value.setApplicationId(id); value.setFromStatus(from); value.setToStatus(to); value.setOperatorId(trim(operator));
|
||||
value.setRemark(trim(remark)); value.setCreatedAt(now); histories.insert(value);
|
||||
}
|
||||
|
||||
@@ -35,15 +35,15 @@ public class LastlegOrderService {
|
||||
@Resource private LastlegOrderDetailMapper detailMapper;
|
||||
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
||||
|
||||
public PageResult<LastlegOrderSummary> page(String tenantId, LastlegOrderRequests.Query query) {
|
||||
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
|
||||
Page<LastlegOrder> page = orderMapper.selectPage(
|
||||
new Page<>(query.page(), query.size()), filter(tenantId, query).orderByDesc(LastlegOrder::getCreatedAt));
|
||||
new Page<>(query.page(), query.size()), filter(customerId, query).orderByDesc(LastlegOrder::getCreatedAt));
|
||||
return new PageResult<>(page.getRecords().stream().map(this::summary).toList(),
|
||||
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
public LastlegOrderDetailResponse detail(String tenantId, long orderId) {
|
||||
LastlegOrder order = findOwned(tenantId, orderId);
|
||||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||||
LastlegOrder order = findOwned(customerId, orderId);
|
||||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
|
||||
List<LastlegOrderStatusHistory> history = historyMapper.selectList(
|
||||
@@ -54,13 +54,13 @@ public class LastlegOrderService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public int transferDestination(String tenantId, LastlegOrderRequests.TransferDestination request) {
|
||||
public int transferDestination(String customerId, LastlegOrderRequests.TransferDestination request) {
|
||||
List<Long> ids = request.orderIds().stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (ids.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
|
||||
}
|
||||
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getTenantId, tenantId).in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
.eq(LastlegOrder::getCustomerId, customerId).in(LastlegOrder::getId, ids).last("FOR UPDATE"));
|
||||
if (orders.size() != ids.size()) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more tail-leg orders were not found");
|
||||
}
|
||||
@@ -96,8 +96,8 @@ public class LastlegOrderService {
|
||||
return orders.size();
|
||||
}
|
||||
|
||||
public byte[] exportCsv(String tenantId, LastlegOrderRequests.Query query) {
|
||||
List<LastlegOrder> orders = orderMapper.selectList(filter(tenantId, query)
|
||||
public byte[] exportCsv(String customerId, LastlegOrderRequests.Query query) {
|
||||
List<LastlegOrder> orders = orderMapper.selectList(filter(customerId, query)
|
||||
.orderByDesc(LastlegOrder::getCreatedAt).last("LIMIT 10000"));
|
||||
List<String> lines = new ArrayList<>();
|
||||
lines.add("ID,尾程单号,订单类型,入仓码,头程单号,货主,海外仓,转仓,转仓编码,FBA仓库,集装箱号,货柜状态,是否异常,是否暂扣,托数,箱数,已拣箱数,已出库箱数,剩余箱数,体积(m3),数量,体积重量,预计到港时间,实际到港时间,预计到仓,入库单号,出库单号,派送单号,换标单号,换标状态,合托标记,当前状态,备注(仅自己),备注(客户),备注(仓库)");
|
||||
@@ -120,12 +120,12 @@ public class LastlegOrderService {
|
||||
return ("\uFEFF" + String.join("\r\n", lines)).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String tenantId, LastlegOrderRequests.Query query) {
|
||||
private LambdaQueryWrapper<LastlegOrder> filter(String customerId, LastlegOrderRequests.Query query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
|
||||
}
|
||||
return new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getTenantId, tenantId)
|
||||
.eq(LastlegOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), LastlegOrder::getOrderNo, query.orderNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), LastlegOrder::getInboundCode, query.inboundCode())
|
||||
.like(StringUtils.hasText(query.sourceOrderNo()), LastlegOrder::getSourceOrderNo, query.sourceOrderNo())
|
||||
@@ -139,9 +139,9 @@ public class LastlegOrderService {
|
||||
.le(query.createdTo() != null, LastlegOrder::getCreatedAt, query.createdTo());
|
||||
}
|
||||
|
||||
private LastlegOrder findOwned(String tenantId, long orderId) {
|
||||
private LastlegOrder findOwned(String customerId, long orderId) {
|
||||
LastlegOrder order = orderMapper.selectOne(new LambdaQueryWrapper<LastlegOrder>()
|
||||
.eq(LastlegOrder::getTenantId, tenantId).eq(LastlegOrder::getId, orderId));
|
||||
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, orderId));
|
||||
if (order == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Tail-leg order not found");
|
||||
return order;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,10 @@ package com.baiyue.lastleg.service;
|
||||
import com.baiyue.common.event.*;
|
||||
import com.baiyue.lastleg.entity.*;
|
||||
import com.baiyue.lastleg.mapper.*;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -12,6 +15,9 @@ import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
@Service
|
||||
public class LoadingCompletedConsumer {
|
||||
@@ -21,6 +27,7 @@ public class LoadingCompletedConsumer {
|
||||
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Resource private LastlegOrderNoGenerator orderNoGenerator;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
|
||||
@Transactional
|
||||
public int consume(EventEnvelope envelope) {
|
||||
@@ -42,7 +49,7 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
|
||||
LastlegEventInbox inbox=new LastlegEventInbox();
|
||||
inbox.setEventId(envelope.eventId()); inbox.setTenantId(envelope.tenantId()); inbox.setEventType(envelope.eventType());
|
||||
inbox.setEventId(envelope.eventId()); inbox.setCustomerId(envelope.tenantId()); inbox.setEventType(envelope.eventType());
|
||||
inbox.setEventVersion(envelope.eventVersion()); inbox.setPayloadHash(envelope.payloadHash());
|
||||
inbox.setPayloadJson(envelope.payload().toString()); inbox.setStatus("RECEIVED"); inbox.setReceivedAt(LocalDateTime.now());
|
||||
try { inboxMapper.insert(inbox); } catch(DuplicateKeyException exception) {
|
||||
@@ -50,13 +57,22 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
|
||||
String orderNo = orderNoGenerator.forContainerNo(payload.containerNo());
|
||||
for(var source:payload.orders()) createOrder(envelope,payload,source,orderNo);
|
||||
JsonNode rawOrders = envelope.payload().path("orders");
|
||||
for (int index = 0; index < payload.orders().size(); index++) {
|
||||
var source = payload.orders().get(index);
|
||||
JsonNode rawSource = rawOrders.isArray() && index < rawOrders.size() ? rawOrders.get(index) : objectMapper.createObjectNode();
|
||||
createOrder(envelope, payload, source, orderNo, rawSource);
|
||||
}
|
||||
inbox.setStatus("PROCESSED"); inbox.setProcessedAt(LocalDateTime.now()); inboxMapper.updateById(inbox);
|
||||
return payload.orders().size();
|
||||
}
|
||||
|
||||
private FirstLegLoadingPlanCompletedV1 parsePayload(EventEnvelope envelope) {
|
||||
try { return objectMapper.treeToValue(envelope.payload(),FirstLegLoadingPlanCompletedV1.class); }
|
||||
try {
|
||||
ObjectMapper compatibleMapper = objectMapper.copy()
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
return compatibleMapper.treeToValue(envelope.payload(), FirstLegLoadingPlanCompletedV1.class);
|
||||
}
|
||||
catch(Exception exception) { throw new IllegalArgumentException("尾程事件负载无效",exception); }
|
||||
}
|
||||
|
||||
@@ -65,22 +81,33 @@ public class LoadingCompletedConsumer {
|
||||
}
|
||||
|
||||
private void createOrder(EventEnvelope event,FirstLegLoadingPlanCompletedV1 payload,
|
||||
FirstLegLoadingPlanCompletedV1.Order source,String orderNo) {
|
||||
String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(objectMapper.valueToTree(source)));
|
||||
FirstLegLoadingPlanCompletedV1.Order source,String orderNo, JsonNode rawSource) {
|
||||
String orderHash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(rawSource));
|
||||
LastlegOrder existing=orderMapper.findSource(event.tenantId(),event.sourceRegion().name(),source.sourceInboundId());
|
||||
if(existing!=null) {
|
||||
if(!Objects.equals(existing.getSourcePayloadHash(),orderHash)) throw new IllegalStateException("来源入仓单快照冲突: "+source.sourceInboundId());
|
||||
return;
|
||||
}
|
||||
LastlegOrder order=new LastlegOrder();
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setTenantId(event.tenantId()); order.setOrderType("FIRST_LEG");
|
||||
String orderType = rawSource.path("orderType").asText(event.payload().path("orderType").asText("FIRST_LEG"));
|
||||
String customerId = rawSource.path("customerId")
|
||||
.asText(event.payload().path("customerId").asText(event.tenantId()));
|
||||
if (!event.tenantId().equals(customerId)) {
|
||||
throw new IllegalArgumentException("Order customerId does not match the trusted event customer");
|
||||
}
|
||||
List<DropshipRequests.Pallet> dropshipPallets = "ONE_CLICK_DROPSHIP".equals(orderType)
|
||||
? pallets(rawSource) : List.of();
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType) && dropshipPallets.isEmpty()) {
|
||||
throw new IllegalArgumentException("One-click dropship event has no pallet snapshot");
|
||||
}
|
||||
order.setId(IdWorker.getId()); order.setOrderNo(orderNo); order.setCustomerId(customerId); order.setOrderType(orderType);
|
||||
order.setInboundCode(source.inboundCode()); order.setSourceRegion(event.sourceRegion().name());
|
||||
order.setSourceInboundId(source.sourceInboundId()); order.setRootInboundId(source.rootInboundId());
|
||||
order.setSourceOrderId(source.sourceOrderId()); order.setSourceOrderNo(source.sourceOrderNo()); order.setRootOrderId(source.rootOrderId());
|
||||
order.setOverseasWarehouseId(payload.warehouseId()); order.setOverseasWarehouseName(payload.warehouseName());
|
||||
order.setFbaWarehouseCode(source.destinationWarehouseCode()); order.setContainerNo(payload.containerNo());
|
||||
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
|
||||
order.setPackageCount(source.packageCount()); order.setPalletCount(Objects.requireNonNullElse(source.palletCount(),0));
|
||||
order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());
|
||||
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
|
||||
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
|
||||
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
|
||||
@@ -110,5 +137,57 @@ public class LoadingCompletedConsumer {
|
||||
LastlegOrderStatusHistory history=new LastlegOrderStatusHistory();
|
||||
history.setOrderId(order.getId()); history.setToStatus("PENDING_ASSIGNMENT"); history.setEventId(event.eventId());
|
||||
history.setRemark("头程装柜完成自动生成"); history.setCreatedAt(LocalDateTime.now()); historyMapper.insert(history);
|
||||
if ("ONE_CLICK_DROPSHIP".equals(orderType)) {
|
||||
long warehouseId;
|
||||
try { warehouseId = Long.parseLong(payload.warehouseId()); }
|
||||
catch (Exception exception) { throw new IllegalArgumentException("One-click dropship warehouseId is invalid", exception); }
|
||||
dropshipOrders.receive(new DropshipRequests.Receive(customerId, order.getId(), warehouseId,
|
||||
event.eventId() + ":DROPSHIP", dropshipPallets));
|
||||
}
|
||||
}
|
||||
|
||||
private List<DropshipRequests.Pallet> pallets(JsonNode source) {
|
||||
List<DropshipRequests.Pallet> result = new ArrayList<>();
|
||||
JsonNode nodes = source.path("pallets");
|
||||
if (nodes.isArray()) {
|
||||
for (JsonNode pallet : nodes) {
|
||||
String palletNo = pallet.path("palletNo").asText(pallet.path("sourcePalletNo").asText(null));
|
||||
List<DropshipRequests.SkuPackages> skuRows = skuPackages(pallet);
|
||||
if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) {
|
||||
result.add(new DropshipRequests.Pallet(palletNo, skuRows));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
String palletNo = source.path("palletNo").asText(source.path("sourcePalletNo").asText(null));
|
||||
List<DropshipRequests.SkuPackages> skuRows = skuPackages(source);
|
||||
if (palletNo != null && !palletNo.isBlank() && !skuRows.isEmpty()) {
|
||||
result.add(new DropshipRequests.Pallet(palletNo, skuRows));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<DropshipRequests.SkuPackages> skuPackages(JsonNode source) {
|
||||
List<DropshipRequests.SkuPackages> result = new ArrayList<>();
|
||||
JsonNode nodes = source.path("skuPackages");
|
||||
if (!nodes.isArray()) nodes = source.path("items");
|
||||
if (!nodes.isArray()) return result;
|
||||
for (JsonNode item : nodes) {
|
||||
String sku = item.path("skuCode").asText(null);
|
||||
long packageCount = item.path("packageCount").asLong(0);
|
||||
long productQuantity = item.path("productQuantity").asLong(0);
|
||||
JsonNode markNodes = item.path("cartonMarks");
|
||||
if (!markNodes.isArray()) markNodes = item.path("boxMarks");
|
||||
if (!markNodes.isArray()) markNodes = item.path("cartonNos");
|
||||
List<String> marks = new ArrayList<>();
|
||||
if (markNodes.isArray()) {
|
||||
for (JsonNode mark : markNodes) if (!mark.asText().isBlank()) marks.add(mark.asText());
|
||||
}
|
||||
if (sku != null && !sku.isBlank() && packageCount > 0 && productQuantity > 0 && !marks.isEmpty()) {
|
||||
String productName = item.path("productName").asText(item.path("productNameZh").asText(null));
|
||||
result.add(new DropshipRequests.SkuPackages(sku, productName, packageCount, productQuantity, marks));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.baiyue.lastleg.transfer;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@Configuration @MapperScan("com.baiyue.lastleg.transfer.mapper") public class TransferMapperConfiguration {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user