Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a13586aff0 | ||
|
|
6c59525dec |
+59
-24
@@ -1,30 +1,65 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchBolData;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
/** BOL按真实明细客户逐行读取整车,不依赖已移除的主单客户头。 */
|
||||
|
||||
/** BOL读取原调度逐来源数量及真实预约分配,扫码进度和订单商品JOIN均不放大或缩小箱数。 */
|
||||
@Mapper
|
||||
public interface DispatchBolMapper {
|
||||
@Select("SELECT di.customer_id AS customerId,di.destination_type AS destinationType,di.delivery_address AS deliveryAddress,di.postal_code AS postalCode," +
|
||||
"COALESCE(MAX(NULLIF(o.container_no,'')),MAX(NULLIF(ip.container_no,''))) AS containerNo," +
|
||||
"MAX(NULLIF(di.pallet_no,'')) AS palletNo," +
|
||||
"COALESCE(MAX(NULLIF(l.destination_warehouse_code,'')),MAX(NULLIF(di.fba_warehouse_code,''))," +
|
||||
"MAX(NULLIF(di.destination_warehouse_code,''))) AS fbaWarehouseCode," +
|
||||
"COALESCE(MAX(NULLIF(l.destination_warehouse_name,'')),MAX(NULLIF(di.destination_warehouse_name,''))) AS destinationWarehouseName," +
|
||||
"COALESCE(MAX(NULLIF(l.inbound_code,'')),MAX(NULLIF(di.inbound_code,''))) AS warehouseNo," +
|
||||
"GROUP_CONCAT(DISTINCT NULLIF(od.shipment_no,'') ORDER BY od.shipment_no SEPARATOR ', ') AS fbaShipmentId," +
|
||||
"GROUP_CONCAT(DISTINCT NULLIF(od.fba_tracking_no,'') ORDER BY od.fba_tracking_no SEPARATOR ', ') AS poReferenceNo," +
|
||||
"CASE WHEN MAX(l.id) IS NULL THEN COALESCE(MAX(di.package_count),0) " +
|
||||
"WHEN COALESCE(MAX(a.picked_package_count),0)>0 THEN MAX(a.picked_package_count) " +
|
||||
"ELSE COALESCE(MAX(a.planned_package_count),0) END AS packageCount " +
|
||||
"FROM ll_dispatch_item di " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet p ON p.id=di.plan_pallet_id " +
|
||||
"LEFT JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id AND di.return_receipt_id IS NULL AND COALESCE(di.source_business_type,'')<>'RELABEL_ORDER' " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id AND l.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id AND o.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_order_detail od ON od.order_id=l.order_id AND od.customer_id=di.customer_id " +
|
||||
"WHERE di.dispatch_order_id=#{dispatchOrderId} " +
|
||||
"GROUP BY di.id,di.customer_id,di.destination_type,di.delivery_address,di.postal_code,l.id ORDER BY di.id,l.id")
|
||||
List<DispatchBolData.Line> findWholeDispatch(@Param("dispatchOrderId") Long dispatchOrderId);
|
||||
record Cargo(Long dispatchItemId,Long orderId,Long planPalletId,String customerId,String destinationType,
|
||||
String deliveryAddress,String postalCode,String containerNo,String palletNo,String fbaWarehouseCode,
|
||||
String destinationWarehouseName,String warehouseNo,String fbaShipmentId,String poReferenceNo,
|
||||
Integer packageCount,Integer itemPackageCount,boolean originalPlan) { }
|
||||
record Allocation(Long dispatchItemId,Long planPalletId,String batchKey,String customerId,String sourceKey,
|
||||
String destinationKey,Integer packageCount,Long appointmentId,Long orderId,String confirmationNo) { }
|
||||
|
||||
/** 一行对应本车一个真实计划来源行;商品明细只聚合展示字段,原调度数量不取部分已拣数。 */
|
||||
@Select("""
|
||||
SELECT di.id AS dispatchItemId,
|
||||
CASE WHEN di.plan_pallet_id IS NOT NULL AND di.return_receipt_id IS NULL
|
||||
AND COALESCE(di.source_business_type,'')<>'RELABEL_ORDER' THEN l.order_id ELSE di.order_id END AS orderId,
|
||||
di.plan_pallet_id AS planPalletId,di.customer_id AS customerId,di.destination_type AS destinationType,
|
||||
di.delivery_address AS deliveryAddress,di.postal_code AS postalCode,
|
||||
COALESCE(MAX(NULLIF(o.container_no,'')),MAX(NULLIF(ip.container_no,''))) AS containerNo,
|
||||
MAX(NULLIF(di.pallet_no,'')) AS palletNo,
|
||||
COALESCE(MAX(NULLIF(l.fba_warehouse_code,'')),MAX(NULLIF(l.destination_warehouse_code,'')),
|
||||
MAX(NULLIF(di.fba_warehouse_code,'')),MAX(NULLIF(di.destination_warehouse_code,''))) AS fbaWarehouseCode,
|
||||
COALESCE(MAX(NULLIF(l.destination_warehouse_name,'')),MAX(NULLIF(di.destination_warehouse_name,''))) AS destinationWarehouseName,
|
||||
COALESCE(MAX(NULLIF(l.inbound_code,'')),MAX(NULLIF(di.inbound_code,''))) AS warehouseNo,
|
||||
GROUP_CONCAT(DISTINCT NULLIF(od.shipment_no,'') ORDER BY od.shipment_no SEPARATOR ', ') AS fbaShipmentId,
|
||||
GROUP_CONCAT(DISTINCT NULLIF(od.fba_tracking_no,'') ORDER BY od.fba_tracking_no SEPARATOR ', ') AS poReferenceNo,
|
||||
CASE WHEN l.id IS NULL THEN MAX(di.package_count)
|
||||
WHEN di.source_stage='RECEIVED' THEN MAX(a.counted_package_count)
|
||||
ELSE MAX(a.planned_package_count) END AS packageCount,
|
||||
MAX(di.package_count) AS itemPackageCount,
|
||||
CASE WHEN di.plan_pallet_id IS NOT NULL AND di.return_receipt_id IS NULL
|
||||
AND COALESCE(di.source_business_type,'')<>'RELABEL_ORDER' THEN TRUE ELSE FALSE END AS originalPlan
|
||||
FROM ll_dispatch_item di
|
||||
LEFT JOIN ll_inbound_plan_pallet p ON p.id=di.plan_pallet_id
|
||||
LEFT JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=di.customer_id
|
||||
LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id AND a.customer_id=di.customer_id
|
||||
AND di.return_receipt_id IS NULL AND COALESCE(di.source_business_type,'')<>'RELABEL_ORDER'
|
||||
LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id AND l.customer_id=di.customer_id AND l.plan_id=p.plan_id
|
||||
LEFT JOIN ll_order o ON o.id=l.order_id AND o.customer_id=di.customer_id
|
||||
LEFT JOIN ll_order_detail od ON od.order_id=l.order_id AND od.customer_id=l.customer_id
|
||||
WHERE di.dispatch_order_id=#{dispatchOrderId}
|
||||
GROUP BY di.id,di.order_id,di.plan_pallet_id,di.customer_id,di.destination_type,di.delivery_address,
|
||||
di.postal_code,di.source_stage,di.return_receipt_id,di.source_business_type,l.id,l.order_id,a.id
|
||||
ORDER BY di.id,l.id,a.id
|
||||
""")
|
||||
List<Cargo> findWholeDispatch(@Param("dispatchOrderId") long dispatchOrderId);
|
||||
|
||||
/** 原batch+客户+稳定来源+目的地定位预约货物;BOOKING使用持久化order_id,绝不自行拼ORDER键。 */
|
||||
@Select("""
|
||||
SELECT a.dispatch_item_id AS dispatchItemId,a.plan_pallet_id AS planPalletId,a.batch_key AS batchKey,
|
||||
a.customer_id AS customerId,a.source_key AS sourceKey,a.destination_key AS destinationKey,
|
||||
a.package_count AS packageCount,p.id AS appointmentId,i.order_id AS orderId,p.confirmation_no AS confirmationNo
|
||||
FROM ll_dispatch_appointment_allocation a
|
||||
LEFT JOIN ll_inbound_fba_appointment p ON a.batch_key=CONCAT('FBA_APPOINTMENT:',p.id)
|
||||
LEFT JOIN ll_inbound_fba_appointment_item i ON i.appointment_id=p.id AND i.customer_id=a.customer_id
|
||||
AND i.source_key=a.source_key AND CONCAT('FBA:',i.fba_warehouse_code)=a.destination_key
|
||||
WHERE a.dispatch_id=#{dispatchOrderId} AND a.batch_key LIKE 'FBA_APPOINTMENT:%'
|
||||
ORDER BY a.dispatch_item_id,a.customer_id,a.source_key,a.destination_key,a.batch_key
|
||||
""")
|
||||
List<Allocation> findAppointmentAllocations(@Param("dispatchOrderId") long dispatchOrderId);
|
||||
}
|
||||
|
||||
+9
-5
@@ -3,13 +3,17 @@ import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
/** 打印服务内部一致快照,不恢复旧排车JSON协议。 */
|
||||
public record DispatchBolData(String dispatchNo,String overseasWarehouseId,String overseasWarehouseName,String carrierName,
|
||||
String vehiclePlate,LocalDateTime estimatedArrivalAt,List<Line> bolLines,Map<String,String> appointmentNumbers) {
|
||||
public record Line(String customerId,String destinationType,String deliveryAddress,String postalCode,String containerNo,
|
||||
String vehiclePlate,LocalDateTime estimatedArrivalAt,List<Line> bolLines) {
|
||||
public record Line(Long dispatchItemId,Long orderId,String customerId,String destinationType,String deliveryAddress,String postalCode,String containerNo,
|
||||
String palletNo,String fbaWarehouseCode,String destinationWarehouseName,String warehouseNo,String fbaShipmentId,
|
||||
String poReferenceNo,int packageCount) {
|
||||
String poReferenceNo,int packageCount,String appointmentNo) {
|
||||
/** 外部确认号只去首尾空白,未取得真实约号时保留空。 */
|
||||
public Line { appointmentNo=appointmentNo==null || appointmentNo.isBlank()?null:appointmentNo.strip(); }
|
||||
/** 目的地身份不使用客户作为分组条件。 */
|
||||
public String destinationKey(){return com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(destinationType,fbaWarehouseCode,null,deliveryAddress,postalCode);}
|
||||
/** 打印展示使用业务名称或真实地址。 */
|
||||
public String destinationLabel(){return "PRIVATE".equals(destinationType)?deliveryAddress:destinationWarehouseName;}
|
||||
public String groupKey(){return customerId+"\u0000"+destinationKey();}
|
||||
/** 同目的地同约号跨客户合并,无号也可组成同目的地文档。 */
|
||||
public String groupKey(){return destinationKey()+"\u0000"+Objects.requireNonNullElse(appointmentNo,"");}
|
||||
}
|
||||
public String appointmentFor(Line line){return appointmentNumbers.get(line.groupKey());}
|
||||
}
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** BOL专用只读事实入口,应用服务不直接持有Mapper。 */
|
||||
@Repository
|
||||
public class DispatchBolRepository {
|
||||
private final DispatchBolMapper mapper;
|
||||
/** 装配同一次事务使用的来源及分配读取。 */
|
||||
public DispatchBolRepository(DispatchBolMapper mapper){this.mapper=mapper;}
|
||||
/** 返回完整明细身份、原调度箱数及逐来源打印资料。 */
|
||||
public List<DispatchBolMapper.Cargo> cargo(long dispatchId){return mapper.findWholeDispatch(dispatchId);}
|
||||
/** 仅正式FBA预约分配有外部预约号;客户仓内部DISPATCH批次不伪造约号。 */
|
||||
public List<DispatchBolMapper.Allocation> allocations(long dispatchId){return mapper.findAppointmentAllocations(dispatchId);}
|
||||
}
|
||||
+9
-2
@@ -369,7 +369,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var row=lockPlatform(actor,number);var identity=identity(actor,number,Action.SUBMIT_REVIEW,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
requireEditing(row,request.recordVersion());
|
||||
if(!Set.of(ReviewState.DRAFT.name(),ReviewState.DISPATCH_REJECTED.name(),ReviewState.LOADING_REJECTED.name()).contains(row.getReviewState()))throw conflict("当前审核已提交,请刷新核对");
|
||||
if(!canSubmitReview(row.getReviewState()))throw conflict("当前审核已提交,请刷新核对");
|
||||
verifyReviewResources(row);
|
||||
if(n(row.getPackageCount())<=0 || repository.cargo(row.getId()).isEmpty())throw bad("空排车不能提交审核");
|
||||
long round=row.getReviewRound()+1;String basis=basis(row);
|
||||
@@ -739,6 +739,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
if(!Status.ACTIVE.name().equals(row.getStatus()) || !Stage.BEFORE_LOADING.name().equals(row.getStage()) || n(row.getPickedPackageCount())>0 || n(row.getShippedPackageCount())>0) throw conflict("仅装车前且未扫码的有效排车可以调整货物");
|
||||
}
|
||||
|
||||
/** 提交资格和写入共用可重提状态,已进入两级待审的整车不得再次显示提交入口。 */
|
||||
private static boolean canSubmitReview(String reviewState) {
|
||||
return ReviewState.DRAFT.name().equals(reviewState) || ReviewState.DISPATCH_REJECTED.name().equals(reviewState)
|
||||
|| ReviewState.LOADING_REJECTED.name().equals(reviewState);
|
||||
}
|
||||
|
||||
/** 动作快照只反映实际当前资格,写入仍须再次加锁校验。 */
|
||||
Map<Action,Gate> actions(Actor actor,DispatchAggregate row) {
|
||||
var gates=new EnumMap<Action,Gate>(Action.class); boolean active=Status.ACTIVE.name().equals(row.getStatus());
|
||||
@@ -746,7 +752,8 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
for(var action:Action.values()) {
|
||||
boolean state=switch(action) {
|
||||
case CREATE -> actor.mode()==ActorMode.PLATFORM;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL,SUBMIT_REVIEW -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
case EDIT,ADD_CARGO,REMOVE_CARGO,CANCEL -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0;
|
||||
case SUBMIT_REVIEW -> active && stage==Stage.BEFORE_LOADING && n(row.getPickedPackageCount())==0 && canSubmitReview(row.getReviewState());
|
||||
case DISPATCH_REVIEW -> active && review==ReviewState.DISPATCH_PENDING;
|
||||
case LOADING_REVIEW -> active && review==ReviewState.LOADING_PENDING;
|
||||
case RETURN_TO_REVIEW -> active && stage==Stage.LOADING && n(row.getPickedPackageCount())==0;
|
||||
|
||||
+3
-1
@@ -32,6 +32,7 @@ public class DispatchBolPrintService {
|
||||
@Resource private DomesticWarehouseDirectoryClient warehouses;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
/** 按本车一致快照组装全部BOL,一次调用打印服务并保持一个PDF下载。 */
|
||||
public GeneratedPdf generate(String dispatchNo, String requestId) {
|
||||
DispatchBolData detail = dispatches.read(dispatchNo);
|
||||
if (detail.bolLines() == null || detail.bolLines().isEmpty()) {
|
||||
@@ -54,6 +55,7 @@ public class DispatchBolPrintService {
|
||||
}
|
||||
}
|
||||
|
||||
/** 将同目的地同预约号的跨客户货物组装为独立分页BOL,无预约号时ISA保持空白。 */
|
||||
private List<PrintPdfClient.Document> documents(DispatchBolData detail) {
|
||||
String shipFromAddress = warehouseAddress(detail.overseasWarehouseId());
|
||||
Map<String, List<DispatchBolData.Line>> groups = new LinkedHashMap<>();
|
||||
@@ -72,7 +74,7 @@ public class DispatchBolPrintService {
|
||||
.map(line -> new PrintPdfClient.CargoRow(text(line.containerNo()), text(line.palletNo()), text(line.fbaWarehouseCode()),
|
||||
text(line.warehouseNo()), text(line.fbaShipmentId()), text(line.poReferenceNo()), line.packageCount())).toList();
|
||||
documents.add(new PrintPdfClient.Document(text(detail.overseasWarehouseName()), shipFromAddress, text(detail.carrierName()),
|
||||
text(detail.appointmentFor(lines.get(0))), dateTime(detail.estimatedArrivalAt()), text(detail.vehiclePlate()), "—",
|
||||
lines.get(0).appointmentNo() == null ? "" : lines.get(0).appointmentNo().strip(), dateTime(detail.estimatedArrivalAt()), text(detail.vehiclePlate()), "—",
|
||||
text(lines.get(0).fbaWarehouseCode()), destination, "—", text(detail.dispatchNo()), unique(lines, true), unique(lines, false),
|
||||
total, page + 1, pages, rows));
|
||||
offset = end;
|
||||
|
||||
+71
-10
@@ -1,31 +1,92 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchBolData;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchBolRepository;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Resources;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.*;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
/** 先在本地一致事务读全车打印事实,事务结束后才调用远程打印。 */
|
||||
|
||||
/** 同一只读事务内核实本车逐来源数量,再按真实预约份额拆行;事务结束后才调用远程打印。 */
|
||||
@Service
|
||||
public class DispatchBolReadService {
|
||||
private final DispatchAggregateRepository roots;private final DispatchBolMapper lines;private final ObjectMapper json;
|
||||
public DispatchBolReadService(DispatchAggregateRepository roots,DispatchBolMapper lines,ObjectMapper json){this.roots=roots;this.lines=lines;this.json=json;}
|
||||
private final DispatchAggregateRepository roots;private final DispatchBolRepository facts;private final ObjectMapper json;
|
||||
private record Source(Long itemId,String customerId,Long orderId,String destinationKey) { }
|
||||
private record AllocationKey(Long itemId,String batchKey,String customerId,String sourceKey,String destinationKey) { }
|
||||
/** 打印读取只经Repository,不写预约、调度或库存事实。 */
|
||||
public DispatchBolReadService(DispatchAggregateRepository roots,DispatchBolRepository facts,ObjectMapper json){this.roots=roots;this.facts=facts;this.json=json;}
|
||||
|
||||
/** 保留原调度总量,未约部分以空约号打印,不按部分拣货进度缩量。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public DispatchBolData read(String number) {
|
||||
var row=roots.find(number);if(row==null)throw new BizException(ResultCode.NOT_FOUND,"排车单不存在");
|
||||
Resources resources;try{resources=json.readValue(row.getResourcesJson(),Resources.class);}catch(com.fasterxml.jackson.core.JsonProcessingException e){throw new IllegalStateException("本车资源快照损坏",e);}
|
||||
var cargo=lines.findWholeDispatch(row.getId());
|
||||
if(cargo.stream().anyMatch(l->l.customerId()==null || l.destinationKey()==null || l.packageCount()<=0))throw new BizException(ResultCode.VALIDATION_FAILED,"BOL货物客户、目的地或箱数尚未核实");
|
||||
var confirmations=new LinkedHashMap<String,LinkedHashSet<String>>();
|
||||
for(var batch:roots.batches(row.getId()))if(batch.confirmationNo()!=null && !batch.confirmationNo().isBlank())confirmations.computeIfAbsent(batch.customerId()+"\u0000"+batch.destinationKey(),k->new LinkedHashSet<>()).add(batch.confirmationNo());
|
||||
var numbers=confirmations.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey,e->String.join(", ",e.getValue())));
|
||||
var cargo=new LinkedHashMap<Source,DispatchBolData.Line>();var totals=new HashMap<Long,Integer>();var pallets=new HashMap<Long,Long>();
|
||||
for(var raw:facts.cargo(row.getId())) {
|
||||
int packages=nonnegative(raw.packageCount());var line=line(raw,null,packages);
|
||||
if(raw.dispatchItemId()==null || blank(raw.customerId()) || line.destinationKey()==null
|
||||
|| raw.originalPlan() && raw.orderId()==null)throw invalid();
|
||||
int total=positive(raw.itemPackageCount());var prior=totals.putIfAbsent(raw.dispatchItemId(),total);
|
||||
if(prior!=null && prior!=total)throw invalid();
|
||||
if(pallets.containsKey(raw.dispatchItemId()) && !Objects.equals(pallets.get(raw.dispatchItemId()),raw.planPalletId()))throw invalid();
|
||||
pallets.put(raw.dispatchItemId(),raw.planPalletId());
|
||||
if(packages==0)continue;
|
||||
var key=new Source(raw.dispatchItemId(),raw.customerId(),raw.orderId(),line.destinationKey());
|
||||
cargo.merge(key,line,DispatchBolReadService::merge);
|
||||
}
|
||||
var sourceTotals=new HashMap<Long,Integer>();
|
||||
cargo.forEach((key,line)->sourceTotals.merge(key.itemId(),line.packageCount(),Math::addExact));
|
||||
if(!sourceTotals.equals(totals))throw invalid();
|
||||
var remaining=new LinkedHashMap<Source,Integer>();cargo.forEach((key,line)->remaining.put(key,line.packageCount()));
|
||||
var seen=new HashSet<AllocationKey>();var result=new ArrayList<DispatchBolData.Line>();
|
||||
for(var allocation:facts.allocations(row.getId())) {
|
||||
if(allocation.appointmentId()==null || allocation.orderId()==null || blank(allocation.sourceKey())
|
||||
|| !Objects.equals(pallets.get(allocation.dispatchItemId()),allocation.planPalletId()))throw invalid();
|
||||
var key=new Source(allocation.dispatchItemId(),allocation.customerId(),allocation.orderId(),allocation.destinationKey());
|
||||
var source=cargo.get(key);int count=positive(allocation.packageCount());
|
||||
if(source==null || !seen.add(new AllocationKey(allocation.dispatchItemId(),allocation.batchKey(),allocation.customerId(),allocation.sourceKey(),allocation.destinationKey()))
|
||||
|| count>remaining.get(key))throw invalid();
|
||||
remaining.put(key,remaining.get(key)-count);result.add(copy(source,count,allocation.confirmationNo()));
|
||||
}
|
||||
// 每个来源只消费实际分配份额,余额有明确实物依据,保持空ISA而非重复整托。
|
||||
remaining.forEach((key,count)->{if(count>0)result.add(copy(cargo.get(key),count,null));});
|
||||
var printedTotals=new HashMap<Long,Integer>();result.forEach(line->printedTotals.merge(line.dispatchItemId(),line.packageCount(),Math::addExact));
|
||||
if(!printedTotals.equals(totals))throw invalid();
|
||||
return new DispatchBolData(row.getDispatchNo(),row.getOverseasWarehouseId(),row.getOverseasWarehouseName(),resources.vehicle()==null?null:resources.vehicle().carrierName(),resources.vehicle()==null?null:resources.vehicle().plate(),
|
||||
row.getEstimatedArrivalAt()==null?null:LocalDateTime.ofInstant(row.getEstimatedArrivalAt().toInstant(ZoneOffset.UTC),ZoneId.systemDefault()),List.copyOf(cargo),Map.copyOf(numbers));
|
||||
row.getEstimatedArrivalAt()==null?null:LocalDateTime.ofInstant(row.getEstimatedArrivalAt().toInstant(ZoneOffset.UTC),ZoneId.systemDefault()),List.copyOf(result));
|
||||
}
|
||||
/** 将数据库原始来源行转换为带真实身份的内部打印行。 */
|
||||
private static DispatchBolData.Line line(DispatchBolMapper.Cargo c,String appointment,int packages) {
|
||||
return new DispatchBolData.Line(c.dispatchItemId(),c.orderId(),c.customerId(),c.destinationType(),c.deliveryAddress(),c.postalCode(),c.containerNo(),c.palletNo(),
|
||||
c.fbaWarehouseCode(),c.destinationWarehouseName(),c.warehouseNo(),c.fbaShipmentId(),c.poReferenceNo(),packages,appointment);
|
||||
}
|
||||
/** 拆份只改变份额和预约号,真实客户、源订单和排车明细身份不变。 */
|
||||
private static DispatchBolData.Line copy(DispatchBolData.Line c,int packages,String appointment) {
|
||||
return new DispatchBolData.Line(c.dispatchItemId(),c.orderId(),c.customerId(),c.destinationType(),c.deliveryAddress(),c.postalCode(),c.containerNo(),c.palletNo(),
|
||||
c.fbaWarehouseCode(),c.destinationWarehouseName(),c.warehouseNo(),c.fbaShipmentId(),c.poReferenceNo(),packages,appointment);
|
||||
}
|
||||
/** 同订单多来源行先守恒合并,再让每条预约分配消费一次;展示字符串只去重合并。 */
|
||||
private static DispatchBolData.Line merge(DispatchBolData.Line a,DispatchBolData.Line b) {
|
||||
return new DispatchBolData.Line(a.dispatchItemId(),a.orderId(),a.customerId(),a.destinationType(),a.deliveryAddress(),a.postalCode(),join(a.containerNo(),b.containerNo()),a.palletNo(),
|
||||
a.fbaWarehouseCode(),a.destinationWarehouseName(),join(a.warehouseNo(),b.warehouseNo()),join(a.fbaShipmentId(),b.fbaShipmentId()),join(a.poReferenceNo(),b.poReferenceNo()),Math.addExact(a.packageCount(),b.packageCount()),null);
|
||||
}
|
||||
/** 仅打印展示字段去重,不用逗号字符串表示预约身份。 */
|
||||
private static String join(String a,String b) {
|
||||
var values=new LinkedHashSet<String>();for(var text:Arrays.asList(a,b))if(text!=null)for(var part:text.split(","))if(!part.isBlank())values.add(part.strip());
|
||||
return values.isEmpty()?null:String.join(", ",values);
|
||||
}
|
||||
/** 数量未知或非正不得伪造打印份额。 */
|
||||
private static int positive(Integer value){if(value==null || value<=0)throw invalid();return value;}
|
||||
/** 已核实为零的源行没有待打印货物,但不以零替代未知数量。 */
|
||||
private static int nonnegative(Integer value){if(value==null || value<0)throw invalid();return value;}
|
||||
/** 来源身份必须来自真实持久事实。 */
|
||||
private static boolean blank(String value){return value==null || value.isBlank();}
|
||||
/** 不完整、跨客户或超额分配不能生成看似守恒的BOL。 */
|
||||
private static BizException invalid(){return new BizException(ResultCode.VALIDATION_FAILED,"BOL货物来源、预约归属或分配箱数尚未核实");}
|
||||
}
|
||||
|
||||
@@ -874,7 +874,7 @@ paths:
|
||||
/api/platform/v1/last-leg/dispatches/{dispatchNo}/prints/bol:
|
||||
get:
|
||||
summary: 生成排车 BOL PDF
|
||||
description: 权限 platform:overseas-warehouse:dispatch:list;服务端按目的 FBA 仓分组并分页生成固定 A4 PDF,浏览器不直接访问内部打印服务。
|
||||
description: 权限 platform:overseas-warehouse:dispatch:list;服务端按目的地与预约号归集BOL,同目的地同预约号跨客户合并,不同预约号分别生成BOL并独立分页;无预约号也能打印且预约栏留空,同目的地空号合并。本次仍返回包含各份BOL的一个固定A4 PDF,浏览器不直接访问内部打印服务,打印不改变货物客户归属。
|
||||
security: [{ bearerAuth: [] }]
|
||||
parameters: [{ name: dispatchNo, in: path, required: true, schema: { type: string } }]
|
||||
responses:
|
||||
|
||||
+58
@@ -287,6 +287,64 @@ public class DispatchAggregatePersistenceTest {
|
||||
assertThatThrownBy(()->f.tx.execute(s->f.service.loadingComplete(f.actor,created.dispatchNo(),new DispatchCommands.LoadingComplete(approved.recordVersion(),"missing-rejected",preview.manifestHash(),List.of(),null)))).isInstanceOf(BizException.class).hasMessageContaining("核平");
|
||||
assertThat(f.db.queryForObject("SELECT stage FROM ll_dispatch_order",String.class)).isEqualTo("LOADING");
|
||||
}
|
||||
/** 已提交根的公开资格关闭;新键重复提交不改变事实,原键仍先于当前状态恢复结果。 */
|
||||
@Test void pendingSubmissionGateRejectsNewKeyWithoutMutationAndRestoresOriginalCommand() throws Exception {
|
||||
var security=org.springframework.security.core.context.SecurityContextHolder.getContext();var previous=security.getAuthentication();
|
||||
security.setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("operator-test","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit"));
|
||||
try {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("submission-gate-create",List.of(source))));
|
||||
var request=new DispatchCommands.SubmitReview(created.recordVersion(),"submission-gate-original",null);
|
||||
var submitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),request));
|
||||
var detail=f.tx.execute(s->f.service.detail(f.actor,created.dispatchNo()));
|
||||
assertThat(detail.reviewState()).isEqualTo(ReviewState.DISPATCH_PENDING);
|
||||
assertThat(detail.actions().get(Action.SUBMIT_REVIEW).allowed()).isFalse();
|
||||
var before=f.json.writeValueAsString(f.repository.find(created.dispatchNo()));
|
||||
var commands=f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id");
|
||||
assertThatThrownBy(()->f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),
|
||||
new DispatchCommands.SubmitReview(detail.recordVersion(),"submission-gate-duplicate",null))))
|
||||
.isInstanceOf(BizException.class).hasMessageContaining("当前审核已提交");
|
||||
assertThat(f.json.writeValueAsString(f.repository.find(created.dispatchNo()))).isEqualTo(before);
|
||||
assertThat(f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id")).isEqualTo(commands);
|
||||
var replay=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),request));
|
||||
assertThat(replay.replayed()).isTrue();assertThat(replay.recordVersion()).isEqualTo(submitted.recordVersion());
|
||||
assertThat(replay.result()).isEqualTo(submitted.result());
|
||||
assertThat(f.json.writeValueAsString(f.repository.find(created.dispatchNo()))).isEqualTo(before);
|
||||
assertThat(f.db.queryForList("SELECT id,idempotency_key,record_version,command_hash FROM ll_dispatch_command ORDER BY id")).isEqualTo(commands);
|
||||
} finally { security.setAuthentication(previous); }
|
||||
}
|
||||
|
||||
/** 真实两级审核流证明三种可重提状态开放,而两种待审状态均关闭提交资格。 */
|
||||
@Test void submissionActionMatchesDraftRejectedAndPendingReviewStates() throws Exception {
|
||||
var security=org.springframework.security.core.context.SecurityContextHolder.getContext();var previous=security.getAuthentication();
|
||||
security.setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("operator-test","",
|
||||
"platform:overseas-warehouse:dispatch:list","platform:overseas-warehouse:dispatch:edit",
|
||||
"platform:fleet:trip:dispatch-review","platform:fleet:trip:loading-review"));
|
||||
try {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
var created=f.tx.execute(s->f.service.create(f.actor,f.configured("submission-states-create",List.of(source))));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DRAFT,true);
|
||||
var submitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(created.recordVersion(),"submission-states-1",null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_PENDING,false);
|
||||
var rejected=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(submitted.recordVersion(),"submission-states-dispatch-rejected",Decision.REJECTED,"核对调度")));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_REJECTED,true);
|
||||
var resubmitted=f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(rejected.recordVersion(),"submission-states-2",null)));
|
||||
var approved=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),false,new DispatchCommands.ReviewCommand(resubmitted.recordVersion(),"submission-states-dispatch-approved",Decision.APPROVED,null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.LOADING_PENDING,false);
|
||||
var loadingRejected=f.tx.execute(s->f.service.review(f.actor,created.dispatchNo(),true,new DispatchCommands.ReviewCommand(approved.recordVersion(),"submission-states-loading-rejected",Decision.REJECTED,"核对装载")));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.LOADING_REJECTED,true);
|
||||
f.tx.execute(s->f.service.submitReview(f.actor,created.dispatchNo(),new DispatchCommands.SubmitReview(loadingRejected.recordVersion(),"submission-states-3",null)));
|
||||
assertSubmissionGate(f,created.dispatchNo(),ReviewState.DISPATCH_PENDING,false);
|
||||
} finally { security.setAuthentication(previous); }
|
||||
}
|
||||
|
||||
/** 从真实详情DTO读取动作资格,避免只测内部枚举或权限缺失导致的假阴性。 */
|
||||
private static void assertSubmissionGate(Fixture f,String number,ReviewState state,boolean allowed) {
|
||||
var detail=f.tx.execute(s->f.service.detail(f.actor,number));
|
||||
assertThat(detail.reviewState()).isEqualTo(state);assertThat(detail.actions().get(Action.SUBMIT_REVIEW).allowed()).isEqualTo(allowed);
|
||||
}
|
||||
|
||||
/** 两级顺序、驳回重提、原结果恢复和零扫码退回均实际提交到同一整车。 */
|
||||
@Test void reviewsUseActualRootRoundAndImmutableCommandsBeforeAndAfterLoading() throws Exception {
|
||||
var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);
|
||||
|
||||
+87
-13
@@ -10,7 +10,7 @@ import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
|
||||
import com.baiyue.lastleg.client.PrintPdfClient;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchBolData;
|
||||
import java.util.Map;
|
||||
import java.util.ArrayList;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
@@ -33,11 +33,13 @@ class DispatchBolPrintServiceTest {
|
||||
@InjectMocks private DispatchBolPrintService service;
|
||||
|
||||
@BeforeEach
|
||||
/** 设置本次受控打印调用的内部身份。 */
|
||||
void setUp() {
|
||||
ReflectionTestUtils.setField(service, "internalToken", "internal-test-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 核对首页十八行及续页二十六行的全部分页边界。 */
|
||||
void usesDenseFirstAndContinuationPageCapacities() {
|
||||
assertThat(DispatchBolPrintService.pageCount(1)).isEqualTo(1);
|
||||
assertThat(DispatchBolPrintService.pageCount(18)).isEqualTo(1);
|
||||
@@ -49,10 +51,11 @@ class DispatchBolPrintServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 保留从实际起运仓主档解析营业地址的打印行为。 */
|
||||
void resolvesTheOperatingWarehouseAddressIntoTheBolRequest() {
|
||||
String warehouseId = "193000000000200003";
|
||||
DispatchBolData detail = new DispatchBolData("DS-WH-20260911-624449",warehouseId,"洛杉矶仓","五月开发验收车队",null,
|
||||
LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2",1)),Map.of());
|
||||
LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2",1)));
|
||||
var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga",
|
||||
null, "91730", "10767 7th Street", null);
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
@@ -74,10 +77,11 @@ class DispatchBolPrintServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
/** 无预约号时不同目的仓仍分别生成各自BOL。 */
|
||||
void createsOneBolDocumentPerFbaWarehouse() {
|
||||
String warehouseId = "193000000000200003";
|
||||
DispatchBolData detail = new DispatchBolData("DS-WH-20260911-624449",warehouseId,"洛杉矶仓","五月开发验收车队",null,
|
||||
LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2 Warehouse",1),line("102","ONT8","ONT8 Warehouse",1)),Map.of());
|
||||
LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2 Warehouse",1),line("102","ONT8","ONT8 Warehouse",1)));
|
||||
var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L,
|
||||
"US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE", null, null);
|
||||
when(dispatches.read(detail.dispatchNo())).thenReturn(detail);
|
||||
@@ -96,21 +100,91 @@ class DispatchBolPrintServiceTest {
|
||||
.containsExactly("ABE2", "ONT8");
|
||||
}
|
||||
@Test
|
||||
void keepsTwoCustomersAtTheSameDestinationInSeparateBolDocuments() {
|
||||
var first=line("101","ABE2","ABE2 Warehouse",3);var second=line("102","ABE2","ABE2 Warehouse",5);
|
||||
var detail=new DispatchBolData("DS-ONE-TRUCK","9","Operating Warehouse",null,null,null,List.of(first,second),
|
||||
Map.of(first.groupKey(),"CONF-A",second.groupKey(),"CONF-B"));
|
||||
/** 同仓同号合并客户,同时保留各自入仓码、托盘与箱数。 */
|
||||
void mergesCustomersWithTheSameDestinationAndAppointmentWithoutLosingTheirCargo() {
|
||||
var request=render(List.of(cargo(11,"101","ABE2","IN-A",3,"CONF-SHARED"),cargo(12,"102","ABE2","IN-B",5,"CONF-SHARED")),1);
|
||||
assertThat(request.documents()).singleElement().satisfies(document->{
|
||||
assertThat(document.isaNumber()).isEqualTo("CONF-SHARED");assertThat(document.totalPackages()).isEqualTo(8);
|
||||
assertThat(document.pageNumber()).isEqualTo(1);assertThat(document.pageCount()).isEqualTo(1);
|
||||
assertThat(document.cargoRows()).extracting(PrintPdfClient.CargoRow::warehouseNo).containsExactly("IN-A","IN-B");
|
||||
assertThat(document.cargoRows()).extracting(PrintPdfClient.CargoRow::packageCount).containsExactly(3,5);
|
||||
assertThat(document.cargoRows()).extracting(PrintPdfClient.CargoRow::palletNo).containsExactly("PALLET-11","PALLET-12");
|
||||
});
|
||||
}
|
||||
@Test
|
||||
/** 同目的仓的不同预约号不得合成同一份BOL。 */
|
||||
void keepsDifferentAppointmentsAtTheSameDestinationInSeparateDocuments() {
|
||||
var request=render(List.of(cargo(11,"101","ABE2","IN-A",3,"CONF-A"),cargo(12,"102","ABE2","IN-B",5,"CONF-B")),2);
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::isaNumber).containsExactly("CONF-A","CONF-B");
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::totalPackages).containsExactly(3,5);
|
||||
assertThat(request.documents().get(0).cargoRows()).extracting(PrintPdfClient.CargoRow::warehouseNo).containsExactly("IN-A");
|
||||
assertThat(request.documents().get(1).cargoRows()).extracting(PrintPdfClient.CargoRow::warehouseNo).containsExactly("IN-B");
|
||||
}
|
||||
@Test
|
||||
/** 相同预约号不能将不同目的仓的货物混入同一份BOL。 */
|
||||
void keepsTheSameAppointmentAtDifferentDestinationsInSeparateDocuments() {
|
||||
var request=render(List.of(cargo(11,"101","ABE2","IN-A",3,"CONF-SHARED"),cargo(12,"102","ONT8","IN-B",5,"CONF-SHARED")),2);
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::isaNumber).containsExactly("CONF-SHARED","CONF-SHARED");
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::fbaWarehouseCode).containsExactly("ABE2","ONT8");
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::totalPackages).containsExactly(3,5);
|
||||
}
|
||||
@Test
|
||||
/** 无号及空白号合并同仓货物,并把ISA作为空串传递。 */
|
||||
void mergesMissingAppointmentsAtTheSameDestinationAndLeavesIsaEmpty() {
|
||||
var request=render(List.of(cargo(11,"101","ABE2","IN-A",3,null),cargo(12,"102","ABE2","IN-B",5," ")),1);
|
||||
assertThat(request.documents()).singleElement().satisfies(document->{
|
||||
assertThat(document.isaNumber()).isEmpty();assertThat(document.totalPackages()).isEqualTo(8);
|
||||
assertThat(document.cargoRows()).extracting(PrintPdfClient.CargoRow::warehouseNo).containsExactly("IN-A","IN-B");
|
||||
});
|
||||
}
|
||||
@Test
|
||||
/** 两仓各两个预约在一次PDF请求中保留四份独立BOL。 */
|
||||
void keepsTwoAppointmentsForEachOfTwoWarehousesInOnePdfRequest() {
|
||||
var request=render(List.of(cargo(11,"101","ABE2","IN-A1",2,"CONF-A1"),cargo(12,"102","ABE2","IN-A2",3,"CONF-A2"),
|
||||
cargo(13,"101","ONT8","IN-B1",4,"CONF-B1"),cargo(14,"102","ONT8","IN-B2",5,"CONF-B2")),4);
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::isaNumber).containsExactly("CONF-A1","CONF-A2","CONF-B1","CONF-B2");
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::fbaWarehouseCode).containsExactly("ABE2","ABE2","ONT8","ONT8");
|
||||
assertThat(request.documents()).extracting(PrintPdfClient.Document::totalPackages).containsExactly(2,3,4,5);
|
||||
assertThat(request.downloadFileName()).isEqualTo("DS-ONE-TRUCK-BOL.pdf");
|
||||
}
|
||||
@Test
|
||||
/** 每份BOL重置页码,跨页全部原始货物仅累计一次。 */
|
||||
void restartsPaginationPerBolAndCountsEveryCargoRowOnlyOnce() {
|
||||
var lines=new ArrayList<DispatchBolData.Line>();
|
||||
for(int i=0;i<45;i++)lines.add(cargo(100+i,i%2==0?"101":"102","ABE2","IN-A-"+i,i%3+1,"CONF-A"));
|
||||
for(int i=0;i<19;i++)lines.add(cargo(200+i,"103","ABE2","IN-B-"+i,2,"CONF-B"));
|
||||
var documents=render(lines,5).documents();
|
||||
assertThat(documents).extracting(PrintPdfClient.Document::pageNumber).containsExactly(1,2,3,1,2);
|
||||
assertThat(documents).extracting(PrintPdfClient.Document::pageCount).containsExactly(3,3,3,2,2);
|
||||
assertThat(documents.stream().map(d->d.cargoRows().size()).toList()).containsExactly(18,26,1,18,1);
|
||||
assertThat(documents).extracting(PrintPdfClient.Document::totalPackages).containsExactly(90,90,90,38,38);
|
||||
assertThat(documents).extracting(PrintPdfClient.Document::isaNumber).containsExactly("CONF-A","CONF-A","CONF-A","CONF-B","CONF-B");
|
||||
var rows=documents.stream().flatMap(d->d.cargoRows().stream()).toList();assertThat(rows).hasSize(64);
|
||||
assertThat(rows.stream().mapToInt(PrintPdfClient.CargoRow::packageCount).sum()).isEqualTo(128);
|
||||
assertThat(rows).extracting(PrintPdfClient.CargoRow::warehouseNo).containsExactlyElementsOf(lines.stream().map(DispatchBolData.Line::warehouseNo).toList());
|
||||
}
|
||||
/** 所有分组仍通过一次远程调用生成同一个PDF,并保留原文件名。 */
|
||||
private PrintPdfClient.Request render(List<DispatchBolData.Line> lines,int pages) {
|
||||
var detail=new DispatchBolData("DS-ONE-TRUCK","9","Operating Warehouse",null,null,null,lines);
|
||||
when(dispatches.read(detail.dispatchNo())).thenReturn(detail);
|
||||
var warehouse=new DomesticWarehouseDirectoryClient.Warehouse(9L,0L,"WH9","Operating Warehouse","OVERSEAS","US","ACTIVE",null,null);
|
||||
when(warehouses.activeWarehouse(9L,"internal-test-token")).thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse,List.of())));
|
||||
when(print.generateDispatchBol(eq("internal-test-token"),eq("two-customers"),any())).thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body("%PDF-".getBytes(StandardCharsets.US_ASCII)));
|
||||
service.generate(detail.dispatchNo(),"two-customers");
|
||||
when(print.generateDispatchBol(eq("internal-test-token"),eq("grouped-bol"),any())).thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
|
||||
.header("X-Print-Page-Count",Integer.toString(pages)).body("%PDF-".getBytes(StandardCharsets.US_ASCII)));
|
||||
var generated=service.generate(detail.dispatchNo(),"grouped-bol");
|
||||
assertThat(generated.fileName()).isEqualTo("DS-ONE-TRUCK-BOL.pdf");assertThat(generated.pageCount()).isEqualTo(Integer.toString(pages));
|
||||
assertThat(generated.content()).isEqualTo("%PDF-".getBytes(StandardCharsets.US_ASCII));
|
||||
var request=ArgumentCaptor.forClass(PrintPdfClient.Request.class);
|
||||
verify(print).generateDispatchBol(eq("internal-test-token"),eq("two-customers"),request.capture());
|
||||
assertThat(request.getValue().documents()).hasSize(2);
|
||||
assertThat(request.getValue().documents().stream().map(PrintPdfClient.Document::isaNumber).toList()).containsExactly("CONF-A","CONF-B");
|
||||
verify(print).generateDispatchBol(eq("internal-test-token"),eq("grouped-bol"),request.capture());
|
||||
return request.getValue();
|
||||
}
|
||||
/** 新快照携带真实明细及原订单身份,不恢复旧预约号映射。 */
|
||||
private static DispatchBolData.Line cargo(long item,String customer,String code,String incoming,int packages,String appointment) {
|
||||
return new DispatchBolData.Line(item,item+1000,customer,"FBA",null,null,"TSTU3539519","PALLET-"+item,code,code+" Warehouse",incoming,
|
||||
"SHIP-"+item,"REF-"+item,packages,appointment);
|
||||
}
|
||||
/** 为既有仓地址与目的地测试构造没有预约号的真实身份快照。 */
|
||||
private static DispatchBolData.Line line(String customer,String code,String name,int packages) {
|
||||
return new DispatchBolData.Line(customer,"FBA",null,null,"TSTU3539519","DP-001",code,name,"NYEXX2609110214",null,null,packages);
|
||||
return new DispatchBolData.Line(Long.valueOf(customer),Long.parseLong(customer)+1000,customer,"FBA",null,null,"TSTU3539519","DP-001",code,name,"NYEXX2609110214",null,null,packages,null);
|
||||
}
|
||||
}
|
||||
|
||||
+187
-14
@@ -1,41 +1,214 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchAggregate;
|
||||
import com.baiyue.lastleg.dispatch.model.DispatchBolData;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchBolRepository;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 真实H2/MyBatis读取来源与预约分配;不通过mock返回新模型自证数量守恒。 */
|
||||
class DispatchBolReadPersistenceTest {
|
||||
/** 同仓同目的地无号货物跨客户合并文档,逐行客户和来源仍独立。 */
|
||||
@Test void wholeTruckBolReadsBothTrueOwnersAndPreservesTheirOriginalLineComposition() throws Exception {
|
||||
var f=new Fixture();f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(1,10),(2,20)");f.db.update("INSERT INTO ll_inbound_plan VALUES(10,'101','CN-A'),(20,'102','CN-B')");
|
||||
f.db.update("INSERT INTO ll_inbound_plan_line VALUES(100,'101',1000,'ONT8','ONT8','IN-A'),(200,'102',2000,'ONT8','ONT8','IN-B')");f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1,100,0,3),(2,200,0,5)");
|
||||
f.db.update("INSERT INTO ll_order VALUES(1000,'101','CN-A'),(2000,'102','CN-B')");f.db.update("INSERT INTO ll_order_detail VALUES(1000,'101','SHIP-A','REF-A'),(2000,'102','SHIP-B','REF-B')");
|
||||
f.item(11,"101",1,null,"FBA",null,3);f.item(12,"102",2,null,"FBA",null,5);
|
||||
var result=f.service.read("DP-ONE");assertThat(result.bolLines()).extracting(l->l.customerId()).containsExactly("101","102");assertThat(result.bolLines()).extracting(l->l.packageCount()).containsExactly(3,5);assertThat(result.bolLines()).extracting(l->l.fbaShipmentId()).containsExactly("SHIP-A","SHIP-B");
|
||||
assertThat(result.bolLines().get(0).groupKey()).isNotEqualTo(result.bolLines().get(1).groupKey());
|
||||
var f=new Fixture();f.source(11,"101",1,1000,3,"ONT8","RECEIVED",0);f.source(12,"102",2,2000,5,"ONT8","RECEIVED",0);
|
||||
f.db.update("INSERT INTO ll_order_detail VALUES(1000,'101','SHIP-A','REF-A'),(2000,'102','SHIP-B','REF-B')");
|
||||
var result=f.read();assertThat(result.bolLines()).extracting(DispatchBolData.Line::customerId).containsExactly("101","102");
|
||||
assertThat(result.bolLines()).extracting(DispatchBolData.Line::packageCount).containsExactly(3,5);
|
||||
assertThat(result.bolLines()).extracting(DispatchBolData.Line::fbaShipmentId).containsExactly("SHIP-A","SHIP-B");
|
||||
assertThat(result.bolLines().get(0).groupKey()).isEqualTo(result.bolLines().get(1).groupKey());
|
||||
assertThat(result.bolLines()).allSatisfy(line->assertThat(line.appointmentNo()).isNull());
|
||||
}
|
||||
|
||||
/** 部分退回仅打印所选箱数,不借原计划已清点/已拣总数放大。 */
|
||||
@Test void partialReturnReceiptPrintsOnlySelectedQuantityAndPrivateActualAddress() throws Exception {
|
||||
var f=new Fixture();f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(1,10)");f.db.update("INSERT INTO ll_inbound_plan VALUES(10,'101','CN-A')");f.db.update("INSERT INTO ll_inbound_plan_line VALUES(100,'101',1000,'ONT8','ONT8','ORIGINAL-IN')");f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1,100,8,8)");
|
||||
f.item(11,"101",1,99L,"PRIVATE","Actual receiving street",2);
|
||||
var result=f.service.read("DP-ONE");assertThat(result.bolLines()).singleElement().satisfies(l->{assertThat(l.packageCount()).isEqualTo(2);assertThat(l.destinationLabel()).isEqualTo("Actual receiving street");assertThat(l.warehouseNo()).isEqualTo("RETURN-IN");});
|
||||
var f=new Fixture();f.source(11,"101",1,1000,8,"ONT8","RECEIVED",8);
|
||||
f.db.update("UPDATE ll_dispatch_item SET return_receipt_id=99,destination_type='PRIVATE',delivery_address='Actual receiving street',inbound_code='RETURN-IN',package_count=2 WHERE id=11");
|
||||
var result=f.read();assertThat(result.bolLines()).singleElement().satisfies(line->{
|
||||
assertThat(line.packageCount()).isEqualTo(2);assertThat(line.destinationLabel()).isEqualTo("Actual receiving street");
|
||||
assertThat(line.warehouseNo()).isEqualTo("RETURN-IN");assertThat(line.appointmentNo()).isNull();
|
||||
});
|
||||
}
|
||||
|
||||
/** 换标履约仅采用该逻辑明细,不能复用原计划所有货物。 */
|
||||
@Test void relabelSourceDoesNotBorrowOriginalPlanQuantity() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,8,"ONT8","RECEIVED",8);
|
||||
f.db.update("UPDATE ll_dispatch_item SET source_business_type='RELABEL_ORDER',source_stage='RELABEL',package_count=2,inbound_code='RELABEL-IN' WHERE id=11");
|
||||
assertThat(f.read().bolLines()).singleElement().satisfies(line->{assertThat(line.packageCount()).isEqualTo(2);assertThat(line.warehouseNo()).isEqualTo("RELABEL-IN");});
|
||||
}
|
||||
|
||||
/** 同实物分成两个预约时只消费各自份额,多条订单商品资料不得重复箱数。 */
|
||||
@Test void twoAppointmentsConsumeOnePhysicalSourceExactlyOncePerAllocation() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",1);
|
||||
f.db.update("INSERT INTO ll_order_detail VALUES(1000,'101','SHIP-A','REF-A'),(1000,'101','SHIP-B','REF-B'),(1000,'101','SHIP-A','REF-A')");
|
||||
f.appointment(1," ISA-A ","101","ORDER:1000",1000,"ONT8");f.appointment(2,"ISA-B","101","ORDER:1000",1000,"ONT8");
|
||||
f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",2);f.allocate(11,1,2,"101","ORDER:1000","FBA:ONT8",3);
|
||||
var lines=f.read().bolLines();assertThat(lines).hasSize(2);
|
||||
assertThat(lines).extracting(DispatchBolData.Line::appointmentNo).containsExactly("ISA-A","ISA-B");
|
||||
assertThat(lines).extracting(DispatchBolData.Line::packageCount).containsExactly(2,3);
|
||||
assertThat(lines).allSatisfy(line->{assertThat(line.dispatchItemId()).isEqualTo(11L);assertThat(line.orderId()).isEqualTo(1000L);assertThat(line.fbaShipmentId()).isEqualTo("SHIP-A, SHIP-B");});
|
||||
assertThat(lines.stream().mapToInt(DispatchBolData.Line::packageCount).sum()).isEqualTo(5);
|
||||
}
|
||||
|
||||
/** 同一明细同计划实托的两个订单各取自己的预约份额,商品JOIN不能将另一订单或整托复制过去。 */
|
||||
@Test void oneDispatchItemWithTwoOrdersAllocatesEachOriginalSourceToItsOwnAppointment() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,3,"ONT8","RECEIVED",1);
|
||||
f.db.update("UPDATE ll_dispatch_item SET package_count=5 WHERE id=11");
|
||||
f.db.update("INSERT INTO ll_inbound_plan_line VALUES(120,'101',10,2000,'ONT8','ONT8','ONT8','IN-2000')");
|
||||
f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(121,'101',10,1,120,0,2,2)");
|
||||
f.db.update("INSERT INTO ll_order VALUES(2000,'101','CN-2000')");
|
||||
f.db.update("INSERT INTO ll_order_detail VALUES(1000,'101','SHIP-A1','REF-A1'),(1000,'101','SHIP-A2','REF-A2'),(1000,'101','SHIP-A1','REF-A1'),(2000,'101','SHIP-B1','REF-B1'),(2000,'101','SHIP-B2','REF-B2')");
|
||||
f.appointment(1,"ISA-A","101","ORDER:1000",1000,"ONT8");f.appointment(2,"ISA-B","101","ORDER:2000",2000,"ONT8");
|
||||
f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",3);f.allocate(11,1,2,"101","ORDER:2000","FBA:ONT8",2);
|
||||
var lines=f.read().bolLines();assertThat(lines).hasSize(2);
|
||||
assertThat(lines).extracting(DispatchBolData.Line::dispatchItemId).containsExactly(11L,11L);
|
||||
assertThat(lines).extracting(DispatchBolData.Line::orderId).containsExactly(1000L,2000L);
|
||||
assertThat(lines).extracting(DispatchBolData.Line::appointmentNo).containsExactly("ISA-A","ISA-B");
|
||||
assertThat(lines).extracting(DispatchBolData.Line::packageCount).containsExactly(3,2);
|
||||
assertThat(lines).extracting(DispatchBolData.Line::warehouseNo).containsExactly("IN-1000","IN-2000");
|
||||
assertThat(lines).extracting(DispatchBolData.Line::fbaShipmentId).containsExactly("SHIP-A1, SHIP-A2","SHIP-B1, SHIP-B2");
|
||||
assertThat(lines).extracting(DispatchBolData.Line::poReferenceNo).containsExactly("REF-A1, REF-A2","REF-B1, REF-B2");
|
||||
assertThat(lines.stream().mapToInt(DispatchBolData.Line::packageCount).sum()).isEqualTo(5);
|
||||
}
|
||||
|
||||
/** BOOKING来源通过原预约item的持久order_id关联,不把真实稳定键改拼成ORDER。 */
|
||||
@Test void bookingSourceUsesPersistedAppointmentOrderMapping() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",0);
|
||||
f.appointment(1,"BOOKING-ISA","101","BOOKING:EXT-REAL",1000,"ONT8");
|
||||
f.allocate(11,1,1,"101","BOOKING:EXT-REAL","FBA:ONT8",5);
|
||||
assertThat(f.read().bolLines()).singleElement().satisfies(line->{assertThat(line.orderId()).isEqualTo(1000L);assertThat(line.appointmentNo()).isEqualTo("BOOKING-ISA");assertThat(line.packageCount()).isEqualTo(5);});
|
||||
}
|
||||
|
||||
/** 同外部约号、同目的地的不同客户保留各自份额,但共用文档键。 */
|
||||
@Test void sameAppointmentAcrossCustomersSharesDocumentWithoutLosingOwnership() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,3,"ONT8","RECEIVED",0);f.source(12,"102",2,2000,5,"ONT8","RECEIVED",0);
|
||||
f.appointment(1,"SHARED-ISA","101","ORDER:1000",1000,"ONT8");f.appointment(2," SHARED-ISA ","102","ORDER:2000",2000,"ONT8");
|
||||
f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",3);f.allocate(12,2,2,"102","ORDER:2000","FBA:ONT8",5);
|
||||
var lines=f.read().bolLines();assertThat(lines).extracting(DispatchBolData.Line::customerId).containsExactly("101","102");
|
||||
assertThat(lines).extracting(DispatchBolData.Line::packageCount).containsExactly(3,5);assertThat(lines.stream().map(DispatchBolData.Line::groupKey).distinct().count()).isOne();
|
||||
}
|
||||
|
||||
/** 同约号不同目的地仍分文档;缺约号不以系统预约单号补充。 */
|
||||
@Test void destinationSeparatesSharedNumbersAndBlankConfirmationRemainsPrintable() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,3,"ONT8","RECEIVED",0);f.source(12,"102",2,2000,5,"LAX9","RECEIVED",0);
|
||||
f.appointment(1,"SAME","101","ORDER:1000",1000,"ONT8");f.appointment(2,"SAME","102","ORDER:2000",2000,"LAX9");
|
||||
f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",3);f.allocate(12,2,2,"102","ORDER:2000","FBA:LAX9",5);
|
||||
assertThat(f.read().bolLines().stream().map(DispatchBolData.Line::groupKey).distinct().count()).isEqualTo(2);
|
||||
f.db.update("UPDATE ll_inbound_fba_appointment SET confirmation_no=' '");
|
||||
assertThat(f.read().bolLines()).allSatisfy(line->{assertThat(line.appointmentNo()).isNull();assertThat(line.packageCount()).isPositive();});
|
||||
}
|
||||
|
||||
/** 未约余量有真实来源数量依据,空ISA保留;不能把全托复制到已有预约。 */
|
||||
@Test void unassignedRemainderKeepsItsOwnQuantityAndBlankAppointment() throws Exception {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",1);
|
||||
f.appointment(1,"ISA-A","101","ORDER:1000",1000,"ONT8");f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",2);
|
||||
var lines=f.read().bolLines();assertThat(lines).extracting(DispatchBolData.Line::packageCount).containsExactly(2,3);
|
||||
assertThat(lines.get(0).appointmentNo()).isEqualTo("ISA-A");assertThat(lines.get(1).appointmentNo()).isNull();
|
||||
}
|
||||
|
||||
/** PLAN用计划箱数,RECEIVED用清点箱数;部分扫码量从不替代原调度份额。 */
|
||||
@Test void plannedAndReceivedCountsMatchTheirAuthoritativeSourceStage() throws Exception {
|
||||
for(var stage:List.of("PLAN","RECEIVED")) {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8",stage,1);
|
||||
if(stage.equals("PLAN"))f.db.update("UPDATE ll_inbound_plan_pallet_allocation SET counted_package_count=2");
|
||||
else f.db.update("UPDATE ll_inbound_plan_pallet_allocation SET planned_package_count=9");
|
||||
assertThat(f.read().bolLines()).singleElement().satisfies(line->assertThat(line.packageCount()).isEqualTo(5));
|
||||
}
|
||||
}
|
||||
|
||||
/** 来源合计与本车选择量不一致或数量未知时失败,不以整托/扫码常数补齐。 */
|
||||
@Test void missingCountsAndChangedSelectedTotalsFailClosed() throws Exception {
|
||||
for(var change:List.of("UPDATE ll_inbound_plan_pallet_allocation SET counted_package_count=NULL",
|
||||
"UPDATE ll_dispatch_item SET package_count=4")) {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",1);f.db.update(change);
|
||||
assertThatThrownBy(f::read).isInstanceOf(BizException.class).hasMessageContaining("尚未核实");
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细、客户、来源、目的地、原计划与预约item任一不匹配均不能取得打印份额。 */
|
||||
@Test void foreignOrIncompleteAllocationCannotBorrowAnotherSource() throws Exception {
|
||||
for(var fault:List.of("customer","item","plan","source","destination","appointment-customer","order","missing-order","missing-batch")) {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",0);
|
||||
f.appointment(1,"ISA","101","BOOKING:REAL",1000,"ONT8");f.allocate(11,1,1,"101","BOOKING:REAL","FBA:ONT8",5);
|
||||
f.db.update(switch(fault) {
|
||||
case "customer" -> "UPDATE ll_dispatch_appointment_allocation SET customer_id='FOREIGN'";
|
||||
case "item" -> "UPDATE ll_dispatch_appointment_allocation SET dispatch_item_id=99";
|
||||
case "plan" -> "UPDATE ll_dispatch_appointment_allocation SET plan_pallet_id=99";
|
||||
case "source" -> "UPDATE ll_dispatch_appointment_allocation SET source_key='ORDER:1000'";
|
||||
case "destination" -> "UPDATE ll_dispatch_appointment_allocation SET destination_key='FBA:LAX9'";
|
||||
case "appointment-customer" -> "UPDATE ll_inbound_fba_appointment_item SET customer_id='FOREIGN'";
|
||||
case "order" -> "UPDATE ll_inbound_fba_appointment_item SET order_id=9999";
|
||||
case "missing-order" -> "UPDATE ll_inbound_fba_appointment_item SET order_id=NULL";
|
||||
default -> "DELETE FROM ll_inbound_fba_appointment";
|
||||
});
|
||||
assertThatThrownBy(f::read).as(fault).isInstanceOf(BizException.class).hasMessageContaining("尚未核实");
|
||||
}
|
||||
}
|
||||
|
||||
/** 单份超量和多份累计超量均失败,原货物和分配记录不被读取操作修复。 */
|
||||
@Test void individualAndCumulativeOverAllocationFailWithoutChangingFacts() throws Exception {
|
||||
for(boolean cumulative:List.of(false,true)) {
|
||||
var f=new Fixture();f.source(11,"101",1,1000,5,"ONT8","RECEIVED",0);
|
||||
f.appointment(1,"ISA-A","101","ORDER:1000",1000,"ONT8");f.allocate(11,1,1,"101","ORDER:1000","FBA:ONT8",cumulative?3:6);
|
||||
if(cumulative){f.appointment(2,"ISA-B","101","ORDER:1000",1000,"ONT8");f.allocate(11,1,2,"101","ORDER:1000","FBA:ONT8",3);}
|
||||
var before=f.db.queryForList("SELECT * FROM ll_dispatch_appointment_allocation ORDER BY batch_key");
|
||||
assertThatThrownBy(f::read).isInstanceOf(BizException.class).hasMessageContaining("尚未核实");
|
||||
assertThat(f.db.queryForList("SELECT * FROM ll_dispatch_appointment_allocation ORDER BY batch_key")).isEqualTo(before);
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试表仅取完整模块DDL中实际引用列,保留来源/分配唯一键,事务覆盖两个真实Mapper查询。 */
|
||||
private static final class Fixture {
|
||||
final JdbcTemplate db;final DispatchBolReadService service;
|
||||
final JdbcTemplate db;final DispatchBolReadService service;final TransactionTemplate tx;
|
||||
/** 根主单只提供身份,全部货物和预约事实均经实际Mapper读取。 */
|
||||
Fixture() throws Exception {
|
||||
var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");db=new JdbcTemplate(ds);
|
||||
db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,dispatch_order_id BIGINT,customer_id VARCHAR(64),plan_pallet_id BIGINT,return_receipt_id BIGINT,source_business_type VARCHAR(32),destination_type VARCHAR(32),delivery_address VARCHAR(128),postal_code VARCHAR(32),pallet_no VARCHAR(64),fba_warehouse_code VARCHAR(64),destination_warehouse_code VARCHAR(64),destination_warehouse_name VARCHAR(64),inbound_code VARCHAR(64),package_count INT)");
|
||||
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT)");db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(64),container_no VARCHAR(64))");db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,picked_package_count INT,planned_package_count INT)");db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR(64),order_id BIGINT,destination_warehouse_code VARCHAR(64),destination_warehouse_name VARCHAR(64),inbound_code VARCHAR(64))");db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),container_no VARCHAR(64))");db.execute("CREATE TABLE ll_order_detail(order_id BIGINT,customer_id VARCHAR(64),shipment_no VARCHAR(64),fba_tracking_no VARCHAR(64))");
|
||||
db.execute("CREATE TABLE ll_dispatch_item(id BIGINT PRIMARY KEY,dispatch_order_id BIGINT,customer_id VARCHAR(128),plan_pallet_id BIGINT,return_receipt_id BIGINT,source_business_type VARCHAR(32),source_stage VARCHAR(32),order_id BIGINT,destination_type VARCHAR(32),delivery_address VARCHAR(1000),postal_code VARCHAR(32),pallet_no VARCHAR(64),fba_warehouse_code VARCHAR(128),destination_warehouse_code VARCHAR(128),destination_warehouse_name VARCHAR(255),inbound_code VARCHAR(128),package_count INT)");
|
||||
db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT PRIMARY KEY,plan_id BIGINT)");db.execute("CREATE TABLE ll_inbound_plan(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),container_no VARCHAR(64))");
|
||||
db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),plan_id BIGINT,pallet_id BIGINT,line_id BIGINT,picked_package_count INT,planned_package_count INT,counted_package_count INT,UNIQUE(line_id,pallet_id))");
|
||||
db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),plan_id BIGINT,order_id BIGINT,fba_warehouse_code VARCHAR(128),destination_warehouse_code VARCHAR(128),destination_warehouse_name VARCHAR(255),inbound_code VARCHAR(128),UNIQUE(plan_id,order_id))");
|
||||
db.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),container_no VARCHAR(64))");db.execute("CREATE TABLE ll_order_detail(order_id BIGINT,customer_id VARCHAR(128),shipment_no VARCHAR(255),fba_tracking_no VARCHAR(255))");
|
||||
db.execute("CREATE TABLE ll_dispatch_appointment_allocation(dispatch_id BIGINT NOT NULL,dispatch_item_id BIGINT NOT NULL,plan_pallet_id BIGINT NOT NULL,batch_key VARCHAR(96) NOT NULL,customer_id VARCHAR(64) NOT NULL,source_key VARCHAR(160) NOT NULL,destination_key VARCHAR(160) NOT NULL,package_count INT NOT NULL,PRIMARY KEY(dispatch_item_id,batch_key,source_key,destination_key))");
|
||||
db.execute("CREATE TABLE ll_inbound_fba_appointment(id BIGINT PRIMARY KEY,appointment_no VARCHAR(64),confirmation_no VARCHAR(128))");
|
||||
db.execute("CREATE TABLE ll_inbound_fba_appointment_item(id BIGINT PRIMARY KEY,appointment_id BIGINT NOT NULL,source_key VARCHAR(160) NOT NULL,customer_id VARCHAR(128) NOT NULL,order_id BIGINT,fba_warehouse_code VARCHAR(128) NOT NULL,UNIQUE(appointment_id,source_key))");
|
||||
var config=new MybatisConfiguration();config.addMapper(DispatchBolMapper.class);var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);factory.afterPropertiesSet();var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DispatchBolMapper.class);
|
||||
var roots=mock(DispatchAggregateRepository.class);var row=new DispatchAggregate();row.setId(1L);row.setDispatchNo("DP-ONE");row.setOverseasWarehouseId("9");row.setResourcesJson(new ObjectMapper().writeValueAsString(new DispatchContracts.Resources(null,null)));when(roots.find("DP-ONE")).thenReturn(row);when(roots.batches(1)).thenReturn(List.of());service=new DispatchBolReadService(roots,mapper,new ObjectMapper());
|
||||
var roots=mock(DispatchAggregateRepository.class);var row=new DispatchAggregate();row.setId(1L);row.setDispatchNo("DP-ONE");row.setOverseasWarehouseId("9");row.setResourcesJson(new ObjectMapper().writeValueAsString(new DispatchContracts.Resources(null,null)));when(roots.find("DP-ONE")).thenReturn(row);
|
||||
service=new DispatchBolReadService(roots,new DispatchBolRepository(mapper),new ObjectMapper());
|
||||
tx=new TransactionTemplate(new DataSourceTransactionManager(ds));tx.setReadOnly(true);tx.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ);
|
||||
}
|
||||
/** 本地一致事务读取,不接触任何环境或远程文件。 */
|
||||
DispatchBolData read(){return tx.execute(status->service.read("DP-ONE"));}
|
||||
/** 创建一票真实计划来源;picked与原调度/清点数量有意独立。 */
|
||||
void source(long item,String customer,long pallet,long order,int packages,String code,String stage,int picked) {
|
||||
long plan=pallet*10,line=item*10;
|
||||
db.update("INSERT INTO ll_inbound_plan_pallet VALUES(?,?)",pallet,plan);db.update("INSERT INTO ll_inbound_plan VALUES(?,?,?)",plan,customer,"CN-"+order);
|
||||
db.update("INSERT INTO ll_inbound_plan_line VALUES(?,?,?,?,?,?,?,?)",line,customer,plan,order,code,code,code,"IN-"+order);
|
||||
db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(?,?,?,?,?,?,?,?)",line+1,customer,plan,pallet,line,picked,packages,packages);
|
||||
db.update("INSERT INTO ll_order VALUES(?,?,?)",order,customer,"CN-"+order);
|
||||
db.update("INSERT INTO ll_dispatch_item VALUES(?,1,?,?,NULL,NULL,?,?, 'FBA',NULL,NULL,?,?,?,?,?,?)",item,customer,pallet,stage,order,"P-"+pallet,code,code,code,"IN-"+order,packages);
|
||||
}
|
||||
/** 系统预约业务号与对外确认号分开,稳定来源键原样保存。 */
|
||||
void appointment(long id,String confirmation,String customer,String source,long order,String code) {
|
||||
db.update("INSERT INTO ll_inbound_fba_appointment VALUES(?,?,?)",id,"SYSTEM-APPOINTMENT-"+id,confirmation);
|
||||
db.update("INSERT INTO ll_inbound_fba_appointment_item VALUES(?,?,?,?,?,?)",id,id,source,customer,order,code);
|
||||
}
|
||||
/** 按模块实际主键建立本车明细的真实逐票分配,不以客户目的地总量替代。 */
|
||||
void allocate(long item,long pallet,long appointment,String customer,String source,String destination,int packages) {
|
||||
db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(1,?,?,?,?,?,?,?)",item,pallet,"FBA_APPOINTMENT:"+appointment,customer,source,destination,packages);
|
||||
}
|
||||
void item(long id,String customer,long plan,Long returned,String type,String address,int packages){db.update("INSERT INTO ll_dispatch_item VALUES(?,1,?,?,?,NULL,?,?,NULL,?,'ONT8','ONT8','ONT8',?,?)",id,customer,plan,returned,type,address,"P-"+id,returned==null?"IN-"+customer:"RETURN-IN",packages);}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user