Merge remote-tracking branch 'origin/test' into test
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.baiyue.lastleg.appointmentplanning;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** 读取原货物的货件资料,始终按客户及订单身份隔离。 */
|
||||||
|
@Repository
|
||||||
|
public class AppointmentCargoReferenceRepository {
|
||||||
|
private final AppointmentCargoVerificationMapper mapper;
|
||||||
|
|
||||||
|
/** 复用预约货物 Mapper,不跨服务或跨库补读申报。 */
|
||||||
|
public AppointmentCargoReferenceRepository(AppointmentCargoVerificationMapper mapper) { this.mapper = mapper; }
|
||||||
|
|
||||||
|
/** 一次批读本次明确货源的原货件号,未实收建单的外接货保持空列表。 */
|
||||||
|
public Map<List<String>, List<String>> originalReferences(List<AppointmentSourceService.Source> sources) {
|
||||||
|
var orders = sources.stream().filter(source -> source.orderId() != null)
|
||||||
|
.map(source -> new AppointmentCoverageMapper.OrderRef(source.customerId(), source.orderId())).distinct().toList();
|
||||||
|
var byOrder = orders.isEmpty() ? Map.<AppointmentCoverageMapper.OrderRef, List<AppointmentCargoVerificationMapper.ShipmentReference>>of()
|
||||||
|
: mapper.originalShipmentReferences(orders).stream().collect(Collectors.groupingBy(
|
||||||
|
row -> new AppointmentCoverageMapper.OrderRef(row.customerId(), row.orderId())));
|
||||||
|
var result = new HashMap<List<String>, List<String>>();
|
||||||
|
for (var source : sources) {
|
||||||
|
var references = byOrder.getOrDefault(new AppointmentCoverageMapper.OrderRef(source.customerId(), source.orderId()), List.of())
|
||||||
|
.stream().map(AppointmentCargoVerificationMapper.ShipmentReference::shipmentReference).filter(Objects::nonNull)
|
||||||
|
.map(String::trim).filter(value -> !value.isEmpty()).distinct().sorted().toList();
|
||||||
|
result.put(List.of(source.customerId(), source.sourceKey()), references);
|
||||||
|
}
|
||||||
|
return Map.copyOf(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+17
@@ -8,6 +8,23 @@ import org.apache.ibatis.annotations.Select;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface AppointmentCargoVerificationMapper extends BaseMapper<AppointmentCargoVerification> {
|
public interface AppointmentCargoVerificationMapper extends BaseMapper<AppointmentCargoVerification> {
|
||||||
|
record ShipmentReference(String customerId, Long orderId, String shipmentReference) { }
|
||||||
|
|
||||||
|
/** 按已授权的客户与原订单读取原有货件号和追踪号,仓储转运读取其正式来源。 */
|
||||||
|
@Select({"<script>",
|
||||||
|
"SELECT o.customer_id AS customerId,o.id AS orderId,",
|
||||||
|
"CASE WHEN o.order_type='WAREHOUSE_TRANSFER' THEN wt.fba_shipment_no ELSE d.shipment_no END AS shipmentReference ",
|
||||||
|
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id AND d.customer_id=o.customer_id AND o.order_type<>'WAREHOUSE_TRANSFER' ",
|
||||||
|
AppointmentCoverageMapper.TRANSFER_JOIN,
|
||||||
|
"WHERE <foreach collection='orders' item='o' open='(' separator=' OR ' close=')'>(o.id=#{o.orderId} AND o.customer_id=#{o.customerId})</foreach>",
|
||||||
|
"UNION ALL SELECT o.customer_id AS customerId,o.id AS orderId,",
|
||||||
|
"CASE WHEN o.order_type='WAREHOUSE_TRANSFER' THEN wt.tracking_no ELSE d.fba_tracking_no END AS shipmentReference ",
|
||||||
|
"FROM ll_order o LEFT JOIN ll_order_detail d ON d.order_id=o.id AND d.customer_id=o.customer_id AND o.order_type<>'WAREHOUSE_TRANSFER' ",
|
||||||
|
AppointmentCoverageMapper.TRANSFER_JOIN,
|
||||||
|
"WHERE <foreach collection='orders' item='o' open='(' separator=' OR ' close=')'>(o.id=#{o.orderId} AND o.customer_id=#{o.customerId})</foreach>",
|
||||||
|
"</script>"})
|
||||||
|
java.util.List<ShipmentReference> originalShipmentReferences(@Param("orders") java.util.List<AppointmentCoverageMapper.OrderRef> orders);
|
||||||
|
|
||||||
/** Same canonical source row used by allocation; flush pre-lock observations before re-reading. */
|
/** Same canonical source row used by allocation; flush pre-lock observations before re-reading. */
|
||||||
@Select("SELECT id FROM ll_order WHERE id=#{orderId} FOR UPDATE")
|
@Select("SELECT id FROM ll_order WHERE id=#{orderId} FOR UPDATE")
|
||||||
@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = false)
|
@Options(flushCache = Options.FlushCachePolicy.TRUE, useCache = false)
|
||||||
|
|||||||
+20
-9
@@ -21,10 +21,12 @@ public class AppointmentCargoVerificationService {
|
|||||||
private final AppointmentSourceService sources;
|
private final AppointmentSourceService sources;
|
||||||
private final AppointmentCargoVerificationMapper mapper;
|
private final AppointmentCargoVerificationMapper mapper;
|
||||||
private final ObjectMapper json;
|
private final ObjectMapper json;
|
||||||
|
private final AppointmentCargoReferenceRepository references;
|
||||||
|
|
||||||
|
/** 核实记录与原货物资料分别读取,已有货件号无需重复录入。 */
|
||||||
public AppointmentCargoVerificationService(AppointmentSourceService sources, AppointmentCargoVerificationMapper mapper,
|
public AppointmentCargoVerificationService(AppointmentSourceService sources, AppointmentCargoVerificationMapper mapper,
|
||||||
ObjectMapper json) {
|
ObjectMapper json, AppointmentCargoReferenceRepository references) {
|
||||||
this.sources=sources; this.mapper=mapper; this.json=json;
|
this.sources=sources; this.mapper=mapper; this.json=json; this.references=references;
|
||||||
}
|
}
|
||||||
public record Save(@NotBlank @Size(max=160) String destinationKey, @Size(max=160) String expectedSourceVersion,
|
public record Save(@NotBlank @Size(max=160) String destinationKey, @Size(max=160) String expectedSourceVersion,
|
||||||
@Min(0) int expectedVersion, @Positive Integer packageCount,
|
@Min(0) int expectedVersion, @Positive Integer packageCount,
|
||||||
@@ -34,7 +36,7 @@ public class AppointmentCargoVerificationService {
|
|||||||
Integer packageCount, List<String> shipmentReferences, String remark,
|
Integer packageCount, List<String> shipmentReferences, String remark,
|
||||||
String operatorName, Instant verifiedAt) { }
|
String operatorName, Instant verifiedAt) { }
|
||||||
public record Context(AppointmentSourceService.Source source, Verification verification,
|
public record Context(AppointmentSourceService.Source source, Verification verification,
|
||||||
long allocatedPackages, long unknownAllocationCount, Integer effectivePackages,
|
long allocatedPackages, long unknownAllocationCount, Integer effectivePackages, List<String> effectiveShipmentReferences,
|
||||||
Long availablePackages, boolean sourceChanged, List<String> allowedActions,
|
Long availablePackages, boolean sourceChanged, List<String> allowedActions,
|
||||||
List<String> blockReasons) { }
|
List<String> blockReasons) { }
|
||||||
|
|
||||||
@@ -50,12 +52,13 @@ public class AppointmentCargoVerificationService {
|
|||||||
public record Inspection(AppointmentCoverageService.SourceRef reference,Context context,ResponseStatusException error) {
|
public record Inspection(AppointmentCoverageService.SourceRef reference,Context context,ResponseStatusException error) {
|
||||||
public Context requireContext() { if(error!=null) throw error; return context; }
|
public Context requireContext() { if(error!=null) throw error; return context; }
|
||||||
}
|
}
|
||||||
/** Shares source and warehouse reads within this call only; never caches mutable business facts across requests. */
|
/** 在本次批读内共用来源、仓库及原货件资料,不缓存可变业务事实。 */
|
||||||
public List<Inspection> inspectBatch(List<AppointmentCoverageService.SourceRef> refs) {
|
public List<Inspection> inspectBatch(List<AppointmentCoverageService.SourceRef> refs) {
|
||||||
var result=new ArrayList<Inspection>();
|
var result=new ArrayList<Inspection>();
|
||||||
for(int start=0;start<refs.size();start+=100) {
|
for(int start=0;start<refs.size();start+=100) {
|
||||||
var batch=refs.subList(start,Math.min(start+100,refs.size()));
|
var batch=refs.subList(start,Math.min(start+100,refs.size()));
|
||||||
var resolved=sources.inspectBatch(batch);
|
var resolved=sources.inspectBatch(batch);
|
||||||
|
var originals=originalShipmentReferences(resolved.stream().filter(r->r.error()==null).map(AppointmentSourceService.Resolution::source).toList());
|
||||||
var valid=resolved.stream().filter(r->r.error()==null).map(AppointmentSourceService.Resolution::reference).toList();
|
var valid=resolved.stream().filter(r->r.error()==null).map(AppointmentSourceService.Resolution::reference).toList();
|
||||||
var latest=new HashMap<List<String>,AppointmentCargoVerification>();
|
var latest=new HashMap<List<String>,AppointmentCargoVerification>();
|
||||||
var allocations=new HashMap<List<String>,AppointmentCargoVerificationMapper.Allocation>();
|
var allocations=new HashMap<List<String>,AppointmentCargoVerificationMapper.Allocation>();
|
||||||
@@ -65,18 +68,19 @@ public class AppointmentCargoVerificationService {
|
|||||||
}
|
}
|
||||||
for(var resolution:resolved) {
|
for(var resolution:resolved) {
|
||||||
var key=List.of(resolution.reference().customerId(),resolution.reference().sourceKey());
|
var key=List.of(resolution.reference().customerId(),resolution.reference().sourceKey());
|
||||||
result.add(new Inspection(resolution.reference(),resolution.error()==null?inspect(resolution.source(),latest.get(key),allocations.getOrDefault(key,new AppointmentCargoVerificationMapper.Allocation(0,0))):null,resolution.error()));
|
result.add(new Inspection(resolution.reference(),resolution.error()==null?inspect(resolution.source(),latest.get(key),allocations.getOrDefault(key,new AppointmentCargoVerificationMapper.Allocation(0,0)),originals.getOrDefault(key,List.of())):null,resolution.error()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return List.copyOf(result);
|
return List.copyOf(result);
|
||||||
}
|
}
|
||||||
/** Uses an already-authorized batched source resolution; never repeats warehouse HTTP lookups per row. */
|
/** 使用已授权来源读取本票核实记录及原有货件资料。 */
|
||||||
public Context inspect(AppointmentSourceService.Source source) {
|
public Context inspect(AppointmentSourceService.Source source) {
|
||||||
var row=mapper.latest(source.customerId(),source.sourceKey());
|
var row=mapper.latest(source.customerId(),source.sourceKey());
|
||||||
var allocation=mapper.allocated(source.customerId(),source.sourceKey());
|
var allocation=mapper.allocated(source.customerId(),source.sourceKey());
|
||||||
return inspect(source,row,allocation);
|
return inspect(source,row,allocation,originalShipmentReferences(List.of(source)).getOrDefault(List.of(source.customerId(),source.sourceKey()),List.of()));
|
||||||
}
|
}
|
||||||
private Context inspect(AppointmentSourceService.Source source,AppointmentCargoVerification row,AppointmentCargoVerificationMapper.Allocation allocation) {
|
/** 已保存的货件核实值优先;尚未补录货件时带入原货物的已有编号。 */
|
||||||
|
private Context inspect(AppointmentSourceService.Source source,AppointmentCargoVerification row,AppointmentCargoVerificationMapper.Allocation allocation,List<String> originals) {
|
||||||
boolean changed=row!=null && changed(row,source);
|
boolean changed=row!=null && changed(row,source);
|
||||||
Integer total=row==null?source.packages():row.getPackageCount();
|
Integer total=row==null?source.packages():row.getPackageCount();
|
||||||
var reasons=new ArrayList<>(source.blockReasons());
|
var reasons=new ArrayList<>(source.blockReasons());
|
||||||
@@ -88,10 +92,17 @@ public class AppointmentCargoVerificationService {
|
|||||||
?null:total.longValue()-allocation.packages();
|
?null:total.longValue()-allocation.packages();
|
||||||
// Actions describe this endpoint's business capability; controller permissions remain mandatory.
|
// Actions describe this endpoint's business capability; controller permissions remain mandatory.
|
||||||
var actions=source.canFollowup()?List.of("VERIFY_CARGO"):List.<String>of();
|
var actions=source.canFollowup()?List.of("VERIFY_CARGO"):List.<String>of();
|
||||||
return new Context(source,row==null?null:view(row),allocation.packages(),allocation.unknownItems(),total,
|
var verified=row==null?null:view(row);
|
||||||
|
var effectiveReferences=verified==null || verified.shipmentReferences().isEmpty()?originals:verified.shipmentReferences();
|
||||||
|
return new Context(source,verified,allocation.packages(),allocation.unknownItems(),total,effectiveReferences,
|
||||||
available,changed,actions,List.copyOf(reasons));
|
available,changed,actions,List.copyOf(reasons));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 对预约明细和核实表单提供同一批原始货件资料,不改写原订单。 */
|
||||||
|
public Map<List<String>,List<String>> originalShipmentReferences(List<AppointmentSourceService.Source> sources) {
|
||||||
|
return references.originalReferences(sources);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(isolation=Isolation.READ_COMMITTED)
|
@Transactional(isolation=Isolation.READ_COMMITTED)
|
||||||
public Verification save(String sourceKey,String customerId,Save request,String operatorId,String operatorName) {
|
public Verification save(String sourceKey,String customerId,Save request,String operatorId,String operatorName) {
|
||||||
validate(request,operatorId);
|
validate(request,operatorId);
|
||||||
|
|||||||
+4
-2
@@ -20,6 +20,7 @@ public class AppointmentCoverageService {
|
|||||||
this.mapper=mapper; this.external=external;
|
this.mapper=mapper; this.external=external;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按真实货物与外部约号核对覆盖,确认时段缺失不伪造时间也不撤销已确认事实。 */
|
||||||
public List<Coverage> query(List<SourceRef> input) {
|
public List<Coverage> query(List<SourceRef> input) {
|
||||||
if (input==null || input.isEmpty()) return List.of();
|
if (input==null || input.isEmpty()) return List.of();
|
||||||
if (input.size()>100 || input.stream().anyMatch(s -> s==null || blank(s.customerId()) || blank(s.sourceKey()) || blank(s.destinationKey()))) {
|
if (input.size()>100 || input.stream().anyMatch(s -> s==null || blank(s.customerId()) || blank(s.sourceKey()) || blank(s.destinationKey()))) {
|
||||||
@@ -85,7 +86,7 @@ public class AppointmentCoverageService {
|
|||||||
for (var fact:relevant) {
|
for (var fact:relevant) {
|
||||||
if (fact.orderId()!=null && !Objects.equals(order.orderId(),fact.orderId()) || !Objects.equals(order.fbaWarehouseCode(),fact.warehouseCode()) || fact.packages()==null || fact.packages()<=0 || !seen.add(fact.itemId())) { invalid=true; continue; }
|
if (fact.orderId()!=null && !Objects.equals(order.orderId(),fact.orderId()) || !Objects.equals(order.fbaWarehouseCode(),fact.warehouseCode()) || fact.packages()==null || fact.packages()<=0 || !seen.add(fact.itemId())) { invalid=true; continue; }
|
||||||
covered+=fact.packages();
|
covered+=fact.packages();
|
||||||
boolean success="SUCCESS".equals(fact.status()) && fact.appointmentAt()!=null && !blank(fact.confirmationNo()) && !blank(fact.timezone());
|
boolean success="SUCCESS".equals(fact.status()) && !blank(fact.confirmationNo()) && !blank(fact.timezone());
|
||||||
if (success) confirmed+=fact.packages();
|
if (success) confirmed+=fact.packages();
|
||||||
distinct.putIfAbsent(fact.appointmentNo(),new Appointment(fact.appointmentNo(),success ? fact.confirmationNo() : null,fact.status(),success ? fact.appointmentAt() : null,success ? fact.appointmentEndAt() : null,fact.timezone()));
|
distinct.putIfAbsent(fact.appointmentNo(),new Appointment(fact.appointmentNo(),success ? fact.confirmationNo() : null,fact.status(),success ? fact.appointmentAt() : null,success ? fact.appointmentEndAt() : null,fact.timezone()));
|
||||||
changed|=!Objects.equals(currentVersion,fact.sourceVersion());
|
changed|=!Objects.equals(currentVersion,fact.sourceVersion());
|
||||||
@@ -99,7 +100,8 @@ public class AppointmentCoverageService {
|
|||||||
else if (confirmed<packages) { status="PENDING"; missing.add("RECEIVING_CONFIRMATION"); }
|
else if (confirmed<packages) { status="PENDING"; missing.add("RECEIVING_CONFIRMATION"); }
|
||||||
else { status="CONFIRMED"; }
|
else { status="CONFIRMED"; }
|
||||||
if (relevant.stream().anyMatch(f->"SUCCESS".equals(f.status()) && blank(f.timezone()))) missing.add("APPOINTMENT_TIMEZONE_UNVERIFIED");
|
if (relevant.stream().anyMatch(f->"SUCCESS".equals(f.status()) && blank(f.timezone()))) missing.add("APPOINTMENT_TIMEZONE_UNVERIFIED");
|
||||||
if (evidence==null ? order.shipmentReferencesMissing() : !hasReferences(evidence.getShipmentReferencesJson())) missing.add("FBA_SHIPMENT_REFERENCES");
|
// 只补录过箱数的核实记录不能遮蔽原订单已经完整提供的货件号。
|
||||||
|
if (order.shipmentReferencesMissing() && (evidence==null || !hasReferences(evidence.getShipmentReferencesJson()))) missing.add("FBA_SHIPMENT_REFERENCES");
|
||||||
if (changed) missing.add("SOURCE_CHANGED_REVIEW");
|
if (changed) missing.add("SOURCE_CHANGED_REVIEW");
|
||||||
output.add(new Coverage(source.customerId(),source.sourceKey(),source.destinationKey(),status,List.copyOf(distinct.values()),List.copyOf(missing),
|
output.add(new Coverage(source.customerId(),source.sourceKey(),source.destinationKey(),status,List.copyOf(distinct.values()),List.copyOf(missing),
|
||||||
(booking==null ? "" : "BOOKING:"+version(booking.recordVersion())+"/")+"ORDER:"+version(order.version()),changed || invalid));
|
(booking==null ? "" : "BOOKING:"+version(booking.recordVersion())+"/")+"ORDER:"+version(order.version()),changed || invalid));
|
||||||
|
|||||||
+6
-2
@@ -7,15 +7,19 @@ import java.time.ZoneId;
|
|||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/** Resolves a location's appointment window without using the operator's system time zone. */
|
/** 解析预约地点的真实确认时段,不使用操作人的系统时区或虚构确认时间。 */
|
||||||
public final class AppointmentTimePolicy {
|
public final class AppointmentTimePolicy {
|
||||||
private AppointmentTimePolicy() { }
|
private AppointmentTimePolicy() { }
|
||||||
|
|
||||||
public record Window(Instant start, Instant end, String timezone) { }
|
public record Window(Instant start, Instant end, String timezone) { }
|
||||||
|
|
||||||
|
/** 未提供确认时段时保持为空;提供时仍校验地区时区、夏令时及先后顺序。 */
|
||||||
public static Window resolve(LocalDateTime start, LocalDateTime end, String timezone,
|
public static Window resolve(LocalDateTime start, LocalDateTime end, String timezone,
|
||||||
ZoneOffset startOffset, ZoneOffset endOffset) {
|
ZoneOffset startOffset, ZoneOffset endOffset) {
|
||||||
if (start == null) throw new IllegalArgumentException("请填写预约开始时间");
|
if (start == null) {
|
||||||
|
if (end != null || startOffset != null || endOffset != null) throw new IllegalArgumentException("未填写确认开始时间,不能填写确认结束时间或时差");
|
||||||
|
return new Window(null, null, timezone);
|
||||||
|
}
|
||||||
if (timezone == null || timezone.isBlank()) throw new IllegalArgumentException("请先补齐预约地点时区");
|
if (timezone == null || timezone.isBlank()) throw new IllegalArgumentException("请先补齐预约地点时区");
|
||||||
final ZoneId zone;
|
final ZoneId zone;
|
||||||
try { zone = ZoneId.of(timezone); }
|
try { zone = ZoneId.of(timezone); }
|
||||||
|
|||||||
+2
-1
@@ -18,6 +18,7 @@ public interface AppointmentWorkDispatchPort {
|
|||||||
Map<CandidatePhysical,CandidateBatches> candidateBatches(List<CandidatePhysical> candidates);
|
Map<CandidatePhysical,CandidateBatches> candidateBatches(List<CandidatePhysical> candidates);
|
||||||
/** 只读取本车已选实物的逐票预约选项;本车原占用不扣减本车可重新分配量。 */
|
/** 只读取本车已选实物的逐票预约选项;本车原占用不扣减本车可重新分配量。 */
|
||||||
Map<CandidatePhysical,List<AllocationSource>> allocationOptions(long dispatchId,List<CandidatePhysical> items);
|
Map<CandidatePhysical,List<AllocationSource>> allocationOptions(long dispatchId,List<CandidatePhysical> items);
|
||||||
|
/** 正式确认的已知时段原样返回;未提供确认时间时保持为空。 */
|
||||||
record AllocationOption(String batchKey,String batchNo,Long recordVersion,int availablePackages,
|
record AllocationOption(String batchKey,String batchNo,Long recordVersion,int availablePackages,
|
||||||
String confirmationNo,String appointmentAt,String appointmentEndAt,String timezone) { }
|
String confirmationNo,String appointmentAt,String appointmentEndAt,String timezone) { }
|
||||||
record AllocationSource(String sourceKey,String orderNo,String inboundCode,String destinationKey,Integer packageCount,
|
record AllocationSource(String sourceKey,String orderNo,String inboundCode,String destinationKey,Integer packageCount,
|
||||||
@@ -33,7 +34,7 @@ public interface AppointmentWorkDispatchPort {
|
|||||||
String departureBlockReason(String dispatchNo);
|
String departureBlockReason(String dispatchNo);
|
||||||
record ReceivingWindow(String batchKey,String batchNo,String customerId,String destinationKey,String confirmationNo,
|
record ReceivingWindow(String batchKey,String batchNo,String customerId,String destinationKey,String confirmationNo,
|
||||||
java.time.Instant startAt,java.time.Instant endAt,String agreementKey,int version) { }
|
java.time.Instant startAt,java.time.Instant endAt,String agreementKey,int version) { }
|
||||||
/** FBA选入前须已有正式时间确认;提交审核、审核通过和开始拣货继续复核运输安排。 */
|
/** FBA选入前须已有正式外部确认;提交审核、审核通过和开始拣货继续复核运输安排及已知时段。 */
|
||||||
String reviewBlockReason(String dispatchNo,java.time.Instant plannedDepartureAt,java.time.Instant estimatedArrivalAt);
|
String reviewBlockReason(String dispatchNo,java.time.Instant plannedDepartureAt,java.time.Instant estimatedArrivalAt);
|
||||||
List<ReceivingWindow> reviewEvidence(String dispatchNo);
|
List<ReceivingWindow> reviewEvidence(String dispatchNo);
|
||||||
void removeItem(long dispatchId,long itemId);
|
void removeItem(long dispatchId,long itemId);
|
||||||
|
|||||||
+17
-8
@@ -381,7 +381,7 @@ public class InboundFbaAppointmentService {
|
|||||||
return response(row,items(id));
|
return response(row,items(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Explicit adoption updates only the chosen customer's evidence, never their allocated amount. */
|
/** 显式采用本客户最新货物资料及有效货件号,保留已分配数量和其他客户快照。 */
|
||||||
@Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY)
|
@Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||||
public FbaAppointmentResponse refreshReceivingCargo(String key,String customerId,List<com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CargoVersion> expected) {
|
public FbaAppointmentResponse refreshReceivingCargo(String key,String customerId,List<com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CargoVersion> expected) {
|
||||||
var snapshot=lockReceivingBatch(key);var row=appointmentMapper.selectById(snapshot.id());
|
var snapshot=lockReceivingBatch(key);var row=appointmentMapper.selectById(snapshot.id());
|
||||||
@@ -395,6 +395,7 @@ public class InboundFbaAppointmentService {
|
|||||||
}
|
}
|
||||||
var refs=selected.stream().map(i->new AppointmentCoverageService.SourceRef(i.getCustomerId(),i.getSourceKey(),"FBA:"+i.getFbaWarehouseCode())).toList();
|
var refs=selected.stream().map(i->new AppointmentCoverageService.SourceRef(i.getCustomerId(),i.getSourceKey(),"FBA:"+i.getFbaWarehouseCode())).toList();
|
||||||
var resolved=resolveForWrite(refs);var evidence=verification(resolved);
|
var resolved=resolveForWrite(refs);var evidence=verification(resolved);
|
||||||
|
var originals=verification.originalShipmentReferences(resolved);
|
||||||
var bySource=resolved.stream().collect(Collectors.toMap(AppointmentSourceService.Source::sourceKey,s->s));
|
var bySource=resolved.stream().collect(Collectors.toMap(AppointmentSourceService.Source::sourceKey,s->s));
|
||||||
for(var item:selected) {
|
for(var item:selected) {
|
||||||
var requested=versions.get(item.getSourceKey());var source=bySource.get(item.getSourceKey());
|
var requested=versions.get(item.getSourceKey());var source=bySource.get(item.getSourceKey());
|
||||||
@@ -407,7 +408,7 @@ public class InboundFbaAppointmentService {
|
|||||||
var other=queryMapper.allocation(customerId,source.sourceKey(),source.orderId(),row.getId());
|
var other=queryMapper.allocation(customerId,source.sourceKey(),source.orderId(),row.getId());
|
||||||
if(count==null || count<=0 || total==null || other.unknownCount()>0 || (long)count+other.packages()>total) throw conflict("最新核实量不足以覆盖当前全部批次,请先处理数量差异,不能直接采用资料");
|
if(count==null || count<=0 || total==null || other.unknownCount()>0 || (long)count+other.packages()>total) throw conflict("最新核实量不足以覆盖当前全部批次,请先处理数量差异,不能直接采用资料");
|
||||||
item.setSourceVersion(source.sourceVersion());item.setCargoVerificationVersion(verified==null?null:verified.version());
|
item.setSourceVersion(source.sourceVersion());item.setCargoVerificationVersion(verified==null?null:verified.version());
|
||||||
try {item.setShipmentReferencesJson(verified==null?null:json.writeValueAsString(verified.shipmentReferences()));}
|
try {item.setShipmentReferencesJson(json.writeValueAsString(effectiveShipmentReferences(source,verified,originals)));}
|
||||||
catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("核实货件资料无法保存",error);}
|
catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("核实货件资料无法保存",error);}
|
||||||
}
|
}
|
||||||
if(snapshot.equals(response(row,current))) return snapshot;
|
if(snapshot.equals(response(row,current))) return snapshot;
|
||||||
@@ -497,7 +498,7 @@ public class InboundFbaAppointmentService {
|
|||||||
if(!groups.isEmpty() && groups.size()==active.size() && active.stream().map(a->a.agreementKey()).distinct().count()==1) {
|
if(!groups.isEmpty() && groups.size()==active.size() && active.stream().map(a->a.agreementKey()).distinct().count()==1) {
|
||||||
var a=active.get(0);var d=a.details();row.setAppointmentStatus("SUCCESS");row.setConfirmationNo(d.confirmationNo());row.setAppointmentAt(d.appointmentAt());row.setAppointmentEndAt(d.appointmentEndAt());row.setTimezone(a.timezone());
|
var a=active.get(0);var d=a.details();row.setAppointmentStatus("SUCCESS");row.setConfirmationNo(d.confirmationNo());row.setAppointmentAt(d.appointmentAt());row.setAppointmentEndAt(d.appointmentEndAt());row.setTimezone(a.timezone());
|
||||||
var window=AppointmentTimePolicy.resolve(d.appointmentAt(),d.appointmentEndAt(),a.timezone(),offset(d.startOffset()),offset(d.endOffset()));
|
var window=AppointmentTimePolicy.resolve(d.appointmentAt(),d.appointmentEndAt(),a.timezone(),offset(d.startOffset()),offset(d.endOffset()));
|
||||||
row.setAppointmentStartUtc(LocalDateTime.ofInstant(window.start(),ZoneOffset.UTC));row.setAppointmentEndUtc(window.end()==null?null:LocalDateTime.ofInstant(window.end(),ZoneOffset.UTC));
|
row.setAppointmentStartUtc(window.start()==null?null:LocalDateTime.ofInstant(window.start(),ZoneOffset.UTC));row.setAppointmentEndUtc(window.end()==null?null:LocalDateTime.ofInstant(window.end(),ZoneOffset.UTC));
|
||||||
row.setPlannedDate(d.plannedDate());row.setPlannedAt(d.plannedAt());row.setPlannedEndAt(d.plannedEndAt());
|
row.setPlannedDate(d.plannedDate());row.setPlannedAt(d.plannedAt());row.setPlannedEndAt(d.plannedEndAt());
|
||||||
} else {
|
} else {
|
||||||
var plans=receiving.plans(key).stream().collect(Collectors.toMap(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CustomerPlan::customerId,p->p));
|
var plans=receiving.plans(key).stream().collect(Collectors.toMap(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CustomerPlan::customerId,p->p));
|
||||||
@@ -562,6 +563,7 @@ public class InboundFbaAppointmentService {
|
|||||||
var verified=evidence.get(source.sourceKey());
|
var verified=evidence.get(source.sourceKey());
|
||||||
return verified==null?source.packages():verified.packageCount();
|
return verified==null?source.packages():verified.packageCount();
|
||||||
}
|
}
|
||||||
|
/** 保存外部约号及真实已知时段;未提供确认时间时不从计划日期伪造。 */
|
||||||
private void apply(InboundFbaAppointment appointment,FbaAppointmentDetails details,List<AppointmentSourceService.Source> rows,
|
private void apply(InboundFbaAppointment appointment,FbaAppointmentDetails details,List<AppointmentSourceService.Source> rows,
|
||||||
Map<String,AppointmentCargoVerificationService.Verification> evidence) {
|
Map<String,AppointmentCargoVerificationService.Verification> evidence) {
|
||||||
String status=status(details.appointmentStatus());
|
String status=status(details.appointmentStatus());
|
||||||
@@ -572,26 +574,27 @@ public class InboundFbaAppointmentService {
|
|||||||
appointment.setPlannedAt(details.plannedAt());appointment.setPlannedEndAt(details.plannedEndAt());appointment.setTimezone(timezone);
|
appointment.setPlannedAt(details.plannedAt());appointment.setPlannedEndAt(details.plannedEndAt());appointment.setTimezone(timezone);
|
||||||
appointment.setAppointmentStatus(status);appointment.setAppointmentRemark(clean(details.appointmentRemark()));
|
appointment.setAppointmentStatus(status);appointment.setAppointmentRemark(clean(details.appointmentRemark()));
|
||||||
if("SUCCESS".equals(status)) {
|
if("SUCCESS".equals(status)) {
|
||||||
if(clean(details.confirmationNo())==null || details.appointmentAt()==null) throw bad("登记对方确认时请填写外部约号和确认时间");
|
if(clean(details.confirmationNo())==null) throw bad("登记对方确认时请填写外部约号");
|
||||||
var ids=rows.stream().map(AppointmentSourceService.Source::orderId).filter(Objects::nonNull).distinct().toList();
|
var ids=rows.stream().map(AppointmentSourceService.Source::orderId).filter(Objects::nonNull).distinct().toList();
|
||||||
var unverifiedIds=rows.stream().filter(s->!evidence.containsKey(s.sourceKey())).map(AppointmentSourceService.Source::orderId).filter(Objects::nonNull).distinct().toList();
|
var unverifiedIds=rows.stream().filter(s->!evidence.containsKey(s.sourceKey()) || evidence.get(s.sourceKey()).shipmentReferences().isEmpty()).map(AppointmentSourceService.Source::orderId).filter(Objects::nonNull).distinct().toList();
|
||||||
if(ids.size()!=rows.size() || rows.stream().anyMatch(s->quantity(s,evidence)==null || quantity(s,evidence)<=0)
|
if(ids.size()!=rows.size() || rows.stream().anyMatch(s->quantity(s,evidence)==null || quantity(s,evidence)<=0)
|
||||||
|| evidence.values().stream().anyMatch(v->v.shipmentReferences().isEmpty())
|
|
||||||
|| !unverifiedIds.isEmpty() && queryMapper.countMissingShipmentReferences(unverifiedIds)>0)
|
|| !unverifiedIds.isEmpty() && queryMapper.countMissingShipmentReferences(unverifiedIds)>0)
|
||||||
throw bad("正式确认前请核实货量并补齐各票FBA货件号或追踪号");
|
throw bad("正式确认前请核实货量并补齐各票FBA货件号或追踪号");
|
||||||
final AppointmentTimePolicy.Window window;
|
final AppointmentTimePolicy.Window window;
|
||||||
try {window=AppointmentTimePolicy.resolve(details.appointmentAt(),details.appointmentEndAt(),timezone,offset(details.startOffset()),offset(details.endOffset()));}
|
try {window=AppointmentTimePolicy.resolve(details.appointmentAt(),details.appointmentEndAt(),timezone,offset(details.startOffset()),offset(details.endOffset()));}
|
||||||
catch(IllegalArgumentException | DateTimeException error) {throw bad(error.getMessage());}
|
catch(IllegalArgumentException | DateTimeException error) {throw bad(error.getMessage());}
|
||||||
appointment.setConfirmationNo(details.confirmationNo().trim());appointment.setAppointmentAt(details.appointmentAt());appointment.setAppointmentEndAt(details.appointmentEndAt());
|
appointment.setConfirmationNo(details.confirmationNo().trim());appointment.setAppointmentAt(details.appointmentAt());appointment.setAppointmentEndAt(details.appointmentEndAt());
|
||||||
appointment.setAppointmentStartUtc(LocalDateTime.ofInstant(window.start(),ZoneOffset.UTC));appointment.setAppointmentEndUtc(window.end()==null?null:LocalDateTime.ofInstant(window.end(),ZoneOffset.UTC));
|
appointment.setAppointmentStartUtc(window.start()==null?null:LocalDateTime.ofInstant(window.start(),ZoneOffset.UTC));appointment.setAppointmentEndUtc(window.end()==null?null:LocalDateTime.ofInstant(window.end(),ZoneOffset.UTC));
|
||||||
} else {
|
} else {
|
||||||
if(clean(details.confirmationNo())!=null || details.appointmentAt()!=null || details.appointmentEndAt()!=null) throw bad("待确认安排请填写计划时间;取得对方确认后再登记外部约号和确认时段");
|
if(clean(details.confirmationNo())!=null || details.appointmentAt()!=null || details.appointmentEndAt()!=null) throw bad("待确认安排请填写计划时间;取得对方确认后再登记外部约号和确认时段");
|
||||||
appointment.setConfirmationNo(null);appointment.setAppointmentAt(null);appointment.setAppointmentEndAt(null);appointment.setAppointmentStartUtc(null);appointment.setAppointmentEndUtc(null);
|
appointment.setConfirmationNo(null);appointment.setAppointmentAt(null);appointment.setAppointmentEndAt(null);appointment.setAppointmentStartUtc(null);appointment.setAppointmentEndUtc(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** 固化本次货量及有效货件号,优先核实记录,未补录时采用原货物资料。 */
|
||||||
private void writeItems(InboundFbaAppointment appointment,List<AppointmentSourceService.Source> rows,List<InboundFbaAppointmentItem> previous,
|
private void writeItems(InboundFbaAppointment appointment,List<AppointmentSourceService.Source> rows,List<InboundFbaAppointmentItem> previous,
|
||||||
Map<String,AppointmentCargoVerificationService.Verification> evidence,Map<String,Integer> requested) {
|
Map<String,AppointmentCargoVerificationService.Verification> evidence,Map<String,Integer> requested) {
|
||||||
var existing=previous.stream().collect(Collectors.toMap(InboundFbaAppointmentItem::getSourceKey,item->item));
|
var existing=previous.stream().collect(Collectors.toMap(InboundFbaAppointmentItem::getSourceKey,item->item));
|
||||||
|
var originals=verification.originalShipmentReferences(rows);
|
||||||
for(var source:rows) {
|
for(var source:rows) {
|
||||||
var item=existing.get(source.sourceKey());boolean fresh=item==null;
|
var item=existing.get(source.sourceKey());boolean fresh=item==null;
|
||||||
var allocated=queryMapper.allocation(source.customerId(),source.sourceKey(),source.orderId(),appointment.getId());
|
var allocated=queryMapper.allocation(source.customerId(),source.sourceKey(),source.orderId(),appointment.getId());
|
||||||
@@ -611,12 +614,18 @@ public class InboundFbaAppointmentService {
|
|||||||
item.setFbaWarehouseCode(source.destinationWarehouseCode());item.setPackageCount(count);item.setActive(true);
|
item.setFbaWarehouseCode(source.destinationWarehouseCode());item.setPackageCount(count);item.setActive(true);
|
||||||
var verified=evidence.get(source.sourceKey());
|
var verified=evidence.get(source.sourceKey());
|
||||||
item.setCargoVerificationVersion(verified==null?null:verified.version());
|
item.setCargoVerificationVersion(verified==null?null:verified.version());
|
||||||
try {item.setShipmentReferencesJson(verified==null?null:json.writeValueAsString(verified.shipmentReferences()));}
|
var references=effectiveShipmentReferences(source,verified,originals);
|
||||||
|
try {item.setShipmentReferencesJson(json.writeValueAsString(references));}
|
||||||
catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("核实货件资料无法保存",error);}
|
catch(com.fasterxml.jackson.core.JsonProcessingException error) {throw new IllegalStateException("核实货件资料无法保存",error);}
|
||||||
try {if(fresh)itemMapper.insert(item);else itemMapper.updateById(item);}
|
try {if(fresh)itemMapper.insert(item);else itemMapper.updateById(item);}
|
||||||
catch(DuplicateKeyException duplicate){throw conflict("所选货物刚被其他安排占用,请刷新后核对");}
|
catch(DuplicateKeyException duplicate){throw conflict("所选货物刚被其他安排占用,请刷新后核对");}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/** 新建及采用最新资料均优先非空核实编号,未补录时使用同客户原订单编号。 */
|
||||||
|
private static List<String> effectiveShipmentReferences(AppointmentSourceService.Source source,AppointmentCargoVerificationService.Verification verified,
|
||||||
|
Map<List<String>,List<String>> originals) {
|
||||||
|
return verified==null || verified.shipmentReferences().isEmpty()?originals.getOrDefault(List.of(source.customerId(),source.sourceKey()),List.of()):verified.shipmentReferences();
|
||||||
|
}
|
||||||
private FbaAppointmentResponse command(String operation,String operator,String key,Object request,Supplier<FbaAppointmentResponse> action) {
|
private FbaAppointmentResponse command(String operation,String operator,String key,Object request,Supplier<FbaAppointmentResponse> action) {
|
||||||
return executeCommand(operation,operator,key,request,FbaAppointmentResponse.class,FbaAppointmentResponse::appointmentNo,action);
|
return executeCommand(operation,operator,key,request,FbaAppointmentResponse.class,FbaAppointmentResponse::appointmentNo,action);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -43,14 +43,14 @@ public class AppointmentDispatchAllocationReader {
|
|||||||
int confirmed=availability.confirmedPackages(confirmations.get(b.batchKey()),b.customerId(),source,p.destinationKey());
|
int confirmed=availability.confirmedPackages(confirmations.get(b.batchKey()),b.customerId(),source,p.destinationKey());
|
||||||
long remaining=b.packages()==null?0:Math.min(b.packages(),confirmed)-b.allocated();
|
long remaining=b.packages()==null?0:Math.min(b.packages(),confirmed)-b.allocated();
|
||||||
if(remaining>0 && b.recordVersion()!=null && b.recordVersion()>=0) {
|
if(remaining>0 && b.recordVersion()!=null && b.recordVersion()>=0) {
|
||||||
var agreement=confirmations.get(b.batchKey()).agreements().stream().filter(a->"ACTIVE".equals(a.status())
|
var agreement=confirmations.get(b.batchKey()).agreements().stream().filter(a->"ACTIVE".equals(a.status()) && b.destinationKey().equals(a.destinationKey())
|
||||||
&& a.members().stream().anyMatch(m->b.batchKey().equals(m.batchKey()) && b.customerId().equals(m.customerId()))).findFirst().orElseThrow();
|
&& a.members().stream().anyMatch(m->b.batchKey().equals(m.batchKey()) && b.customerId().equals(m.customerId()))).findFirst().orElseThrow();
|
||||||
var details=agreement.details();
|
var details=agreement.details();
|
||||||
options.add(new AllocationOption(b.batchKey(),b.batchNo(),b.recordVersion(),Math.toIntExact(remaining),details.confirmationNo(),
|
options.add(new AllocationOption(b.batchKey(),b.batchNo(),b.recordVersion(),Math.toIntExact(remaining),details.confirmationNo(),
|
||||||
details.appointmentAt().toString(),details.appointmentEndAt()==null?null:details.appointmentEndAt().toString(),agreement.timezone()));
|
details.appointmentAt()==null?null:details.appointmentAt().toString(),details.appointmentEndAt()==null?null:details.appointmentEndAt().toString(),agreement.timezone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(options.stream().mapToLong(AllocationOption::availablePackages).sum()<p.packages())reasons.add("请先在预约列表确认本票FBA送仓时间及覆盖箱数,再返回刷新");
|
if(options.stream().mapToLong(AllocationOption::availablePackages).sum()<p.packages())reasons.add("请先在预约列表登记本票FBA外部约号及覆盖箱数,再返回刷新");
|
||||||
}
|
}
|
||||||
rows.add(new AllocationSource(source,p.orderNo(),p.inboundCode(),p.destinationKey(),p.packages(),List.copyOf(options),List.copyOf(reasons)));
|
rows.add(new AllocationSource(source,p.orderNo(),p.inboundCode(),p.destinationKey(),p.packages(),List.copyOf(options),List.copyOf(reasons)));
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-17
@@ -1,5 +1,6 @@
|
|||||||
package com.baiyue.lastleg.inboundplan.work;
|
package com.baiyue.lastleg.inboundplan.work;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.appointmentplanning.AppointmentTimePolicy;
|
||||||
import com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore;
|
import com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore;
|
||||||
import com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.*;
|
import com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -18,32 +19,22 @@ public class AppointmentDispatchAvailability {
|
|||||||
for(int offset=0;offset<unique.size();offset+=200) for(var snapshot:receiving.snapshots(unique.subList(offset,Math.min(offset+200,unique.size()))))result.put(snapshot.batchKey(),snapshot);
|
for(int offset=0;offset<unique.size();offset+=200) for(var snapshot:receiving.snapshots(unique.subList(offset,Math.min(offset+200,unique.size()))))result.put(snapshot.batchKey(),snapshot);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
/** 未确认、待仓库回复、时间无效或覆盖身份不唯一时均不可作为排车依据。 */
|
/** 正式确认允许尚无时段;已填时段须合法,且客户、来源和目的仓覆盖必须唯一。 */
|
||||||
public int confirmedPackages(BatchReceiving snapshot,String customer,String source,String destination) {
|
public int confirmedPackages(BatchReceiving snapshot,String customer,String source,String destination) {
|
||||||
if(snapshot==null || snapshot.pendingChanges().stream().anyMatch(p->"WAITING".equals(p.status()) && customer.equals(p.intent().customerId())))return 0;
|
if(snapshot==null || snapshot.pendingChanges().stream().anyMatch(p->"WAITING".equals(p.status()) && customer.equals(p.intent().customerId())))return 0;
|
||||||
var agreements=snapshot.agreements().stream().filter(a->"ACTIVE".equals(a.status()) && destination.equals(a.destinationKey())
|
var agreements=snapshot.agreements().stream().filter(a->"ACTIVE".equals(a.status()) && destination.equals(a.destinationKey())
|
||||||
&& a.members().stream().anyMatch(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId()))).toList();
|
&& a.members().stream().anyMatch(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId()))).toList();
|
||||||
if(agreements.size()!=1)return 0;
|
if(agreements.size()!=1)return 0;
|
||||||
var agreement=agreements.get(0);var details=agreement.details();
|
var agreement=agreements.get(0);var details=agreement.details();
|
||||||
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.appointmentAt()==null)return 0;
|
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.confirmationNo()==null || details.confirmationNo().isBlank())return 0;
|
||||||
var start=confirmedInstant(details.appointmentAt(),agreement.timezone(),details.startOffset());
|
// 与保存正式确认共用可选时段规则,不从计划日期推造送仓时间。
|
||||||
if(start==null)return 0;
|
try {AppointmentTimePolicy.resolve(details.appointmentAt(),details.appointmentEndAt(),agreement.timezone(),offset(details.startOffset()),offset(details.endOffset()));}
|
||||||
if(details.appointmentEndAt()!=null) {
|
catch(IllegalArgumentException|DateTimeException error){return 0;}
|
||||||
var end=confirmedInstant(details.appointmentEndAt(),agreement.timezone(),details.endOffset());
|
|
||||||
if(end==null || end.isBefore(start))return 0;
|
|
||||||
}
|
|
||||||
var members=agreement.members().stream().filter(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId())).toList();
|
var members=agreement.members().stream().filter(m->snapshot.batchKey().equals(m.batchKey()) && customer.equals(m.customerId())).toList();
|
||||||
if(members.size()!=1)return 0;
|
if(members.size()!=1)return 0;
|
||||||
var sources=members.get(0).sources().stream().filter(s->source.equals(s.sourceKey()) && destination.equals(s.destinationKey())).toList();
|
var sources=members.get(0).sources().stream().filter(s->source.equals(s.sourceKey()) && destination.equals(s.destinationKey())).toList();
|
||||||
return sources.size()==1?Math.max(0,sources.get(0).packageCount()):0;
|
return sources.size()==1?Math.max(0,sources.get(0).packageCount()):0;
|
||||||
}
|
}
|
||||||
/** 夏令时歧义须有明确偏移;不能把当地时间按服务器时区猜测。 */
|
/** 留空的夏令时时差保持未知,已填写值由正式时段规则核验。 */
|
||||||
private Instant confirmedInstant(LocalDateTime at,String timezone,String offset) {
|
private ZoneOffset offset(String value) {return value==null || value.isBlank()?null:ZoneOffset.of(value.trim());}
|
||||||
try {
|
|
||||||
var offsets=ZoneId.of(timezone).getRules().getValidOffsets(at);
|
|
||||||
if(offsets.isEmpty() || offset==null && offsets.size()!=1)return null;
|
|
||||||
var selected=offset==null?offsets.get(0):ZoneOffset.of(offset);
|
|
||||||
return offsets.contains(selected)?at.toInstant(selected):null;
|
|
||||||
} catch(DateTimeException|NullPointerException error){return null;}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -31,6 +31,7 @@ public class AppointmentWorkAppointmentChanges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
record Receiving(Agreement agreement,Pending pending,Slot target,Slot confirmed,String timezone,boolean cancelled,String status) { }
|
record Receiving(Agreement agreement,Pending pending,Slot target,Slot confirmed,String timezone,boolean cancelled,String status) { }
|
||||||
|
/** 独立识别外部收货确认与其可选时段,只有真实填写的时间才生成确认日历槽位。 */
|
||||||
static Receiving receiving(FbaAppointmentResponse batch,String customer,BatchReceiving snapshot) {
|
static Receiving receiving(FbaAppointmentResponse batch,String customer,BatchReceiving snapshot) {
|
||||||
if(snapshot==null || !batch.batchKey().equals(snapshot.batchKey())) throw AppointmentWorkPolicy.unavailable("收货预约批量读取不完整");
|
if(snapshot==null || !batch.batchKey().equals(snapshot.batchKey())) throw AppointmentWorkPolicy.unavailable("收货预约批量读取不完整");
|
||||||
var groups=snapshot.agreements().stream().filter(a->"ACTIVE".equals(a.status()) && a.members().stream().anyMatch(m->batch.batchKey().equals(m.batchKey()) && customer.equals(m.customerId()))).toList();
|
var groups=snapshot.agreements().stream().filter(a->"ACTIVE".equals(a.status()) && a.members().stream().anyMatch(m->batch.batchKey().equals(m.batchKey()) && customer.equals(m.customerId()))).toList();
|
||||||
@@ -44,7 +45,7 @@ public class AppointmentWorkAppointmentChanges {
|
|||||||
String status=null,timezone=null;Slot target=null,confirmed=null;
|
String status=null,timezone=null;Slot target=null,confirmed=null;
|
||||||
if(agreement!=null) {
|
if(agreement!=null) {
|
||||||
var details=agreement.details();timezone=agreement.timezone();
|
var details=agreement.details();timezone=agreement.timezone();
|
||||||
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.appointmentAt()==null) throw AppointmentWorkPolicy.unavailable("有效收货预约缺少正式确认时间");
|
if(details==null || !"SUCCESS".equals(details.appointmentStatus()) || details.confirmationNo()==null || details.confirmationNo().isBlank()) throw AppointmentWorkPolicy.unavailable("有效收货预约缺少外部约号");
|
||||||
target=target(details,timezone);status=details.appointmentStatus();
|
target=target(details,timezone);status=details.appointmentStatus();
|
||||||
confirmed=slot(details.appointmentAt(),details.appointmentEndAt(),timezone);
|
confirmed=slot(details.appointmentAt(),details.appointmentEndAt(),timezone);
|
||||||
} else if(!plans.isEmpty()) {
|
} else if(!plans.isEmpty()) {
|
||||||
|
|||||||
+1
-1
@@ -98,7 +98,7 @@ public class AppointmentWorkDispatchCandidates {
|
|||||||
if(item==null) { reasons.add("托盘含本批之外的货物,请先拆托或选择完整的关联批次"); continue; }
|
if(item==null) { reasons.add("托盘含本批之外的货物,请先拆托或选择完整的关联批次"); continue; }
|
||||||
long used=mapper.allocated(batch,part.customerId(),ref.sourceKey(),part.destinationKey(),-1);
|
long used=mapper.allocated(batch,part.customerId(),ref.sourceKey(),part.destinationKey(),-1);
|
||||||
if(item.packageCount()==null || used+count>item.packageCount()) reasons.add("本托货量超过该来源本批剩余可排量,请先核对分批或拆托");
|
if(item.packageCount()==null || used+count>item.packageCount()) reasons.add("本托货量超过该来源本批剩余可排量,请先核对分批或拆托");
|
||||||
if(used+count>availability.confirmedPackages(confirmation,part.customerId(),ref.sourceKey(),part.destinationKey()))reasons.add("FBA货物须先预约成功并明确送仓时间,再加入排车");
|
if(used+count>availability.confirmedPackages(confirmation,part.customerId(),ref.sourceKey(),part.destinationKey()))reasons.add("FBA货物须先取得外部约号并确认覆盖箱数,再加入排车");
|
||||||
if(appointment.version()==null || appointment.version()<0) throw unavailable("原预约批次版本缺失");
|
if(appointment.version()==null || appointment.version()<0) throw unavailable("原预约批次版本缺失");
|
||||||
allocations.add(new Allocation(candidate.planPalletId(),batch,appointment.version().longValue(),part.customerId(),ref.sourceKey(),part.destinationKey(),count));
|
allocations.add(new Allocation(candidate.planPalletId(),batch,appointment.version().longValue(),part.customerId(),ref.sourceKey(),part.destinationKey(),count));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ components:
|
|||||||
allocatedPackages: {type: integer, format: int64}
|
allocatedPackages: {type: integer, format: int64}
|
||||||
unknownAllocationCount: {type: integer, format: int64}
|
unknownAllocationCount: {type: integer, format: int64}
|
||||||
effectivePackages: {type: integer, nullable: true, description: 独立核实箱数;没有核实记录时采用原来源箱数}
|
effectivePackages: {type: integer, nullable: true, description: 独立核实箱数;没有核实记录时采用原来源箱数}
|
||||||
|
effectiveShipmentReferences: {type: array, items: {type: string}, description: 有效货件号和追踪号;非空独立核实值优先,未补录货件时带入同客户原订单资料,去空去重;读取不会新增核实记录}
|
||||||
availablePackages: {type: integer, format: int64, nullable: true}
|
availablePackages: {type: integer, format: int64, nullable: true}
|
||||||
sourceChanged: {type: boolean}
|
sourceChanged: {type: boolean}
|
||||||
allowedActions: {type: array, items: {type: string, enum: [VERIFY_CARGO]}}
|
allowedActions: {type: array, items: {type: string, enum: [VERIFY_CARGO]}}
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ components:
|
|||||||
plannedAt: {type: string, nullable: true, description: 已知的计划开始当地时段,日期必须与plannedDate一致}
|
plannedAt: {type: string, nullable: true, description: 已知的计划开始当地时段,日期必须与plannedDate一致}
|
||||||
plannedEndAt: {type: string, nullable: true}
|
plannedEndAt: {type: string, nullable: true}
|
||||||
confirmationNo: {type: string, maxLength: 128, nullable: true, description: 仅SUCCESS登记收货方的外部约号;同一FBA仓不得重复}
|
confirmationNo: {type: string, maxLength: 128, nullable: true, description: 仅SUCCESS登记收货方的外部约号;同一FBA仓不得重复}
|
||||||
appointmentAt: {type: string, nullable: true, description: 对方确认开始当地时间;SUCCESS必填,PENDING不能当作计划填写}
|
appointmentAt: {type: string, nullable: true, description: 对方确认开始当地时间;SUCCESS可留空,缺失时不从计划日期推造,填写时须校验地区时区及夏令时;PENDING不能当作计划填写}
|
||||||
appointmentEndAt: {type: string, nullable: true, description: 明确结束时间时必须晚于开始时间;未知留空,不推算}
|
appointmentEndAt: {type: string, nullable: true, description: 明确结束时间时必须晚于开始时间;未知留空,不推算}
|
||||||
appointmentStatus: {type: string, enum: [PENDING, SUCCESS, FAILED]}
|
appointmentStatus: {type: string, enum: [PENDING, SUCCESS, FAILED]}
|
||||||
appointmentRemark: {type: string, maxLength: 1000, nullable: true}
|
appointmentRemark: {type: string, maxLength: 1000, nullable: true}
|
||||||
|
|||||||
@@ -7161,7 +7161,8 @@ components:
|
|||||||
confirmationNo: {type: string, nullable: true}
|
confirmationNo: {type: string, nullable: true}
|
||||||
appointmentAt:
|
appointmentAt:
|
||||||
type: string
|
type: string
|
||||||
description: 目的仓当地确认开始时间,须结合timezone解释,不按浏览器时区转换。
|
nullable: true
|
||||||
|
description: 目的仓当地确认开始时间;未提供时保持为空,已提供时结合timezone解释,不按浏览器时区转换。
|
||||||
appointmentEndAt: {type: string, nullable: true}
|
appointmentEndAt: {type: string, nullable: true}
|
||||||
timezone: {type: string}
|
timezone: {type: string}
|
||||||
DispatchAppointmentAllocationCommand:
|
DispatchAppointmentAllocationCommand:
|
||||||
|
|||||||
+3
-3
@@ -9,17 +9,17 @@ import static org.mockito.Mockito.*;
|
|||||||
class AppointmentBatchPreviewServiceTest {
|
class AppointmentBatchPreviewServiceTest {
|
||||||
private static AppointmentCargoVerificationService verification() {
|
private static AppointmentCargoVerificationService verification() {
|
||||||
var service=mock(AppointmentCargoVerificationService.class);
|
var service=mock(AppointmentCargoVerificationService.class);
|
||||||
when(service.inspect(any())).thenAnswer(call->{AppointmentSourceService.Source source=call.getArgument(0);return new AppointmentCargoVerificationService.Context(source,null,0,0,source.packages(),source.packages()==null?null:source.packages().longValue(),false,List.of("VERIFY_CARGO"),List.of());});
|
when(service.inspect(any())).thenAnswer(call->{AppointmentSourceService.Source source=call.getArgument(0);return new AppointmentCargoVerificationService.Context(source,null,0,0,source.packages(),List.of(),source.packages()==null?null:source.packages().longValue(),false,List.of("VERIFY_CARGO"),List.of());});
|
||||||
return service;
|
return service;
|
||||||
}
|
}
|
||||||
@Test void previewUsesVerifiedRemainingQuantityAndDoesNotRejectAnEntirePartiallyAllocatedSource() {
|
@Test void previewUsesVerifiedRemainingQuantityAndDoesNotRejectAnEntirePartiallyAllocatedSource() {
|
||||||
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentBatchPreviewMapper.class);var verification=verification();
|
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentBatchPreviewMapper.class);var verification=verification();
|
||||||
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"));var source=source("ORDER:1","A",1L,"100","FBA:ONT8");
|
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"));var source=source("ORDER:1","A",1L,"100","FBA:ONT8");
|
||||||
when(sources.resolveBatch(refs)).thenReturn(List.of(source));when(mapper.flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,true)));
|
when(sources.resolveBatch(refs)).thenReturn(List.of(source));when(mapper.flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,true)));
|
||||||
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,6,0,10,4L,false,List.of("VERIFY_CARGO"),List.of()));
|
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,6,0,10,List.of(),4L,false,List.of("VERIFY_CARGO"),List.of()));
|
||||||
var service=new AppointmentBatchPreviewService(sources,mapper,verification);
|
var service=new AppointmentBatchPreviewService(sources,mapper,verification);
|
||||||
assertEquals(4,service.preview(refs).group().packages());
|
assertEquals(4,service.preview(refs).group().packages());
|
||||||
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,10,0,10,0L,false,List.of("VERIFY_CARGO"),List.of()));
|
when(verification.inspect(source)).thenReturn(new AppointmentCargoVerificationService.Context(source,null,10,0,10,List.of(),0L,false,List.of("VERIFY_CARGO"),List.of()));
|
||||||
assertThrows(ResponseStatusException.class,()->service.preview(refs));
|
assertThrows(ResponseStatusException.class,()->service.preview(refs));
|
||||||
}
|
}
|
||||||
private static AppointmentSourceService.Source source(String key,String customer,Long orderId,String origin,String destination) {
|
private static AppointmentSourceService.Source source(String key,String customer,Long orderId,String origin,String destination) {
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ import static org.mockito.Mockito.*;
|
|||||||
class AppointmentCargoVerificationBatchTest {
|
class AppointmentCargoVerificationBatchTest {
|
||||||
@Test void inspectionUsesTwoBoundedBatchesAndRetainsPerSourcePrerequisitesAndAllocations() {
|
@Test void inspectionUsesTwoBoundedBatchesAndRetainsPerSourcePrerequisitesAndAllocations() {
|
||||||
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentCargoVerificationMapper.class);
|
var sources=mock(AppointmentSourceService.class);var mapper=mock(AppointmentCargoVerificationMapper.class);
|
||||||
var service=new AppointmentCargoVerificationService(sources,mapper,new ObjectMapper());
|
var service=new AppointmentCargoVerificationService(sources,mapper,new ObjectMapper(),new AppointmentCargoReferenceRepository(mapper));
|
||||||
var refs=IntStream.rangeClosed(1,200).mapToObj(i->new AppointmentCoverageService.SourceRef("C","ORDER:"+i,"FBA:ONT8")).toList();
|
var refs=IntStream.rangeClosed(1,200).mapToObj(i->new AppointmentCoverageService.SourceRef("C","ORDER:"+i,"FBA:ONT8")).toList();
|
||||||
when(sources.inspectBatch(anyList())).thenAnswer(call->call.<List<AppointmentCoverageService.SourceRef>>getArgument(0).stream().map(ref->
|
when(sources.inspectBatch(anyList())).thenAnswer(call->call.<List<AppointmentCoverageService.SourceRef>>getArgument(0).stream().map(ref->
|
||||||
ref.sourceKey().equals("ORDER:2")?new AppointmentSourceService.Resolution(ref,null,new ResponseStatusException(HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中")):
|
ref.sourceKey().equals("ORDER:2")?new AppointmentSourceService.Resolution(ref,null,new ResponseStatusException(HttpStatus.BAD_REQUEST,"接仓点不在启用的美国仓目录中")):
|
||||||
@@ -25,7 +25,7 @@ class AppointmentCargoVerificationBatchTest {
|
|||||||
assertEquals("接仓点不在启用的美国仓目录中",assertThrows(ResponseStatusException.class,()->result.get(1).requireContext()).getReason());
|
assertEquals("接仓点不在启用的美国仓目录中",assertThrows(ResponseStatusException.class,()->result.get(1).requireContext()).getReason());
|
||||||
assertEquals(10L,result.get(199).requireContext().availablePackages());
|
assertEquals(10L,result.get(199).requireContext().availablePackages());
|
||||||
verify(sources,times(2)).inspectBatch(argThat(batch->batch.size()==100));
|
verify(sources,times(2)).inspectBatch(argThat(batch->batch.size()==100));
|
||||||
verify(mapper,times(2)).latestBatch(anyList());verify(mapper,times(2)).allocatedBatch(anyList());verifyNoMoreInteractions(mapper,sources);
|
verify(mapper,times(2)).latestBatch(anyList());verify(mapper,times(2)).allocatedBatch(anyList());verify(mapper,times(2)).originalShipmentReferences(anyList());verifyNoMoreInteractions(mapper,sources);
|
||||||
when(sources.inspectBatch(anyList())).thenThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"目录不可用"));
|
when(sources.inspectBatch(anyList())).thenThrow(new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"目录不可用"));
|
||||||
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.inspectBatch(refs)).getStatusCode().value());
|
assertEquals(503,assertThrows(ResponseStatusException.class,()->service.inspectBatch(refs)).getStatusCode().value());
|
||||||
}
|
}
|
||||||
|
|||||||
+15
@@ -24,6 +24,21 @@ class AppointmentCoverageServiceTest {
|
|||||||
assertEquals("America/Los_Angeles",result.appointments().get(0).timezone()); assertEquals("EXT-1",result.appointments().get(0).confirmationNo());assertFalse(result.missingFields().contains("APPOINTMENT_TIMEZONE_UNVERIFIED"));
|
assertEquals("America/Los_Angeles",result.appointments().get(0).timezone()); assertEquals("EXT-1",result.appointments().get(0).confirmationNo());assertFalse(result.missingFields().contains("APPOINTMENT_TIMEZONE_UNVERIFIED"));
|
||||||
assertFalse(result.changeReviewRequired()); verifyNoInteractions(gateway);
|
assertFalse(result.changeReviewRequired()); verifyNoInteractions(gateway);
|
||||||
}
|
}
|
||||||
|
@Test void confirmedNumberWithoutATimeRemainsConfirmedAndQuantityOnlyVerificationKeepsOriginalReferences() {
|
||||||
|
var mapper=mock(AppointmentCoverageMapper.class);
|
||||||
|
when(mapper.sources(anyList())).thenReturn(List.of(order()));
|
||||||
|
when(mapper.appointments(anyList())).thenReturn(List.of(new AppointmentCoverageMapper.Fact(1L,"A","AP-1","SUCCESS",null,null,AFTER,10L,20,"ONT8","ORDER:1","ORDER:2","EXT-1","America/Los_Angeles",1)));
|
||||||
|
var evidence=new AppointmentCargoVerification();evidence.setCustomerId("A");evidence.setSourceKey("ORDER:1");evidence.setDestinationKey("FBA:ONT8");
|
||||||
|
evidence.setSourceVersion("ORDER:2");evidence.setSourcePackages(20);evidence.setPackageCount(20);evidence.setVersion(1);evidence.setShipmentReferencesJson("[]");
|
||||||
|
when(mapper.verifications(anyList())).thenReturn(List.of(evidence));
|
||||||
|
var service=new AppointmentCoverageService(mapper,mock(AppointmentExternalSourceGateway.class));
|
||||||
|
var refs=List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"));
|
||||||
|
var result=service.query(refs).get(0);
|
||||||
|
assertEquals("CONFIRMED",result.status());assertEquals("EXT-1",result.appointments().get(0).confirmationNo());
|
||||||
|
assertNull(result.appointments().get(0).confirmedAt());assertFalse(result.missingFields().contains("FBA_SHIPMENT_REFERENCES"));
|
||||||
|
when(mapper.sources(anyList())).thenReturn(List.of(new AppointmentCoverageMapper.Source(1L,"A","FBA","ONT8",20,2,BEFORE,true)));
|
||||||
|
assertTrue(service.query(refs).get(0).missingFields().contains("FBA_SHIPMENT_REFERENCES"));
|
||||||
|
}
|
||||||
@Test void partialPendingAndDuplicateClaimsNeverAppearAsCompleteCoverage() {
|
@Test void partialPendingAndDuplicateClaimsNeverAppearAsCompleteCoverage() {
|
||||||
var mapper=mock(AppointmentCoverageMapper.class); when(mapper.sources(anyList())).thenReturn(List.of(order()));
|
var mapper=mock(AppointmentCoverageMapper.class); when(mapper.sources(anyList())).thenReturn(List.of(order()));
|
||||||
var service=new AppointmentCoverageService(mapper,mock(AppointmentExternalSourceGateway.class));
|
var service=new AppointmentCoverageService(mapper,mock(AppointmentExternalSourceGateway.class));
|
||||||
|
|||||||
+48
@@ -26,6 +26,54 @@ class AppointmentCargoVerificationPersistenceTest {
|
|||||||
private static void flags(AnnotationConfigApplicationContext context) {
|
private static void flags(AnnotationConfigApplicationContext context) {
|
||||||
when(context.getBean(AppointmentBatchPreviewMapper.class).flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,false)));
|
when(context.getBean(AppointmentBatchPreviewMapper.class).flags(List.of(1L))).thenReturn(List.of(new AppointmentBatchPreviewMapper.Flags(1L,false,false,false)));
|
||||||
}
|
}
|
||||||
|
@Test void readsOriginalShipmentAndTrackingNumbersForTheAuthorizedCustomerAndKeepsExplicitVerificationFirst() {
|
||||||
|
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
|
||||||
|
var db=new JdbcTemplate(context.getBean(DataSource.class));
|
||||||
|
db.update("UPDATE ll_order SET customer_id='A' WHERE id=1");
|
||||||
|
db.update("INSERT INTO ll_order_detail(id,order_id,customer_id,shipment_no,fba_tracking_no) VALUES(1,1,'A',' FBA-ORIGINAL ','TRACK-ORIGINAL'),(2,1,'B','OTHER-CUSTOMER','OTHER-TRACK')");
|
||||||
|
var cargo=context.getBean(AppointmentCargoVerificationService.class);
|
||||||
|
var source=context.getBean(AppointmentSourceService.class).resolveBatch(List.of(new AppointmentCoverageService.SourceRef("A","ORDER:1","FBA:ONT8"))).get(0);
|
||||||
|
var first=cargo.inspect(source);
|
||||||
|
assertNull(first.verification());assertEquals(List.of("FBA-ORIGINAL","TRACK-ORIGINAL"),first.effectiveShipmentReferences());
|
||||||
|
cargo.save("ORDER:1","A",save(0,10,"quantity-only"),"operator","客服");
|
||||||
|
assertEquals(first.effectiveShipmentReferences(),cargo.inspect(source).effectiveShipmentReferences());
|
||||||
|
var details=new FbaAppointmentDetails(java.time.LocalDate.of(2026,10,28),null,null,"EXT-ORIGINAL",null,null,"SUCCESS",null,null,null);
|
||||||
|
var appointment=context.getBean(InboundFbaAppointmentService.class).createFromSources("operator",appointment("reuse-original",details));
|
||||||
|
assertEquals(first.effectiveShipmentReferences(),appointment.items().get(0).shipmentReferences());
|
||||||
|
assertNull(appointment.appointmentAt());assertEquals("SUCCESS",appointment.appointmentStatus());
|
||||||
|
cargo.save("ORDER:1","A",save(1,10,"explicit","FBA-VERIFIED"),"operator","客服");
|
||||||
|
assertEquals(List.of("FBA-VERIFIED"),cargo.inspect(source).effectiveShipmentReferences());
|
||||||
|
assertEquals(" FBA-ORIGINAL ",db.queryForObject("SELECT shipment_no FROM ll_order_detail WHERE id=1",String.class));
|
||||||
|
db.update("UPDATE ll_order SET order_type='WAREHOUSE_TRANSFER' WHERE id=1");
|
||||||
|
db.update("INSERT INTO ll_warehouse_transfer_order(id,lastleg_order_id,customer_id,fba_shipment_no,tracking_no) VALUES(1,1,'A','TRANSFER-FBA','TRANSFER-TRACK')");
|
||||||
|
assertEquals(List.of("TRANSFER-FBA","TRANSFER-TRACK"),context.getBean(AppointmentCargoReferenceRepository.class).originalReferences(List.of(source)).get(List.of("A","ORDER:1")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 采用最新资料与新建共用货件号优先级:未核实、仅核箱数和明确核实均写入真实快照。 */
|
||||||
|
@Test void adoptingLatestCargoKeepsOriginalReferencesUntilExplicitVerificationReplacesThem() {
|
||||||
|
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
|
||||||
|
var db=new JdbcTemplate(context.getBean(DataSource.class));
|
||||||
|
db.update("UPDATE ll_order SET customer_id='A' WHERE id=1");
|
||||||
|
db.update("INSERT INTO ll_order_detail(id,order_id,customer_id,shipment_no,fba_tracking_no) VALUES(1,1,'A','FBA-ORIGINAL','TRACK-ORIGINAL'),(2,1,'B','OTHER-CUSTOMER','OTHER-TRACK')");
|
||||||
|
var cargo=context.getBean(AppointmentCargoVerificationService.class);
|
||||||
|
var appointments=context.getBean(InboundFbaAppointmentService.class);
|
||||||
|
var created=appointments.createFromSources("operator",appointment("refresh-original",confirmed()));
|
||||||
|
var transaction=new org.springframework.transaction.support.TransactionTemplate(context.getBean(org.springframework.jdbc.datasource.DataSourceTransactionManager.class));
|
||||||
|
var original=List.of("FBA-ORIGINAL","TRACK-ORIGINAL");
|
||||||
|
assertEquals(original,created.items().get(0).shipmentReferences());
|
||||||
|
db.update("UPDATE ll_order_detail SET shipment_no='FBA-LATEST' WHERE id=1");
|
||||||
|
var latestOriginal=List.of("FBA-LATEST","TRACK-ORIGINAL");
|
||||||
|
for(int version=0;version<=2;version++) {
|
||||||
|
if(version==1)cargo.save("ORDER:1","A",save(0,10,"refresh-quantity-only"),"operator","客服");
|
||||||
|
if(version==2)cargo.save("ORDER:1","A",save(1,10,"refresh-explicit","FBA-VERIFIED"),"operator","客服");
|
||||||
|
Integer expectedVersion=version==0?null:version;
|
||||||
|
var latest=transaction.execute(status->appointments.refreshReceivingCargo(created.batchKey(),"A",List.of(new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.CargoVersion("ORDER:1","FBA:ONT8","ORDER:1",expectedVersion))));
|
||||||
|
assertNotNull(latest);assertEquals(version==2?List.of("FBA-VERIFIED"):latestOriginal,latest.items().get(0).shipmentReferences());
|
||||||
|
assertEquals(latest,appointments.detail(created.appointmentNo()),"刷新后的货件号必须实际持久化");
|
||||||
|
}
|
||||||
|
assertEquals("FBA-LATEST",db.queryForObject("SELECT shipment_no FROM ll_order_detail WHERE id=1",String.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
@Test void savesWithoutAppointmentOrConfirmationAndReplaysOriginalVersionAfterLaterEdits() {
|
@Test void savesWithoutAppointmentOrConfirmationAndReplaysOriginalVersionAfterLaterEdits() {
|
||||||
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
|
try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) {
|
||||||
var service=context.getBean(AppointmentCargoVerificationService.class);
|
var service=context.getBean(AppointmentCargoVerificationService.class);
|
||||||
|
|||||||
+5
-2
@@ -211,7 +211,9 @@ class FbaAppointmentPersistenceTest {
|
|||||||
int start=baseline.indexOf("CREATE TABLE IF NOT EXISTS "+table+" (");assertTrue(start>=0);
|
int start=baseline.indexOf("CREATE TABLE IF NOT EXISTS "+table+" (");assertTrue(start>=0);
|
||||||
String sql=baseline.substring(start,baseline.indexOf(';',start)+1).replace(" STORED","").replaceAll(" ENGINE=InnoDB[^;]*;",";");statement.execute(sql);
|
String sql=baseline.substring(start,baseline.indexOf(';',start)+1).replace(" STORED","").replaceAll(" ENGINE=InnoDB[^;]*;",";");statement.execute(sql);
|
||||||
}
|
}
|
||||||
statement.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,version INT DEFAULT 0,customer_id VARCHAR(128),customer_name VARCHAR(128))");statement.execute("INSERT INTO ll_order(id) VALUES(1),(2)");
|
statement.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,version INT DEFAULT 0,customer_id VARCHAR(128),customer_name VARCHAR(128),order_type VARCHAR(32) DEFAULT 'FBA')");statement.execute("INSERT INTO ll_order(id) VALUES(1),(2)");
|
||||||
|
statement.execute("CREATE TABLE ll_order_detail(id BIGINT PRIMARY KEY,order_id BIGINT,customer_id VARCHAR(128),shipment_no VARCHAR(128),fba_tracking_no VARCHAR(128))");
|
||||||
|
statement.execute("CREATE TABLE ll_warehouse_transfer_order(id BIGINT PRIMARY KEY,lastleg_order_id BIGINT,customer_id VARCHAR(128),fba_shipment_no VARCHAR(128),tracking_no VARCHAR(128))");
|
||||||
statement.execute("CREATE TABLE ll_dispatch_item(id BIGINT,order_id BIGINT,customer_id VARCHAR(128),dispatch_order_id BIGINT,plan_pallet_id BIGINT,source_stage VARCHAR(32),package_count INT)");
|
statement.execute("CREATE TABLE ll_dispatch_item(id BIGINT,order_id BIGINT,customer_id VARCHAR(128),dispatch_order_id BIGINT,plan_pallet_id BIGINT,source_stage VARCHAR(32),package_count INT)");
|
||||||
statement.execute("CREATE TABLE ll_dispatch_order(id BIGINT,dispatch_no VARCHAR(64),record_version BIGINT,status VARCHAR(32))");
|
statement.execute("CREATE TABLE ll_dispatch_order(id BIGINT,dispatch_no VARCHAR(64),record_version BIGINT,status VARCHAR(32))");
|
||||||
String work;try(var input=FbaAppointmentPersistenceTest.class.getResourceAsStream("/db/appointment-work.sql")){work=new String(Objects.requireNonNull(input).readAllBytes(),StandardCharsets.UTF_8);}
|
String work;try(var input=FbaAppointmentPersistenceTest.class.getResourceAsStream("/db/appointment-work.sql")){work=new String(Objects.requireNonNull(input).readAllBytes(),StandardCharsets.UTF_8);}
|
||||||
@@ -254,7 +256,8 @@ class FbaAppointmentPersistenceTest {
|
|||||||
}
|
}
|
||||||
@Bean FbaReceivingService receivingService(FbaReceivingStore store,InboundFbaAppointmentService batches,FbaAppointmentTransportPort transport,FbaAppointmentCommandMapper commands,FbaAppointmentWorkPort work,FbaAppointmentExecutionPort execution){return new FbaReceivingService(store,batches,transport,commands,work,execution);}
|
@Bean FbaReceivingService receivingService(FbaReceivingStore store,InboundFbaAppointmentService batches,FbaAppointmentTransportPort transport,FbaAppointmentCommandMapper commands,FbaAppointmentWorkPort work,FbaAppointmentExecutionPort execution){return new FbaReceivingService(store,batches,transport,commands,work,execution);}
|
||||||
@Bean AppointmentCargoVerificationMapper verifications(SqlSessionTemplate session){return session.getMapper(AppointmentCargoVerificationMapper.class);}
|
@Bean AppointmentCargoVerificationMapper verifications(SqlSessionTemplate session){return session.getMapper(AppointmentCargoVerificationMapper.class);}
|
||||||
@Bean AppointmentCargoVerificationService verificationService(AppointmentSourceService sources,AppointmentCargoVerificationMapper mapper,ObjectMapper json){return new AppointmentCargoVerificationService(sources,mapper,json);}
|
@Bean AppointmentCargoReferenceRepository cargoReferences(AppointmentCargoVerificationMapper mapper){return new AppointmentCargoReferenceRepository(mapper);}
|
||||||
|
@Bean AppointmentCargoVerificationService verificationService(AppointmentSourceService sources,AppointmentCargoVerificationMapper mapper,ObjectMapper json,AppointmentCargoReferenceRepository references){return new AppointmentCargoVerificationService(sources,mapper,json,references);}
|
||||||
@Bean AppointmentSourceService sources(){var mock=mock(AppointmentSourceService.class);when(mock.resolveBatch(anyList())).thenAnswer(call->{List<AppointmentCoverageService.SourceRef> refs=call.getArgument(0);return refs.stream().map(ref->cargo(20).stream().filter(s->s.sourceKey().equals(ref.sourceKey()) && s.customerId().equals(ref.customerId())).findFirst().orElseThrow()).toList();});when(mock.destinationTimezone("ONT8")).thenReturn("America/Los_Angeles");return mock;}
|
@Bean AppointmentSourceService sources(){var mock=mock(AppointmentSourceService.class);when(mock.resolveBatch(anyList())).thenAnswer(call->{List<AppointmentCoverageService.SourceRef> refs=call.getArgument(0);return refs.stream().map(ref->cargo(20).stream().filter(s->s.sourceKey().equals(ref.sourceKey()) && s.customerId().equals(ref.customerId())).findFirst().orElseThrow()).toList();});when(mock.destinationTimezone("ONT8")).thenReturn("America/Los_Angeles");return mock;}
|
||||||
@Bean AppointmentBatchPreviewMapper flags(){var mock=mock(AppointmentBatchPreviewMapper.class);when(mock.flags(anyList())).thenAnswer(call->{List<Long> ids=call.getArgument(0);return ids.stream().map(id->new AppointmentBatchPreviewMapper.Flags(id,false,false,false)).toList();});return mock;}
|
@Bean AppointmentBatchPreviewMapper flags(){var mock=mock(AppointmentBatchPreviewMapper.class);when(mock.flags(anyList())).thenAnswer(call->{List<Long> ids=call.getArgument(0);return ids.stream().map(id->new AppointmentBatchPreviewMapper.Flags(id,false,false,false)).toList();});return mock;}
|
||||||
@Bean ObjectMapper json(){return new ObjectMapper().findAndRegisterModules();}
|
@Bean ObjectMapper json(){return new ObjectMapper().findAndRegisterModules();}
|
||||||
|
|||||||
+10
@@ -138,6 +138,16 @@ class InboundFbaAppointmentServiceTest {
|
|||||||
assertThrows(ResponseStatusException.class,()->service.createFromSources("other",request("original",pending(null),"ORDER:1","ORDER:2")));
|
assertThrows(ResponseStatusException.class,()->service.createFromSources("other",request("original",pending(null),"ORDER:1","ORDER:2")));
|
||||||
verify(headers,times(1)).insert(any(InboundFbaAppointment.class));
|
verify(headers,times(1)).insert(any(InboundFbaAppointment.class));
|
||||||
}
|
}
|
||||||
|
@Test void savesExternalConfirmationWithoutInventingAConfirmedTimeFromThePlannedDate() {
|
||||||
|
var details=new FbaAppointmentDetails(LocalDate.of(2026,10,28),null,null,"EXT-NO-TIME",null,null,"SUCCESS",null,null,null);
|
||||||
|
var result=service.createFromSources("operator",request("confirmed-without-time",details,"ORDER:1"));
|
||||||
|
assertEquals("SUCCESS",result.appointmentStatus());assertEquals("EXT-NO-TIME",result.confirmationNo());
|
||||||
|
assertEquals(LocalDate.of(2026,10,28),result.plannedDate());assertNull(result.appointmentAt());
|
||||||
|
assertNull(result.appointmentEndAt());assertNull(result.appointmentStartUtc());assertNull(result.appointmentEndUtc());
|
||||||
|
assertEquals(service.detail(result.appointmentNo()),result);
|
||||||
|
var invalid=new FbaAppointmentDetails(null,null,null,"EXT-END-ONLY",null,at,"SUCCESS",null,null,null);
|
||||||
|
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("end-only",invalid,"ORDER:2")));
|
||||||
|
}
|
||||||
@Test void confirmationNeedsExternalNumberAndLocationTimezone() {
|
@Test void confirmationNeedsExternalNumberAndLocationTimezone() {
|
||||||
var missing=new FbaAppointmentDetails(null,null,null,null,at,null,"SUCCESS",null,null,null);
|
var missing=new FbaAppointmentDetails(null,null,null,null,at,null,"SUCCESS",null,null,null);
|
||||||
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("no-number",missing,"ORDER:1")));
|
assertThrows(ResponseStatusException.class,()->service.createFromSources("operator",request("no-number",missing,"ORDER:1")));
|
||||||
|
|||||||
+14
-2
@@ -12,17 +12,29 @@ import static org.mockito.Mockito.*;
|
|||||||
/** 选入排车前的确认资格按真实客户和逐票覆盖核对。 */
|
/** 选入排车前的确认资格按真实客户和逐票覆盖核对。 */
|
||||||
class AppointmentDispatchAvailabilityTest {
|
class AppointmentDispatchAvailabilityTest {
|
||||||
private final AppointmentDispatchAvailability policy=new AppointmentDispatchAvailability(mock(FbaReceivingStore.class));
|
private final AppointmentDispatchAvailability policy=new AppointmentDispatchAvailability(mock(FbaReceivingStore.class));
|
||||||
/** 预约头成功不能替代当前客户的有效确认,取消及无时间均不放行。 */
|
/** 正式确认可以尚无时段,仍须匹配当前客户、来源和目的仓;取消和未确认均不放行。 */
|
||||||
@Test void onlyCurrentConfirmedCustomerSourceAndDestinationCanEnterDispatch() {
|
@Test void onlyCurrentConfirmedCustomerSourceAndDestinationCanEnterDispatch() {
|
||||||
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isEqualTo(20);
|
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isEqualTo(20);
|
||||||
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"B"),"A")).isZero();
|
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"B"),"A")).isZero();
|
||||||
assertThat(count(snapshot("CANCELLED","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
assertThat(count(snapshot("CANCELLED","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
||||||
assertThat(count(snapshot("ACTIVE","PENDING","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
assertThat(count(snapshot("ACTIVE","PENDING","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
||||||
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",null,"A"),"A")).isZero();
|
assertThat(count(snapshot("ACTIVE","SUCCESS","America/New_York",null,"A"),"A")).isEqualTo(20);
|
||||||
var confirmed=snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A");
|
var confirmed=snapshot("ACTIVE","SUCCESS","America/New_York",LocalDateTime.of(2026,10,26,10,0),"A");
|
||||||
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:2","FBA:ACY2")).isZero();
|
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:2","FBA:ACY2")).isZero();
|
||||||
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:1","FBA:ONT8")).isZero();
|
assertThat(policy.confirmedPackages(confirmed,"A","ORDER:1","FBA:ONT8")).isZero();
|
||||||
}
|
}
|
||||||
|
/** 没有确认时间不代表可以省略外部约号,或单独填写结束时间及偏移。 */
|
||||||
|
@Test void untimedConfirmationStillRequiresExternalNumberAndConsistentOptionalWindow() {
|
||||||
|
var before=snapshot("ACTIVE","SUCCESS","America/New_York",null,"A");var old=before.agreements().get(0);
|
||||||
|
for(var details:List.of(
|
||||||
|
new FbaAppointmentDetails(null,null,null," ",null,null,"SUCCESS",null,null,null),
|
||||||
|
new FbaAppointmentDetails(null,null,null,"CONF-1",null,LocalDateTime.of(2026,10,26,10,0),"SUCCESS",null,null,null),
|
||||||
|
new FbaAppointmentDetails(null,null,null,"CONF-1",null,null,"SUCCESS",null,"-04:00",null),
|
||||||
|
new FbaAppointmentDetails(null,null,null,"CONF-1",null,null,"SUCCESS",null,null,"-04:00"))) {
|
||||||
|
var agreement=new Agreement(old.agreementKey(),old.version(),old.status(),old.destinationKey(),details,old.timezone(),old.members(),old.warehouseEvidence());
|
||||||
|
assertThat(count(new BatchReceiving(before.batchKey(),List.of(agreement),List.of(),List.of()),"A")).isZero();
|
||||||
|
}
|
||||||
|
}
|
||||||
/** 夏令时歧义及未维护时区不能猜成一个可排时间。 */
|
/** 夏令时歧义及未维护时区不能猜成一个可排时间。 */
|
||||||
@Test void invalidOrAmbiguousLocalTimeCannotAuthorizeDispatch() {
|
@Test void invalidOrAmbiguousLocalTimeCannotAuthorizeDispatch() {
|
||||||
assertThat(count(snapshot("ACTIVE","SUCCESS",null,LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
assertThat(count(snapshot("ACTIVE","SUCCESS",null,LocalDateTime.of(2026,10,26,10,0),"A"),"A")).isZero();
|
||||||
|
|||||||
+1
-1
@@ -192,7 +192,7 @@ class AppointmentWorkDeliveryProviderTest {
|
|||||||
@Test void handledPrerequisiteCommitsTheCallerTransactionThroughTheRealCargoProxy() {
|
@Test void handledPrerequisiteCommitsTheCallerTransactionThroughTheRealCargoProxy() {
|
||||||
var sourceService=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class);
|
var sourceService=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceService.class);
|
||||||
when(sourceService.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new Resolution(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"))));
|
when(sourceService.inspectBatch(List.of(new SourceRef("C","ORDER:11",null)))).thenReturn(List.of(new Resolution(new SourceRef("C","ORDER:11",null),null,new org.springframework.web.server.ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST,"当前货源类型不支持FBA预约"))));
|
||||||
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper());
|
var cargo=new AppointmentCargoVerificationService(sourceService,mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoVerificationMapper.class),new com.fasterxml.jackson.databind.ObjectMapper(),mock(com.baiyue.lastleg.appointmentplanning.AppointmentCargoReferenceRepository.class));
|
||||||
var ds=new org.h2.jdbcx.JdbcDataSource();ds.setURL("jdbc:h2:mem:work-read-tx-"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
|
var ds=new org.h2.jdbcx.JdbcDataSource();ds.setURL("jdbc:h2:mem:work-read-tx-"+java.util.UUID.randomUUID()+";DB_CLOSE_DELAY=-1");
|
||||||
var manager=new org.springframework.jdbc.datasource.DataSourceTransactionManager(ds);
|
var manager=new org.springframework.jdbc.datasource.DataSourceTransactionManager(ds);
|
||||||
var proxy=new org.springframework.aop.framework.ProxyFactory(cargo);proxy.setProxyTargetClass(true);
|
var proxy=new org.springframework.aop.framework.ProxyFactory(cargo);proxy.setProxyTargetClass(true);
|
||||||
|
|||||||
+20
@@ -210,6 +210,26 @@ class AppointmentWorkDispatchPersistenceTest {
|
|||||||
assertEquals(1,rows.size());assertEquals("DISPATCH:1",rows.get(0).get("BATCH_KEY"));
|
assertEquals(1,rows.size());assertEquals("DISPATCH:1",rows.get(0).get("BATCH_KEY"));
|
||||||
assertEquals("101",rows.get(0).get("CUSTOMER_ID"));assertEquals(physical.packages(),rows.get(0).get("PACKAGE_COUNT"));
|
assertEquals("101",rows.get(0).get("CUSTOMER_ID"));assertEquals(physical.packages(),rows.get(0).get("PACKAGE_COUNT"));
|
||||||
}
|
}
|
||||||
|
/** 没有确认时段的正式预约仍能带入本车,读写分配均保留真实数量且不虚构时间。 */
|
||||||
|
@Test void confirmedAppointmentWithoutTimeCanBeSelectedAndLinkedToDispatch() {
|
||||||
|
var original=confirmed("101");
|
||||||
|
var details=new FbaAppointmentDetails(null,null,null,original.details().confirmationNo(),null,null,"SUCCESS",null,null,null);
|
||||||
|
var untimed=new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.Agreement(original.agreementKey(),original.version(),original.status(),original.destinationKey(),details,original.timezone(),original.members(),original.warehouseEvidence());
|
||||||
|
when(receiving.currentAgreement(batch,"101")).thenReturn(untimed);
|
||||||
|
when(receiving.snapshots(anyList())).thenReturn(List.of(new com.baiyue.lastleg.inboundplan.receiving.FbaReceivingContracts.BatchReceiving(batch,List.of(untimed,confirmed("202")),List.of(),List.of())));
|
||||||
|
var identity=new CandidatePhysical("101",111L);
|
||||||
|
var reference=candidates.batchReferences(List.of(identity)).get(identity);
|
||||||
|
assertTrue(reference.blockReasons().isEmpty());assertEquals(80,reference.appointmentBatches().get(0).sources().get(0).packageCount());
|
||||||
|
var candidate=candidates.read(batch).candidates().stream().filter(c->"101".equals(c.customerId())).findFirst().orElseThrow();
|
||||||
|
assertTrue(candidate.blockReasons().isEmpty());assertEquals(80,candidate.packageCount());
|
||||||
|
var source=binding.allocationOptions(1,List.of(identity)).get(identity).get(0);
|
||||||
|
assertTrue(source.blockReasons().isEmpty());assertEquals(1,source.options().size());
|
||||||
|
var option=source.options().get(0);assertEquals("CONF-101",option.confirmationNo());assertEquals(80,option.availablePackages());
|
||||||
|
assertNull(option.appointmentAt());assertNull(option.appointmentEndAt());
|
||||||
|
tx.executeWithoutResult(s->{bind(11,21,22);binding.bindItems(1,List.of(item(31)),List.of());});
|
||||||
|
assertEquals(80,jdbc.queryForObject("SELECT package_count FROM ll_dispatch_appointment_allocation WHERE dispatch_item_id=11",Integer.class));
|
||||||
|
assertNull(tx.execute(status->binding.reviewBlockReason("D-1",null,null)));
|
||||||
|
}
|
||||||
/** 查询本车重分配量排除本车原占用,保留其他车占用及真实业务标识。 */
|
/** 查询本车重分配量排除本车原占用,保留其他车占用及真实业务标识。 */
|
||||||
@Test void ownedAllocationOptionsExcludeOnlyThisDispatchAndKeepOtherCarsQuota() {
|
@Test void ownedAllocationOptionsExcludeOnlyThisDispatchAndKeepOtherCarsQuota() {
|
||||||
tx.executeWithoutResult(s->bind(11));
|
tx.executeWithoutResult(s->bind(11));
|
||||||
|
|||||||
Reference in New Issue
Block a user