feat: complete finance and overseas receiving workflow

This commit is contained in:
2026-09-10 22:43:11 +08:00
parent e531ae9407
commit 56635ad4e4
66 changed files with 1601 additions and 417 deletions
@@ -60,10 +60,22 @@ public interface DropshipPalletizationMapper {
@Select("SELECT * FROM fl_dropship_pallet WHERE pallet_no=#{palletNo}") PalletRow palletByNo(@Param("palletNo") String palletNo);
@Select("SELECT * FROM fl_dropship_pallet WHERE seal_operation_id=#{operationId}") PalletRow palletBySealOperation(@Param("operationId") String operationId);
@Select("SELECT * FROM fl_dropship_pallet WHERE id=#{palletId} AND task_id=#{taskId} AND warehouse_id=#{warehouseId}")
@Select("""
SELECT p.* FROM fl_dropship_pallet p
WHERE p.id=#{palletId} AND p.warehouse_id=#{warehouseId}
AND (p.task_id=#{taskId} OR EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=p.id AND c.task_id=#{taskId}))
""")
PalletRow pallet(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("warehouseId") long warehouseId);
@Select("SELECT * FROM fl_dropship_pallet WHERE task_id=#{taskId} ORDER BY id") List<PalletRow> pallets(@Param("taskId") long taskId);
@Select("SELECT p.* FROM fl_dropship_pallet p JOIN fl_dropship_pallet_task t ON t.id=p.task_id WHERE t.tenant_id=#{tenantId} AND t.inbound_id=#{inboundId} ORDER BY p.id")
@Select("""
SELECT DISTINCT p.* FROM fl_dropship_pallet p
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
WHERE c.task_id=#{taskId} ORDER BY p.id
""") List<PalletRow> pallets(@Param("taskId") long taskId);
@Select("""
SELECT DISTINCT p.* FROM fl_dropship_pallet p
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
WHERE c.tenant_id=#{tenantId} AND c.inbound_id=#{inboundId} ORDER BY p.id
""")
List<PalletRow> palletsByInbound(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
@Select("SELECT inbound_code FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
String inboundCode(@Param("tenantId") long tenantId,@Param("inboundId") long inboundId);
@@ -123,46 +135,66 @@ public interface DropshipPalletizationMapper {
@Update("""
UPDATE fl_dropship_pallet SET pallet_status='SEALED',seal_operation_id=#{operationId},completed_by=#{operatorId},
completed_by_name=#{operatorName},completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{palletId} AND task_id=#{taskId} AND pallet_status='OPEN' AND version=#{expectedVersion}
WHERE id=#{palletId} AND warehouse_id=#{warehouseId} AND pallet_status='OPEN' AND version=#{expectedVersion}
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId})
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL)""")
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId} AND c.task_id=#{taskId})
AND NOT EXISTS(
SELECT 1 FROM fl_dropship_pallet_task t
WHERE EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c WHERE c.pallet_id=#{palletId} AND c.task_id=t.id)
AND (t.counted_cartons<>t.total_cartons OR t.allocated_cartons<>t.total_cartons
OR EXISTS(SELECT 1 FROM fl_dropship_pallet_carton pending WHERE pending.task_id=t.id AND pending.pallet_id IS NULL))
)""")
int seal(@Param("taskId") long taskId,@Param("palletId") long palletId,@Param("expectedVersion") int expectedVersion,
@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("SELECT COUNT(*) FROM fl_dropship_pallet WHERE task_id=#{taskId} AND pallet_status='OPEN'") int openPalletCount(@Param("taskId") long taskId);
@Param("warehouseId") long warehouseId,@Param("operationId") String operationId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
WHERE c.task_id=#{taskId} AND p.pallet_status='OPEN'
""") int openPalletCount(@Param("taskId") long taskId);
@Select("SELECT DISTINCT task_id FROM fl_dropship_pallet_carton WHERE pallet_id=#{palletId} ORDER BY task_id")
List<Long> taskIdsByPallet(@Param("palletId") long palletId);
@Update("""
UPDATE fl_dropship_pallet_task SET task_status='COMPLETED',completed_at=CURRENT_TIMESTAMP(3),version=version+1,updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{taskId} AND task_status='CLAIMED' AND counted_cartons=total_cartons AND allocated_cartons=total_cartons
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet p WHERE p.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId);
AND NOT EXISTS(SELECT 1 FROM fl_dropship_pallet_carton c JOIN fl_dropship_pallet p ON p.id=c.pallet_id
WHERE c.task_id=#{taskId} AND p.pallet_status='OPEN')""") int completeTask(@Param("taskId") long taskId);
@Update("""
UPDATE fl_inbound_order o SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(*) FROM fl_dropship_pallet p
WHERE p.inbound_id=o.id AND p.pallet_status='SEALED'),o.updated_by=#{operatorId},o.version=o.version+1
UPDATE fl_inbound_order o SET o.process_status='WAIT_SPLIT',o.pallet_count=(SELECT COUNT(DISTINCT p.id)
FROM fl_dropship_pallet_carton c JOIN fl_dropship_pallet p ON p.id=c.pallet_id
WHERE c.tenant_id=o.tenant_id AND c.inbound_id=o.id AND p.pallet_status='SEALED'),o.updated_by=#{operatorId},o.version=o.version+1
WHERE o.id=#{inboundId} AND o.tenant_id=#{tenantId} AND o.process_status='WAIT_PALLETIZE'
AND EXISTS(SELECT 1 FROM fl_dropship_pallet_task t WHERE t.id=#{taskId} AND t.task_status='COMPLETED')""")
int completeInbound(@Param("taskId") long taskId,@Param("tenantId") long tenantId,@Param("inboundId") long inboundId,@Param("operatorId") long operatorId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.pallet_id=#{palletId} ORDER BY c.sku_code,c.inbound_detail_id,c.id") List<CartonRow> cartons(@Param("palletId") long palletId);
@Select("""
SELECT c.*,p.pallet_no,o.inbound_code FROM fl_dropship_pallet_carton c
LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id=#{palletId} ORDER BY o.inbound_code,c.sku_code,c.inbound_detail_id,c.id
""") List<CartonRow> cartons(@Param("palletId") long palletId);
@Select("SELECT c.*,p.pallet_no FROM fl_dropship_pallet_carton c LEFT JOIN fl_dropship_pallet p ON p.id=c.pallet_id WHERE c.task_id=#{taskId} AND c.pallet_id IS NULL ORDER BY c.counted_at,c.id") List<CartonRow> unboundCartons(@Param("taskId") long taskId);
@Select("""
SELECT COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
LEFT JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
GROUP BY p.id,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count""")
SELECT COALESCE(SUM(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0)),0) weight_kg,
COALESCE(SUM(o.actual_volume_cbm/NULLIF(o.total_package_count,0)),0) volume_cbm
FROM fl_dropship_pallet_carton c
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE c.pallet_id=#{palletId}""")
PalletMetrics metrics(@Param("palletId") long palletId);
@Select("""
SELECT p.id pallet_id,p.pallet_no,'SEALED_PALLET' unit_type,TRUE indivisible,COUNT(c.id) carton_count,
COUNT(DISTINCT c.sku_code) sku_count,
COALESCE(o.actual_gross_weight_kg*COUNT(c.id)/NULLIF(o.total_package_count,0),0) weight_kg,
COALESCE(o.actual_volume_cbm*COUNT(c.id)/NULLIF(o.total_package_count,0),0) volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
WHERE p.inbound_id=#{inboundId} AND p.pallet_status='SEALED'
COALESCE(SUM(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0)),0) weight_kg,
COALESCE(SUM(o.actual_volume_cbm/NULLIF(o.total_package_count,0)),0) volume_cbm
FROM fl_dropship_pallet p JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id
WHERE EXISTS(SELECT 1 FROM fl_dropship_pallet_carton own WHERE own.pallet_id=p.id AND own.inbound_id=#{inboundId})
AND p.pallet_status='SEALED'
AND NOT EXISTS(SELECT 1 FROM fl_loading_plan_item i WHERE i.pallet_id=p.id AND i.deleted=0)
GROUP BY p.id,p.pallet_no,o.actual_gross_weight_kg,o.actual_volume_cbm,o.total_package_count ORDER BY p.id
GROUP BY p.id,p.pallet_no ORDER BY p.id
""") List<PalletCandidate> palletCandidates(@Param("inboundId") long inboundId);
@Select("""
SELECT p.id pallet_id,p.pallet_no,p.pallet_status,p.tenant_id account_id,p.inbound_id,o.order_id,o.order_no,
SELECT p.id pallet_id,p.pallet_no,p.pallet_status,c.tenant_id account_id,c.inbound_id,o.order_id,o.order_no,
o.inbound_code,o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,
o.destination_code destination_warehouse_code,o.destination_name destination_warehouse_name,o.customer_name,
c.inbound_detail_id,c.sku_code,MAX(c.cargo_name) cargo_name,COUNT(c.id) carton_count,
@@ -170,9 +202,9 @@ public interface DropshipPalletizationMapper {
COALESCE(o.actual_gross_weight_kg/NULLIF(o.total_package_count,0),0) unit_gross_weight_kg,
COALESCE(o.actual_net_weight_kg/NULLIF(o.total_package_count,0),0) unit_net_weight_kg,
COALESCE(o.actual_volume_cbm/NULLIF(o.total_package_count,0),0) unit_volume_cbm
FROM fl_dropship_pallet p JOIN fl_inbound_order o ON o.id=p.inbound_id AND o.tenant_id=p.tenant_id
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id WHERE p.id=#{palletId}
GROUP BY p.id,p.pallet_no,p.pallet_status,p.tenant_id,p.inbound_id,o.order_id,o.order_no,o.inbound_code,
FROM fl_dropship_pallet p JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
JOIN fl_inbound_order o ON o.id=c.inbound_id AND o.tenant_id=c.tenant_id WHERE p.id=#{palletId}
GROUP BY p.id,p.pallet_no,p.pallet_status,c.tenant_id,c.inbound_id,o.order_id,o.order_no,o.inbound_code,
o.warehouse_id,o.channel_code,o.channel_name,o.destination_warehouse_id,o.destination_code,o.destination_name,
o.customer_name,c.inbound_detail_id,c.sku_code,o.actual_gross_weight_kg,o.actual_net_weight_kg,o.actual_volume_cbm,o.total_package_count
ORDER BY c.inbound_detail_id
@@ -181,7 +213,7 @@ public interface DropshipPalletizationMapper {
@Getter @Setter class TaskRow { private Long id,tenantId,inboundId,warehouseId,assigneeId; private String inboundCode,customerName,warehouseName,taskStatus,assigneeName,processStatus,fulfillmentType; private Integer totalCartons,countedCartons,allocatedCartons,discrepancyCartons,version; }
@Getter @Setter class PalletRow { private Long id,taskId,tenantId,inboundId,warehouseId; private String palletNo,palletStatus,sealOperationId; private Integer version; private LocalDateTime completedAt; }
@Getter @Setter class LabelRow { private Long cartonLabelId,inboundDetailId; private String skuCode,cargoName,inboundCode,boxCode; private Integer unitsPerCarton; }
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
@Getter @Setter class CartonRow { private Long id,taskId,palletId,inboundId,inboundDetailId; private String palletNo,inboundCode,skuCode,cargoName,boxCode,discrepancyStatus,discrepancyRemark; private Integer expectedSkuQuantity,actualSkuQuantity; }
record PalletMetrics(BigDecimal weightKg,BigDecimal volumeCbm) {}
@Getter @Setter class PalletLine { private Long palletId,tenantId,accountId,inboundId,orderId,warehouseId,destinationWarehouseId,inboundDetailId; private String palletNo,palletStatus,orderNo,inboundCode,channelCode,channelName,destinationWarehouseCode,destinationWarehouseName,customerName,skuCode,cargoName; private Integer cartonCount,actualSkuQuantity; private BigDecimal unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm; }
}
@@ -85,7 +85,8 @@ public interface LoadingCompletedEventMapper {
pallet.pallet_no AS pallet_code,NULL AS allocation_no,
COUNT(*) OVER (PARTITION BY pallet.id,carton.tenant_id,carton.inbound_id) AS allocated_package_count,
carton.inbound_detail_id AS source_inbound_detail_id,carton.sku_code,
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code
item.product_name_zh,item.package_count,item.product_quantity,carton.box_code,
carton.actual_sku_quantity
FROM fl_dropship_pallet pallet
JOIN fl_dropship_pallet_carton carton ON carton.pallet_id=pallet.id
JOIN fl_loading_plan_item item ON item.loading_plan_id=#{planId}
@@ -98,15 +99,17 @@ public interface LoadingCompletedEventMapper {
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='SEALED'
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=c.tenant_id
AND i.inbound_id=c.inbound_id AND i.deleted=0 WHERE p.pallet_status='SEALED'
""")
int workPalletCount(@Param("planId") long planId);
@Select("""
SELECT COUNT(DISTINCT p.id) FROM fl_dropship_pallet p
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=p.tenant_id
AND i.inbound_id=p.inbound_id AND i.deleted=0 WHERE p.pallet_status='OPEN'
JOIN fl_dropship_pallet_carton c ON c.pallet_id=p.id
JOIN fl_loading_plan_item i ON i.loading_plan_id=#{planId} AND i.account_id=c.tenant_id
AND i.inbound_id=c.inbound_id AND i.deleted=0 WHERE p.pallet_status='OPEN'
""")
int openWorkPalletCount(@Param("planId") long planId);
@@ -138,6 +141,6 @@ public interface LoadingCompletedEventMapper {
private Boolean actualBinding;
private Integer allocationNo; private Integer allocatedPackageCount;
private Long sourceInboundDetailId; private String skuCode; private String productNameZh;
private Integer packageCount; private Integer productQuantity; private String boxCode;
private Integer packageCount; private Integer productQuantity; private Integer actualSkuQuantity; private String boxCode;
}
}
@@ -143,16 +143,18 @@ public class DropshipPalletizationService {
public Task seal(long taskId,long palletId,SealPalletRequest request,long operatorId,String operatorName){
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
PalletRow replay=mapper.palletBySealOperation(operation(request.operationId()));
if(replay!=null){ if(!Objects.equals(replay.getTaskId(),taskId)||!Objects.equals(replay.getId(),palletId)) throw conflict("封托幂等号已用于其他托盘"); return view(requireTask(taskId,request.warehouseId())); }
if(replay!=null){ if(!Objects.equals(replay.getId(),palletId)||!Objects.equals(replay.getWarehouseId(),request.warehouseId())) throw conflict("封托幂等号已用于其他托盘"); return view(requireTask(taskId,request.warehouseId())); }
requireOpenPallet(taskId,palletId,request.warehouseId());
if(mapper.unboundCount(taskId)>0) throw conflict("仍有已清点待绑托箱,不能封托");
if(mapper.seal(taskId,palletId,request.expectedVersion(),operation(request.operationId()),operatorId,operatorName)!=1)
if(mapper.seal(taskId,palletId,request.expectedVersion(),request.warehouseId(),operation(request.operationId()),operatorId,operatorName)!=1)
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
TaskRow refreshed=requireTask(taskId,request.warehouseId());
if(Objects.equals(refreshed.getCountedCartons(),refreshed.getTotalCartons()) &&
Objects.equals(refreshed.getAllocatedCartons(),refreshed.getTotalCartons()) && mapper.openPalletCount(taskId)==0){
if(mapper.completeTask(taskId)!=1 || mapper.completeInbound(taskId,refreshed.getTenantId(),refreshed.getInboundId(),operatorId)!=1)
throw conflict("组托完成状态已变化,请刷新后重试");
for(Long participatingTaskId:mapper.taskIdsByPallet(palletId)){
TaskRow participating=requireTask(participatingTaskId,request.warehouseId());
if(Objects.equals(participating.getCountedCartons(),participating.getTotalCartons()) &&
Objects.equals(participating.getAllocatedCartons(),participating.getTotalCartons()) && mapper.openPalletCount(participatingTaskId)==0){
if(mapper.completeTask(participatingTaskId)!=1 || mapper.completeInbound(participatingTaskId,participating.getTenantId(),participating.getInboundId(),operatorId)!=1)
throw conflict("组托完成状态已变化,请刷新后重试");
}
}
return view(requireTask(taskId,request.warehouseId()));
}
@@ -166,14 +168,14 @@ public class DropshipPalletizationService {
private PalletRow getOrCreatePallet(TaskRow task,String palletNo,String operation,long operatorId,String operatorName){
PalletRow existing=mapper.palletByNo(palletNo);
if(existing!=null){
if(!Objects.equals(existing.getTaskId(),task.getId()) || !Objects.equals(existing.getTenantId(),task.getTenantId())) throw conflict("托盘码已被其他客户或入仓任务占用");
if(!Objects.equals(existing.getWarehouseId(),task.getWarehouseId())) throw conflict("托盘码属于其他仓库");
if(!"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘已封托,不能继续绑定箱");
return existing;
}
try{ mapper.insertPallet(IdWorker.getId(),task.getId(),task.getTenantId(),task.getInboundId(),task.getWarehouseId(),palletNo,operation,operatorId,operatorName); }
catch(DuplicateKeyException e){ existing=mapper.palletByNo(palletNo); if(existing==null) throw conflict("托盘码已被并发占用"); }
existing=mapper.palletByNo(palletNo);
if(existing==null || !Objects.equals(existing.getTaskId(),task.getId())) throw conflict("托盘码已被其他任务占用");
if(existing==null || !Objects.equals(existing.getWarehouseId(),task.getWarehouseId()) || !"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘码已被其他仓库占用或已经封托");
return existing;
}
@@ -192,15 +194,15 @@ public class DropshipPalletizationService {
private Pallet palletView(PalletRow row,String inboundCode){
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonRow>> groups=new LinkedHashMap<>();
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),inboundCode,
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundId()+"|"+c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),first.getInboundCode(),
first.getInboundDetailId(),first.getSkuCode(),first.getCargoName(),values.size(),values.stream().mapToInt(v->v.getActualSkuQuantity()==null?0:v.getActualSkuQuantity()).sum(),
values.stream().map(CartonRow::getBoxCode).toList());}).toList();
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
PalletMetrics metrics=mapper.metrics(row.getId());
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),items.size(),discrepancies,
metrics==null?BigDecimal.ZERO:metrics.weightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
cartons.stream().map(c->cartonView(c,inboundCode,false,null)).toList(),row.getCompletedAt());
cartons.stream().map(c->cartonView(c,c.getInboundCode(),false,null)).toList(),row.getCompletedAt());
}
private Carton cartonView(CartonRow row,String inboundCode,boolean canCount,String blockReason){
@@ -67,12 +67,14 @@ public class LoadingCompletedOutboxService {
palletLabelService.prepare(planId, rows.get(0).getWarehouseId());
Map<Long, List<String>> cartonMarks = cartonLabelPools(rows);
List<PalletRow> palletRows = palletRows(planId);
Map<String, PalletManifest> palletManifests = palletManifests(palletRows);
List<com.baiyue.firstleg.mapper.LoadingPalletLabelMapper.LabelRow> standardLabels = palletLabelMapper.labels(planId);
return rows.stream().collect(java.util.stream.Collectors.groupingBy(Row::getAccountId,
java.util.TreeMap::new, java.util.stream.Collectors.toList()))
.entrySet().stream().map(entry -> envelope(entry.getKey(), entry.getValue(),
palletRows.stream().filter(item -> entry.getKey().equals(item.getAccountId())).toList(),
standardLabels.stream().filter(item -> entry.getKey().equals(item.getAccountId())).toList(), cartonMarks))
standardLabels.stream().filter(item -> entry.getKey().equals(item.getAccountId())).toList(), cartonMarks,
palletManifests))
.toList();
}
@@ -98,14 +100,16 @@ public class LoadingCompletedOutboxService {
private EventEnvelope envelope(long accountId, List<Row> rows, List<PalletRow> palletRows,
List<com.baiyue.firstleg.mapper.LoadingPalletLabelMapper.LabelRow> standardLabels,
Map<Long, List<String>> cartonMarks) {
Map<Long, List<String>> cartonMarks,
Map<String, PalletManifest> palletManifests) {
Row first = rows.get(0);
Map<Long, List<Row>> byInbound = rows.stream().collect(java.util.stream.Collectors.groupingBy(
Row::getSourceInboundId, java.util.TreeMap::new, java.util.stream.Collectors.toList()));
List<FirstLegLoadingPlanCompletedV2.Order> orders = byInbound.values().stream()
.map(value -> order(value, palletRows.stream()
.filter(item -> Objects.equals(item.getSourceInboundId(), value.get(0).getSourceInboundId())).toList(),
standardLabels.stream().filter(item -> Objects.equals(item.getInboundId(), value.get(0).getSourceInboundId())).toList(), cartonMarks)).toList();
standardLabels.stream().filter(item -> Objects.equals(item.getInboundId(), value.get(0).getSourceInboundId())).toList(),
cartonMarks, palletManifests)).toList();
var payload = new FirstLegLoadingPlanCompletedV2(String.valueOf(first.getLoadingPlanId()),
first.getPlanNo(), string(first.getContainerBookingId()),
ContainerNumbers.requireCanonical(first.getContainerNo()), first.getCabinetNo(),
@@ -123,7 +127,8 @@ public class LoadingCompletedOutboxService {
private FirstLegLoadingPlanCompletedV2.Order order(List<Row> values, List<PalletRow> palletRows,
List<com.baiyue.firstleg.mapper.LoadingPalletLabelMapper.LabelRow> standardLabels,
Map<Long, List<String>> cartonMarks) {
Map<Long, List<String>> cartonMarks,
Map<String, PalletManifest> palletManifests) {
List<Row> sorted = values.stream().sorted(Comparator.comparing(Row::getSourceInboundDetailId)).toList();
Row first = sorted.get(0);
List<FirstLegLoadingPlanCompletedV2.Item> items = sorted.stream().map(row ->
@@ -138,7 +143,7 @@ public class LoadingCompletedOutboxService {
String fulfillmentType = fulfillmentType(sorted);
String orderType = dropship(fulfillmentType) ? "ONE_CLICK_DROPSHIP" : "FIRST_LEG";
List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> pallets = dropship(fulfillmentType)
? palletSnapshots(sorted, palletRows) : standardLabels.stream()
? palletSnapshots(sorted, palletRows, palletManifests) : standardLabels.stream()
.map(label -> new FirstLegLoadingPlanCompletedV2.PalletSnapshot(label.getPalletCode(), List.of())).toList();
if ("ONE_CLICK_DROPSHIP".equals(orderType) && pallets.isEmpty()) {
throw new BizException(ResultCode.CONFLICT, "一键代发入仓单缺少托盘冻结快照");
@@ -257,10 +262,11 @@ public class LoadingCompletedOutboxService {
private record LabelPoolKey(long accountId, long inboundId) { }
private record InboundPalletKey(long accountId, long inboundId) { }
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source) {
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> palletSnapshots(List<Row> values, List<PalletRow> source,
Map<String, PalletManifest> manifests) {
if (source.isEmpty()) return List.of();
if (source.stream().anyMatch(item -> Boolean.TRUE.equals(item.getActualBinding()))) {
return actualPalletSnapshots(values, source);
return actualPalletSnapshots(values, source, manifests);
}
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
p -> (p.getAllocationNo() == null ? "" : p.getAllocationNo()) + "|" + p.getPalletCode(),
@@ -314,7 +320,7 @@ public class LoadingCompletedOutboxService {
/** 现场逐箱扫描已给出精确箱到托关系,不再使用旧的按托盘容量顺序推算。 */
private List<FirstLegLoadingPlanCompletedV2.PalletSnapshot> actualPalletSnapshots(
List<Row> values, List<PalletRow> source) {
List<Row> values, List<PalletRow> source, Map<String, PalletManifest> manifests) {
Map<Long, Row> details = values.stream().collect(java.util.stream.Collectors.toMap(
Row::getSourceInboundDetailId, value -> value, (left, right) -> left, LinkedHashMap::new));
Map<String, List<PalletRow>> byPallet = source.stream().collect(java.util.stream.Collectors.groupingBy(
@@ -334,14 +340,17 @@ public class LoadingCompletedOutboxService {
if (marks.size() != detailRows.getValue().size()) {
throw new BizException(ResultCode.CONFLICT, "现场托盘包含空箱唛或重复箱唛");
}
int quantity = detail.getProductQuantity() == null ? marks.size()
: (int) Math.round((double) detail.getProductQuantity() * marks.size()
/ Math.max(1, detail.getPackageCount()));
int quantity = detailRows.getValue().stream().map(PalletRow::getActualSkuQuantity)
.filter(Objects::nonNull).mapToInt(Integer::intValue).sum();
if (quantity <= 0) throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘缺少实点 SKU 数量");
skus.add(new FirstLegLoadingPlanCompletedV2.SkuPackageSnapshot(detail.getSourceInboundDetailId().toString(), detail.getSkuCode(),
detail.getProductNameZh(), marks.size(), quantity, marks));
actualPackages += marks.size();
}
result.add(new FirstLegLoadingPlanCompletedV2.PalletSnapshot(pallet.getKey(), skus));
PalletManifest manifest = manifests.get(pallet.getKey());
if (manifest == null) throw new BizException(ResultCode.CONFLICT, "一件代发托盘缺少完整清单摘要");
result.add(new FirstLegLoadingPlanCompletedV2.PalletSnapshot(pallet.getKey(), skus,
manifest.version(), manifest.packageCount(), manifest.fragmentCount(), manifest.hash()));
}
if (actualPackages != sumInt(values, Row::getPackageCount)) {
throw new BizException(ResultCode.CONFLICT, "一件代发现场托盘箱唛数与实装箱数不一致");
@@ -349,12 +358,32 @@ public class LoadingCompletedOutboxService {
return result;
}
private Map<String, PalletManifest> palletManifests(List<PalletRow> rows) {
Map<String, List<PalletRow>> grouped = rows.stream().filter(row -> Boolean.TRUE.equals(row.getActualBinding()))
.collect(java.util.stream.Collectors.groupingBy(PalletRow::getPalletCode, LinkedHashMap::new,
java.util.stream.Collectors.toList()));
Map<String, PalletManifest> result = new LinkedHashMap<>();
grouped.forEach((palletNo, values) -> {
List<PalletRow> sorted = values.stream().sorted(Comparator
.comparing(PalletRow::getAccountId).thenComparing(PalletRow::getSourceInboundId)
.thenComparing(PalletRow::getSourceInboundDetailId).thenComparing(PalletRow::getBoxCode)).toList();
String canonical = sorted.stream().map(value -> value.getAccountId() + "|" + value.getSourceInboundId()
+ "|" + value.getSourceInboundDetailId() + "|" + value.getSkuCode() + "|" + value.getBoxCode()
+ "|" + value.getActualSkuQuantity()).collect(java.util.stream.Collectors.joining("\n"));
int fragments = (int) sorted.stream().map(PalletRow::getAccountId).distinct().count();
result.put(palletNo, new PalletManifest(1, sorted.size(), fragments,
EventPayloadHash.sha256(canonical.getBytes(StandardCharsets.UTF_8))));
});
return result;
}
private static boolean dropship(String value) {
return "ONE_CLICK_DROPSHIP".equals(value);
}
private record DetailSnapshot(Long detailId, String skuCode, String productName,
Integer packageCount, Integer productQuantity, List<String> marks) {}
private record PalletManifest(int version, int packageCount, int fragmentCount, String hash) {}
private void insert(EventEnvelope envelope) {
InboundOutboxEntity value = new InboundOutboxEntity();