优化
This commit is contained in:
@@ -390,13 +390,35 @@ CREATE TABLE IF NOT EXISTS ll_receiving_container_session (
|
||||
id BIGINT NOT NULL COMMENT '柜号签收会话ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识', container_no VARCHAR(64) NOT NULL COMMENT '集装箱号',
|
||||
operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识', operator_name VARCHAR(255) NULL COMMENT '操作账号名称', active_operator_key VARCHAR(255) NULL COMMENT '未完成会话账号唯一锁键',
|
||||
active_container_key VARCHAR(255) NULL COMMENT '未完成会话柜号唯一锁键', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_SIGN' COMMENT '会话状态:WAIT_SIGN、SIGNED、RECEIVING、COMPLETED',
|
||||
photo_file_ids JSON NULL COMMENT '签收卸柜照片文件ID列表', prepared_at DATETIME(3) NULL COMMENT '选择柜号时间', signed_at DATETIME(3) NULL COMMENT '签收时间',
|
||||
sign_photos JSON NULL COMMENT '签收卸柜照片角色与文件ID列表', prepared_at DATETIME(3) NULL COMMENT '选择柜号时间', signed_at DATETIME(3) NULL COMMENT '签收时间',
|
||||
receiving_at DATETIME(3) NULL COMMENT '首次收货时间', completed_at DATETIME(3) NULL COMMENT '完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_receiving_active_operator(active_operator_key), UNIQUE KEY uk_ll_receiving_active_container(active_container_key),
|
||||
KEY idx_ll_receiving_container(customer_id,container_no,status), KEY idx_ll_receiving_operator(customer_id,operator_key,created_at)
|
||||
) ENGINE=InnoDB COMMENT='海外PDA柜号签收收货会话';
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_receiving_container_session' AND column_name='sign_photos'),
|
||||
'ALTER TABLE ll_receiving_container_session MODIFY COLUMN sign_photos JSON NULL COMMENT ''签收卸柜照片角色与文件ID列表''',
|
||||
'ALTER TABLE ll_receiving_container_session ADD COLUMN sign_photos JSON NULL COMMENT ''签收卸柜照片角色与文件ID列表'' AFTER status'));
|
||||
PREPARE phase4_receiving_sign_photos_stmt FROM @ddl; EXECUTE phase4_receiving_sign_photos_stmt; DEALLOCATE PREPARE phase4_receiving_sign_photos_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_receiving_container_session' AND column_name='photo_file_ids'),
|
||||
'ALTER TABLE ll_receiving_container_session DROP COLUMN photo_file_ids', 'SELECT 1'));
|
||||
PREPARE phase4_receiving_drop_raw_photos_stmt FROM @ddl; EXECUTE phase4_receiving_drop_raw_photos_stmt; DEALLOCATE PREPARE phase4_receiving_drop_raw_photos_stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_sku_receipt (
|
||||
id BIGINT NOT NULL COMMENT 'FBA SKU实收进度ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识',
|
||||
session_id BIGINT NOT NULL COMMENT '柜号签收会话ID', plan_id BIGINT NOT NULL COMMENT '入仓计划ID',
|
||||
line_id BIGINT NOT NULL COMMENT '入仓计划明细ID', pallet_id BIGINT NOT NULL COMMENT '计划托盘ID',
|
||||
order_id BIGINT NOT NULL COMMENT '尾程订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓码快照',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照', product_name VARCHAR(255) NULL COMMENT '产品名称快照',
|
||||
received_packages INT NOT NULL COMMENT 'SKU实收箱数', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_receiving_sku_pallet (customer_id,line_id,pallet_id,sku_code),
|
||||
KEY idx_ll_receiving_sku_session (customer_id,session_id,line_id),
|
||||
KEY idx_ll_receiving_sku_plan (customer_id,plan_id,line_id)
|
||||
) ENGINE=InnoDB COMMENT='海外PDA柜号FBA SKU实收进度';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_container_operation (
|
||||
id BIGINT NOT NULL COMMENT '柜号收货操作ID', session_id BIGINT NOT NULL COMMENT '柜号签收会话ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识',
|
||||
operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识', action VARCHAR(32) NOT NULL COMMENT '操作动作', idempotency_key VARCHAR(255) NOT NULL COMMENT '客户端幂等键',
|
||||
|
||||
+8
@@ -86,6 +86,14 @@ public class ReceivingContainerController {
|
||||
return ApiResponse.success(service.resolveCarton(identity.customerId(), identity.operatorKey(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/current/skus/resolve")
|
||||
public ApiResponse<ReceivingContainerResponses.SkuResolved> resolveSku(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ReceivingContainerRequests.SkuResolve request) {
|
||||
Identity identity = identity(user);
|
||||
return ApiResponse.success(service.resolveSku(identity.customerId(), identity.operatorKey(), request));
|
||||
}
|
||||
|
||||
@PostMapping("/current/pallets/resolve")
|
||||
public ApiResponse<ReceivingContainerResponses.PalletResolved> resolvePallet(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+6
@@ -16,6 +16,12 @@ public interface ReceivingSkuReceiptMapper extends BaseMapper<ReceivingSkuReceip
|
||||
@Param("lineId") Long lineId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT * FROM ll_receiving_sku_receipt WHERE customer_id=#{customerId} " +
|
||||
"AND line_id=#{lineId} AND sku_code=#{skuCode} ORDER BY id")
|
||||
List<ReceivingSkuReceipt> listLineSku(@Param("customerId") String customerId,
|
||||
@Param("lineId") Long lineId,
|
||||
@Param("skuCode") String skuCode);
|
||||
|
||||
@Select("SELECT * FROM ll_receiving_sku_receipt WHERE customer_id=#{customerId} " +
|
||||
"AND plan_id=#{planId} ORDER BY line_id,pallet_id,sku_code,id")
|
||||
List<ReceivingSkuReceipt> listPlan(@Param("customerId") String customerId,
|
||||
|
||||
+54
@@ -9,7 +9,9 @@ import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPalletAllocation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundPdaTask;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.InboundStockReceiptMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
@@ -38,6 +40,7 @@ public class InboundStockReceiptService {
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private InboundStockReceiptMapper receipts;
|
||||
@Resource private ReceivingSkuReceiptMapper receivingSkuReceipts;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@@ -56,6 +59,9 @@ public class InboundStockReceiptService {
|
||||
.filter(allocation -> value(allocation.getPickedPackageCount()) > 0)
|
||||
.sorted(Comparator.comparing(InboundPlanPalletAllocation::getId))
|
||||
.collect(Collectors.groupingBy(InboundPlanPalletAllocation::getLineId));
|
||||
Map<Long, List<ReceivingSkuReceipt>> skuReceiptsByLine = receivingSkuReceipts
|
||||
.listPlan(customerId, plan.getId()).stream()
|
||||
.collect(Collectors.groupingBy(ReceivingSkuReceipt::getLineId));
|
||||
|
||||
Set<Long> expectedAllocationIds = allocationsByLine.values().stream().flatMap(List::stream)
|
||||
.map(InboundPlanPalletAllocation::getId).collect(Collectors.toSet());
|
||||
@@ -82,6 +88,12 @@ public class InboundStockReceiptService {
|
||||
if (pickedPackages != value(line.getRequiredPackageCount())) {
|
||||
throw conflict("PDA picked packages must equal the inbound order package total");
|
||||
}
|
||||
List<ReceivingSkuReceipt> exactSkuReceipts = skuReceiptsByLine.getOrDefault(line.getId(), List.of());
|
||||
if (!exactSkuReceipts.isEmpty()) {
|
||||
created.addAll(persistExactSkuReceipts(customerId, plan, task, line, lineAllocations,
|
||||
palletById, order, exactSkuReceipts, pickedPackages, receivedAt));
|
||||
continue;
|
||||
}
|
||||
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||
? List.of() : skuRemainders(order.getId(), pickedPackages);
|
||||
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
||||
@@ -104,6 +116,48 @@ public class InboundStockReceiptService {
|
||||
completeOrders(customerId, plan, orderById, receivedAt);
|
||||
}
|
||||
|
||||
private List<InboundStockReceipt> persistExactSkuReceipts(String customerId, InboundPlan plan,
|
||||
InboundPdaTask task, InboundPlanLine line, List<InboundPlanPalletAllocation> lineAllocations,
|
||||
Map<Long, InboundPlanPallet> palletById, LastlegOrder order,
|
||||
List<ReceivingSkuReceipt> exactReceipts, long pickedPackages, LocalDateTime receivedAt) {
|
||||
long actualPackages = exactReceipts.stream().mapToLong(value -> value(value.getReceivedPackages())).sum();
|
||||
if (actualPackages != pickedPackages) throw conflict("FBA SKU receipts must equal the picked package total");
|
||||
|
||||
Map<String, Long> expectedBySku = skuRemainders(order.getId(), pickedPackages).stream()
|
||||
.collect(Collectors.toMap(value -> value.skuCode, value -> value.remaining, Long::sum,
|
||||
java.util.LinkedHashMap::new));
|
||||
Map<String, Long> actualBySku = exactReceipts.stream().collect(Collectors.groupingBy(
|
||||
value -> value.getSkuCode().toUpperCase(java.util.Locale.ROOT), java.util.LinkedHashMap::new,
|
||||
Collectors.summingLong(value -> value(value.getReceivedPackages()))));
|
||||
Map<String, Long> normalizedExpected = expectedBySku.entrySet().stream().collect(Collectors.toMap(
|
||||
value -> value.getKey().toUpperCase(java.util.Locale.ROOT), Map.Entry::getValue, Long::sum,
|
||||
java.util.LinkedHashMap::new));
|
||||
if (!actualBySku.equals(normalizedExpected)) {
|
||||
throw conflict("FBA SKU receipts must exactly match the frozen SKU package snapshots");
|
||||
}
|
||||
|
||||
Map<Long, InboundPlanPalletAllocation> allocationByPallet = lineAllocations.stream()
|
||||
.collect(Collectors.toMap(InboundPlanPalletAllocation::getPalletId, value -> value));
|
||||
Map<Long, List<ReceivingSkuReceipt>> receiptsByPallet = exactReceipts.stream()
|
||||
.collect(Collectors.groupingBy(ReceivingSkuReceipt::getPalletId));
|
||||
List<InboundStockReceipt> created = new java.util.ArrayList<>();
|
||||
for (Map.Entry<Long, List<ReceivingSkuReceipt>> entry : receiptsByPallet.entrySet()) {
|
||||
InboundPlanPalletAllocation allocation = allocationByPallet.get(entry.getKey());
|
||||
if (allocation == null) throw conflict("FBA SKU receipt pallet has no matching allocation");
|
||||
long palletPackages = entry.getValue().stream().mapToLong(value -> value(value.getReceivedPackages())).sum();
|
||||
if (palletPackages != value(allocation.getPickedPackageCount())) {
|
||||
throw conflict("FBA SKU receipt pallet total must match the allocation total");
|
||||
}
|
||||
InboundPlanPallet pallet = requirePallet(palletById, allocation);
|
||||
for (ReceivingSkuReceipt exact : entry.getValue()) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
new SkuSnapshot(exact.getSkuCode(), exact.getProductName(), null,
|
||||
value(exact.getReceivedPackages())), receivedAt));
|
||||
}
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
private void completeOrders(String customerId, InboundPlan plan, Map<Long, LastlegOrder> orderById,
|
||||
LocalDateTime completedAt) {
|
||||
for (LastlegOrder snapshot : orderById.values()) {
|
||||
|
||||
+160
-14
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
@@ -18,10 +19,13 @@ import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerRequests;
|
||||
import com.baiyue.lastleg.inboundplan.pda.dto.ReceivingContainerResponses;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerOperation;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingContainerSession;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.ReceivingSkuReceipt;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.UnloadPhotoFileGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerOperationMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingContainerSessionMapper;
|
||||
import com.baiyue.lastleg.inboundplan.pda.mapper.ReceivingSkuReceiptMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
@@ -47,6 +51,8 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ReceivingContainerService {
|
||||
@@ -56,8 +62,10 @@ public class ReceivingContainerService {
|
||||
@Resource private InboundPlanPalletAllocationMapper allocations;
|
||||
@Resource private InboundPlanCartonMapper cartons;
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private ReceivingContainerSessionMapper sessions;
|
||||
@Resource private ReceivingContainerOperationMapper operations;
|
||||
@Resource private ReceivingSkuReceiptMapper skuReceipts;
|
||||
@Resource private WarehouseResourceGateway warehouse;
|
||||
@Resource private UnloadPhotoFileGateway unloadPhotos;
|
||||
@Resource private InboundPdaWorkflowService workflow;
|
||||
@@ -85,7 +93,8 @@ public class ReceivingContainerService {
|
||||
Totals totals = totals(entry.getValue());
|
||||
boolean active = current != null && entry.getKey().equals(current.getContainerNo());
|
||||
return new ReceivingContainerResponses.Candidate(entry.getKey(), active ? current.getStatus() : "WAIT_SIGN",
|
||||
entry.getValue().size(), totals.inboundCount(), totals.total(), totals.received(), active);
|
||||
entry.getValue().size(), totals.inboundCount(), totals.destinationCodes(), totals.plannedPalletCount(),
|
||||
totals.total(), totals.received(), active);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -144,14 +153,14 @@ public class ReceivingContainerService {
|
||||
if (replay != null) return read(replay, ReceivingContainerResponses.Session.class);
|
||||
ReceivingContainerSession session = requireCurrent(customerId, operatorKey, true);
|
||||
if (!"WAIT_SIGN".equals(session.getStatus())) throw conflict("Container has already been signed");
|
||||
List<Long> photos = request.photoFileIds().stream().distinct().sorted().toList();
|
||||
if (photos.size() != request.photoFileIds().size()) throw conflict("Unload photo file IDs cannot be duplicated");
|
||||
photos.forEach(fileId -> unloadPhotos.validate(customerId, session.getId(), fileId));
|
||||
List<ReceivingContainerResponses.SignedPhoto> photos = validateSignPhotos(request);
|
||||
List<Long> photoFileIds = photos.stream().map(ReceivingContainerResponses.SignedPhoto::fileId).toList();
|
||||
photoFileIds.forEach(fileId -> unloadPhotos.validate(customerId, session.getId(), fileId));
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
session.setPhotoFileIds(write(photos)); session.setStatus("SIGNED"); session.setSignedAt(now);
|
||||
session.setSignPhotos(write(photos)); session.setStatus("SIGNED"); session.setSignedAt(now);
|
||||
session.setOperatorName(operatorName); session.setUpdatedAt(now); update(session);
|
||||
for (InboundPlan plan : requirePlans(customerId, session.getContainerNo())) {
|
||||
workflow.ensureSignedTask(customerId, plan, operatorKey, operatorName, photos, now);
|
||||
workflow.ensureSignedTask(customerId, plan, operatorKey, operatorName, photoFileIds, now);
|
||||
}
|
||||
ReceivingContainerResponses.Session result = snapshot(session, requirePlans(customerId, session.getContainerNo()));
|
||||
save(session, "SIGN", request.idempotencyKey(), request, result); return result;
|
||||
@@ -181,6 +190,19 @@ public class ReceivingContainerService {
|
||||
inbound.plannedPalletCount(), inbound.totalPackages(), inbound.receivedPackages(), inbound.pendingPackages());
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.SkuResolved resolveSku(String customerId, String operatorKey,
|
||||
ReceivingContainerRequests.SkuResolve request) {
|
||||
ReceivingContainerSession session = requireSigned(customerId, operatorKey);
|
||||
List<SkuRef> matches = refs(customerId, session.getContainerNo()).stream()
|
||||
.filter(ref -> "FBA".equals(destinationType(ref.line(), ref.order())))
|
||||
.map(ref -> skuRef(ref, request.skuCode()))
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
if (matches.isEmpty()) throw notFound("SKU was not found in the current FBA container");
|
||||
if (matches.size() != 1) throw conflict("SKU belongs to multiple inbound codes in the current container");
|
||||
return skuResponse(matches.get(0));
|
||||
}
|
||||
|
||||
public ReceivingContainerResponses.PalletResolved resolvePallet(String customerId, String operatorKey,
|
||||
ReceivingContainerRequests.PalletResolve request) {
|
||||
ReceivingContainerSession session = requireSigned(customerId, operatorKey);
|
||||
@@ -201,14 +223,17 @@ public class ReceivingContainerService {
|
||||
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) {
|
||||
throw conflict("OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden");
|
||||
}
|
||||
SkuRef sku = requireSku(ref, request.skuCode());
|
||||
validateSkuPending(customerId, sku, request.packageCount());
|
||||
workflow.validateBulkResolved(customerId, ref.plan(), ref.line(), request.packageCount());
|
||||
WarehouseResourceGateway.WarehousePalletReference resource = warehouse.resolvePallet(request.palletCode());
|
||||
validatePallet(ref, resource);
|
||||
InboundPlanPallet pallet = choosePallet(ref, resource);
|
||||
workflow.validateBulkResolved(customerId, ref.plan(), ref.line(), request.packageCount());
|
||||
Long resourceLockId = workflow.assignResolvedPallet(customerId, ref.plan(), pallet, resource);
|
||||
try {
|
||||
int accepted = workflow.receiveBulkResolved(customerId, ref.plan(), ref.line(), pallet,
|
||||
request.packageCount(), operatorKey, operatorName);
|
||||
recordSkuReceipt(session, sku, pallet, accepted);
|
||||
markReceiving(session);
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(ref);
|
||||
ReceivingContainerResponses.Receipt result = new ReceivingContainerResponses.Receipt("RECEIVED",
|
||||
@@ -267,6 +292,7 @@ public class ReceivingContainerService {
|
||||
List<InboundPlan> containerPlans = requirePlans(customerId, session.getContainerNo());
|
||||
Totals totals = totals(containerPlans);
|
||||
if (totals.total() != totals.received()) throw conflict("Every inbound code must be fully received before completing the container");
|
||||
validateFbaSkuBalance(customerId, session.getContainerNo());
|
||||
workflow.completeContainer(customerId, containerPlans);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
session.setStatus("COMPLETED"); session.setCompletedAt(now); session.setUpdatedAt(now);
|
||||
@@ -383,7 +409,7 @@ public class ReceivingContainerService {
|
||||
int total = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum();
|
||||
int received = inbounds.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum();
|
||||
return new ReceivingContainerResponses.Session(session.getContainerNo(), session.getStatus(), session.getOperatorName(),
|
||||
value(session.getVersion()), photoIds(session), session.getPreparedAt(), session.getSignedAt(),
|
||||
value(session.getVersion()), signPhotos(session), session.getPreparedAt(), session.getSignedAt(),
|
||||
session.getReceivingAt(), session.getCompletedAt(), total, received, Math.max(0, total - received), inbounds);
|
||||
}
|
||||
|
||||
@@ -403,10 +429,125 @@ public class ReceivingContainerService {
|
||||
private Totals totals(List<InboundPlan> containerPlans) {
|
||||
List<ReceivingContainerResponses.Inbound> values = refs(containerPlans.get(0).getCustomerId(),
|
||||
containerPlans.get(0).getContainerNo()).stream().map(this::inbound).toList();
|
||||
return new Totals(values.size(), values.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum(),
|
||||
List<String> destinationCodes = values.stream().map(this::destinationCode)
|
||||
.filter(Objects::nonNull).distinct().sorted().toList();
|
||||
return new Totals(values.size(), destinationCodes,
|
||||
values.stream().mapToInt(ReceivingContainerResponses.Inbound::plannedPalletCount).sum(),
|
||||
values.stream().mapToInt(ReceivingContainerResponses.Inbound::totalPackages).sum(),
|
||||
values.stream().mapToInt(ReceivingContainerResponses.Inbound::receivedPackages).sum());
|
||||
}
|
||||
|
||||
private String destinationCode(ReceivingContainerResponses.Inbound inbound) {
|
||||
return trim("FBA".equals(inbound.destinationType()) ? inbound.fbaWarehouseCode()
|
||||
: inbound.destinationWarehouseCode());
|
||||
}
|
||||
|
||||
private List<ReceivingContainerResponses.SignedPhoto> validateSignPhotos(ReceivingContainerRequests.Sign request) {
|
||||
if (request.photos() == null || request.photos().size() != 3) {
|
||||
throw conflict("Exactly three typed unload photos are required");
|
||||
}
|
||||
if (request.photos().stream().anyMatch(Objects::isNull)) throw conflict("Unload photo cannot be null");
|
||||
Set<com.baiyue.lastleg.inboundplan.pda.dto.ReceivingPhotoRole> roles = request.photos().stream()
|
||||
.map(ReceivingContainerRequests.SignedPhoto::role).collect(Collectors.toSet());
|
||||
if (roles.size() != 3 || roles.contains(null)) throw conflict("Each unload photo role is required exactly once");
|
||||
Set<Long> fileIds = request.photos().stream().map(ReceivingContainerRequests.SignedPhoto::fileId)
|
||||
.boxed().collect(Collectors.toSet());
|
||||
if (fileIds.size() != 3 || fileIds.stream().anyMatch(value -> value == null || value <= 0)) {
|
||||
throw conflict("Unload photo file IDs must be positive and unique");
|
||||
}
|
||||
return request.photos().stream()
|
||||
.sorted(Comparator.comparing(value -> value.role().ordinal()))
|
||||
.map(value -> new ReceivingContainerResponses.SignedPhoto(value.role(), value.fileId())).toList();
|
||||
}
|
||||
|
||||
private SkuRef requireSku(InboundRef ref, String skuCode) {
|
||||
SkuRef result = skuRef(ref, skuCode);
|
||||
if (result == null) throw notFound("SKU is not part of the selected inbound code");
|
||||
return result;
|
||||
}
|
||||
|
||||
private SkuRef skuRef(InboundRef ref, String skuCode) {
|
||||
String requested = trim(skuCode);
|
||||
if (requested == null) return null;
|
||||
List<LastlegOrderDetail> details = orderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, ref.order().getCustomerId())
|
||||
.eq(LastlegOrderDetail::getOrderId, ref.order().getId()).orderByAsc(LastlegOrderDetail::getId));
|
||||
List<LastlegOrderDetail> matched = details.stream()
|
||||
.filter(detail -> requested.equalsIgnoreCase(trim(detail.getSkuCode()))).toList();
|
||||
int expected = matched.stream().mapToInt(detail -> value(detail.getPackageCount())).sum();
|
||||
if (expected <= 0) return null;
|
||||
String canonicalSku = matched.stream().map(LastlegOrderDetail::getSkuCode).map(ReceivingContainerService::trim)
|
||||
.filter(Objects::nonNull).findFirst().orElse(requested);
|
||||
String productName = matched.stream().map(detail -> first(detail.getProductNameZh(), detail.getProductNameEn()))
|
||||
.filter(Objects::nonNull).findFirst().orElse(null);
|
||||
return new SkuRef(ref, canonicalSku, productName, expected);
|
||||
}
|
||||
|
||||
private ReceivingContainerResponses.SkuResolved skuResponse(SkuRef sku) {
|
||||
ReceivingContainerResponses.Inbound inbound = inbound(sku.ref());
|
||||
int received = receivedSkuPackages(sku.ref().order().getCustomerId(), sku.ref().line().getId(), sku.skuCode());
|
||||
return new ReceivingContainerResponses.SkuResolved(sku.skuCode(), sku.productName(), inbound.inboundCode(),
|
||||
inbound.destinationType(), inbound.destinationWarehouseId(), inbound.destinationWarehouseCode(),
|
||||
inbound.destinationWarehouseName(), inbound.fbaWarehouseCode(), inbound.plannedPalletCount(),
|
||||
sku.expectedPackages(), received, Math.max(0, sku.expectedPackages() - received));
|
||||
}
|
||||
|
||||
private void validateSkuPending(String customerId, SkuRef sku, int packageCount) {
|
||||
int received = receivedSkuPackages(customerId, sku.ref().line().getId(), sku.skuCode());
|
||||
if (packageCount <= 0 || packageCount > sku.expectedPackages() - received) {
|
||||
throw conflict("Received package count exceeds the SKU pending total");
|
||||
}
|
||||
}
|
||||
|
||||
private int receivedSkuPackages(String customerId, Long lineId, String skuCode) {
|
||||
return skuReceipts.listLineSku(customerId, lineId, skuCode).stream()
|
||||
.mapToInt(value -> value(value.getReceivedPackages())).sum();
|
||||
}
|
||||
|
||||
private void recordSkuReceipt(ReceivingContainerSession session, SkuRef sku, InboundPlanPallet pallet, int packages) {
|
||||
List<ReceivingSkuReceipt> existing = skuReceipts.lockLineSku(session.getCustomerId(),
|
||||
sku.ref().line().getId(), sku.skuCode());
|
||||
ReceivingSkuReceipt receipt = existing.stream()
|
||||
.filter(value -> Objects.equals(value.getPalletId(), pallet.getId())).findFirst().orElse(null);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (receipt == null) {
|
||||
receipt = new ReceivingSkuReceipt(); receipt.setId(IdWorker.getId());
|
||||
receipt.setCustomerId(session.getCustomerId()); receipt.setSessionId(session.getId());
|
||||
receipt.setPlanId(sku.ref().plan().getId()); receipt.setLineId(sku.ref().line().getId());
|
||||
receipt.setPalletId(pallet.getId()); receipt.setOrderId(sku.ref().order().getId());
|
||||
receipt.setInboundCode(sku.ref().line().getInboundCode()); receipt.setSkuCode(sku.skuCode());
|
||||
receipt.setProductName(sku.productName()); receipt.setReceivedPackages(packages);
|
||||
receipt.setCreatedAt(now); receipt.setUpdatedAt(now); receipt.setVersion(0);
|
||||
skuReceipts.insert(receipt);
|
||||
} else {
|
||||
receipt.setReceivedPackages(value(receipt.getReceivedPackages()) + packages);
|
||||
receipt.setUpdatedAt(now);
|
||||
if (skuReceipts.updateById(receipt) != 1) throw conflict("SKU receiving progress changed concurrently");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateFbaSkuBalance(String customerId, String containerNo) {
|
||||
for (InboundRef ref : refs(customerId, containerNo)) {
|
||||
if (!"FBA".equals(destinationType(ref.line(), ref.order()))) continue;
|
||||
List<LastlegOrderDetail> details = orderDetails.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, ref.order().getId()));
|
||||
Set<String> skuCodes = details.stream().map(LastlegOrderDetail::getSkuCode)
|
||||
.map(ReceivingContainerService::trim).filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(java.util.LinkedHashSet::new));
|
||||
int expected = 0;
|
||||
int received = 0;
|
||||
for (String skuCode : skuCodes) {
|
||||
SkuRef sku = requireSku(ref, skuCode);
|
||||
int skuReceived = receivedSkuPackages(customerId, ref.line().getId(), sku.skuCode());
|
||||
if (skuReceived != sku.expectedPackages()) throw conflict("Every FBA SKU must be fully received before completing the container");
|
||||
expected += sku.expectedPackages(); received += skuReceived;
|
||||
}
|
||||
if (skuCodes.isEmpty() || expected != value(ref.line().getRequiredPackageCount()) || received != expected) {
|
||||
throw conflict("FBA SKU package snapshots must exactly match the inbound code total");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String destinationType(InboundPlanLine line, LastlegOrder order) {
|
||||
String value = line.getDestinationType();
|
||||
if (value == null || value.isBlank()) value = order.getDestinationType();
|
||||
@@ -453,10 +594,11 @@ public class ReceivingContainerService {
|
||||
value.setCreatedAt(LocalDateTime.now()); operations.insert(value);
|
||||
}
|
||||
|
||||
private List<Long> photoIds(ReceivingContainerSession session) {
|
||||
if (session.getPhotoFileIds() == null || session.getPhotoFileIds().isBlank()) return List.of();
|
||||
try { return objectMapper.readValue(session.getPhotoFileIds(), new TypeReference<List<Long>>() { }); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read receiving photo IDs", exception); }
|
||||
private List<ReceivingContainerResponses.SignedPhoto> signPhotos(ReceivingContainerSession session) {
|
||||
if (session.getSignPhotos() == null || session.getSignPhotos().isBlank()) return List.of();
|
||||
try { return objectMapper.readValue(session.getSignPhotos(),
|
||||
new TypeReference<List<ReceivingContainerResponses.SignedPhoto>>() { }); }
|
||||
catch (JsonProcessingException exception) { throw new IllegalStateException("Cannot read signed receiving photos", exception); }
|
||||
}
|
||||
|
||||
private String hash(Object value) {
|
||||
@@ -484,10 +626,14 @@ public class ReceivingContainerService {
|
||||
return value;
|
||||
}
|
||||
private static int value(Integer value) { return value == null ? 0 : value; }
|
||||
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
|
||||
private static String first(String first, String second) { String value = trim(first); return value == null ? trim(second) : value; }
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
|
||||
private record InboundRef(InboundPlan plan, InboundPlanLine line, LastlegOrder order) { }
|
||||
private record Totals(int inboundCount, int total, int received) { }
|
||||
private record SkuRef(InboundRef ref, String skuCode, String productName, int expectedPackages) { }
|
||||
private record Totals(int inboundCount, List<String> destinationCodes, int plannedPalletCount,
|
||||
int total, int received) { }
|
||||
private record PrepareCommand(String containerNo, String idempotencyKey) { }
|
||||
}
|
||||
|
||||
@@ -682,7 +682,7 @@ CREATE TABLE IF NOT EXISTS ll_receiving_container_session (
|
||||
container_no VARCHAR(64) NOT NULL COMMENT '集装箱号', operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识',
|
||||
operator_name VARCHAR(255) NULL COMMENT '操作账号名称', active_operator_key VARCHAR(255) NULL COMMENT '未完成会话账号唯一锁键',
|
||||
active_container_key VARCHAR(255) NULL COMMENT '未完成会话柜号唯一锁键', status VARCHAR(32) NOT NULL DEFAULT 'WAIT_SIGN' COMMENT '会话状态:WAIT_SIGN、SIGNED、RECEIVING、COMPLETED',
|
||||
photo_file_ids JSON NULL COMMENT '签收卸柜照片文件ID列表', prepared_at DATETIME(3) NULL COMMENT '选择柜号时间',
|
||||
sign_photos JSON NULL COMMENT '签收卸柜照片角色与文件ID列表', prepared_at DATETIME(3) NULL COMMENT '选择柜号时间',
|
||||
signed_at DATETIME(3) NULL COMMENT '签收时间', receiving_at DATETIME(3) NULL COMMENT '首次收货时间',
|
||||
completed_at DATETIME(3) NULL COMMENT '完成时间', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本',
|
||||
@@ -690,6 +690,20 @@ CREATE TABLE IF NOT EXISTS ll_receiving_container_session (
|
||||
KEY idx_ll_receiving_container(customer_id,container_no,status), KEY idx_ll_receiving_operator(customer_id,operator_key,created_at)
|
||||
) ENGINE=InnoDB COMMENT='海外PDA柜号签收收货会话';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_sku_receipt (
|
||||
id BIGINT NOT NULL COMMENT 'FBA SKU实收进度ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识',
|
||||
session_id BIGINT NOT NULL COMMENT '柜号签收会话ID', plan_id BIGINT NOT NULL COMMENT '入仓计划ID',
|
||||
line_id BIGINT NOT NULL COMMENT '入仓计划明细ID', pallet_id BIGINT NOT NULL COMMENT '计划托盘ID',
|
||||
order_id BIGINT NOT NULL COMMENT '尾程订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓码快照',
|
||||
sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码快照', product_name VARCHAR(255) NULL COMMENT '产品名称快照',
|
||||
received_packages INT NOT NULL COMMENT 'SKU实收箱数', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_receiving_sku_pallet (customer_id,line_id,pallet_id,sku_code),
|
||||
KEY idx_ll_receiving_sku_session (customer_id,session_id,line_id),
|
||||
KEY idx_ll_receiving_sku_plan (customer_id,plan_id,line_id)
|
||||
) ENGINE=InnoDB COMMENT='海外PDA柜号FBA SKU实收进度';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_receiving_container_operation (
|
||||
id BIGINT NOT NULL COMMENT '柜号收货操作ID', session_id BIGINT NOT NULL COMMENT '柜号签收会话ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户账号标识',
|
||||
operator_key VARCHAR(128) NOT NULL COMMENT '操作账号唯一标识', action VARCHAR(32) NOT NULL COMMENT '操作动作', idempotency_key VARCHAR(255) NOT NULL COMMENT '客户端幂等键',
|
||||
|
||||
Reference in New Issue
Block a user