优化
This commit is contained in:
+98
@@ -0,0 +1,98 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.CargoLine;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Context;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Destination;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
/** 整车按真实客户与目的地拆分,物理托盘和来源行分别统计。 */
|
||||
@Component
|
||||
public class DeliveryCargoGroups {
|
||||
private final ObjectMapper json;
|
||||
|
||||
/** 使用服务统一序列化器计算稳定业务摘要。 */
|
||||
public DeliveryCargoGroups(ObjectMapper json) { this.json=json; }
|
||||
|
||||
/** 本域分组结果不作为另一套公网DTO或跨域Entity。 */
|
||||
public record Group(String key,String customerId,String customerName,Destination destination,
|
||||
String cargoHash,List<CargoLine> cargo,long packages,long palletCount) { }
|
||||
|
||||
/** 只接受完整真实拣货来源,不从展示入仓码或计划数补齐事实。 */
|
||||
public List<Group> groups(Context context) {
|
||||
if (context==null || context.counts()==null || context.cargo()==null || context.cargo().isEmpty())
|
||||
throw conflict("排车单没有真实待派送货物");
|
||||
var groups=new TreeMap<String,List<CargoLine>>();
|
||||
var attributions=new HashSet<Long>();
|
||||
var palletDestinations=new HashMap<Long,String>();
|
||||
for (var row:context.cargo()) {
|
||||
if (row==null || row.dispatchItemId()==null || row.dispatchItemId()<=0 || blank(row.customerId()) || "0".equals(row.customerId())
|
||||
|| row.orderId()==null || row.orderId()<=0 || row.packages()<=0 || row.pickId()==null || row.pickId()<=0 || row.attributionId()==null || row.attributionId()<=0
|
||||
|| blank(row.sourceKey()) || row.destination()==null || blank(row.destination().key()))
|
||||
throw conflict("货物缺少真实客户、拣货或来源归属");
|
||||
if ("RETURN_RELABEL".equals(row.sourceStage())
|
||||
? row.returnCartonId()==null || row.returnCartonId()<=0
|
||||
: row.inventoryPositionId()==null || row.inventoryPositionId()<=0)
|
||||
throw conflict("本次拣货缺少实际库存仓位或退货箱来源");
|
||||
if (!attributions.add(row.attributionId())) throw conflict("本次派送包含重复拣货来源");
|
||||
if (blank(row.destination().type()) || !Set.of("FBA","PRIVATE").contains(row.destination().type()))
|
||||
throw conflict("货物派送目的地类型不完整");
|
||||
if (row.warehousePalletId()!=null) {
|
||||
var previous=palletDestinations.putIfAbsent(row.warehousePalletId(),row.destination().key());
|
||||
if (previous!=null && !previous.equals(row.destination().key())) throw conflict("同一实物托盘不同目的地须先拆托");
|
||||
}
|
||||
if ("FBA".equals(row.destination().type()) && blank(row.destination().warehouseCode())
|
||||
|| "PRIVATE".equals(row.destination().type()) && blank(row.destination().address()))
|
||||
throw conflict("货物缺少真实目的仓或派送地址");
|
||||
groups.computeIfAbsent(groupKey(row.customerId(),row.destination()),ignored->new ArrayList<>()).add(row);
|
||||
}
|
||||
long total=context.cargo().stream().mapToLong(CargoLine::packages).reduce(0,Math::addExact);
|
||||
if (!context.pickingReconciled() || total!=context.counts().packageCount()
|
||||
|| total!=context.counts().pickedPackageCount() && total!=context.counts().shippedPackageCount())
|
||||
throw conflict("实际拣货来源与本车箱数尚未核平");
|
||||
var result=new ArrayList<Group>();
|
||||
for (var entry:groups.entrySet()) {
|
||||
var rows=entry.getValue().stream().sorted(Comparator.comparing(CargoLine::dispatchItemId)
|
||||
.thenComparing(CargoLine::attributionId)).toList();
|
||||
var first=rows.get(0);
|
||||
if (rows.stream().anyMatch(row->!Objects.equals(row.destination(),first.destination())))
|
||||
throw conflict("同组货物的目的地信息不一致,请核对后安排");
|
||||
long packages=rows.stream().mapToLong(CargoLine::packages).reduce(0,Math::addExact);
|
||||
long pallets=rows.stream().map(CargoLine::warehousePalletId).filter(Objects::nonNull).distinct().count();
|
||||
result.add(new Group(entry.getKey(),first.customerId(),first.customerName(),first.destination(),
|
||||
hash(rows),rows,packages,pallets));
|
||||
}
|
||||
return List.copyOf(result);
|
||||
}
|
||||
|
||||
/** 客户是组身份的一部分,同目的仓的其他客户永不合单。 */
|
||||
public String groupKey(String customerId,Destination destination) {
|
||||
return hash(List.of(customerId,destination.key()));
|
||||
}
|
||||
|
||||
/** 本地派送来源证明沿用单据分组键,明确映射既有FBA和私人地址语义。 */
|
||||
public String documentGroupKey(Destination destination) {
|
||||
if ("PRIVATE".equals(destination.type()) && !blank(destination.address()))
|
||||
return "PRIVATE:"+destination.address().trim()+":"+Objects.toString(destination.postalCode(),"").trim();
|
||||
if ("FBA".equals(destination.type()) && !blank(destination.warehouseCode()))
|
||||
return "CODE:"+destination.warehouseCode().trim().toUpperCase(Locale.ROOT);
|
||||
throw conflict("派送目的地缺少真实业务标识");
|
||||
}
|
||||
|
||||
/** 摘要仅使用规范化领域值,不包含操作者或偶然集合遍历顺序。 */
|
||||
public String hash(Object value) {
|
||||
try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json.writeValueAsBytes(value))); }
|
||||
catch (java.security.NoSuchAlgorithmException | com.fasterxml.jackson.core.JsonProcessingException error) {
|
||||
throw new IllegalStateException("派送业务摘要生成失败",error);
|
||||
}
|
||||
}
|
||||
|
||||
/** 缺失业务事实直接阻断,不用内部编号回退展示。 */
|
||||
private static boolean blank(String value) { return value==null || value.isBlank(); }
|
||||
/** 业务冲突不产生部分单据。 */
|
||||
private static ResponseStatusException conflict(String reason) { return new ResponseStatusException(HttpStatus.CONFLICT,reason); }
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package com.baiyue.lastleg.delivery.model;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Context;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
|
||||
/** 五阶段只接受原实发全部正常签收,不将真实退回当作签收数量。 */
|
||||
@Component
|
||||
public class NormalDeliveryCompletionPolicy {
|
||||
private final DeliveryCargoGroups hashes;
|
||||
|
||||
/** 复用本域确定性摘要器,不申请历史来源订单锁。 */
|
||||
public NormalDeliveryCompletionPolicy(DeliveryCargoGroups hashes) { this.hashes=hashes; }
|
||||
|
||||
/** 按已安排单据精确集合和原实发数量核对,空集合不算完成。 */
|
||||
public NormalDeliveryFacts evaluate(Context context,ArrangementResult arrangement,List<NormalDeliveryFact> actual,Instant now) {
|
||||
var reasons=new ArrayList<String>();
|
||||
var expected=arrangement==null?List.<DeliveryReference>of():arrangement.deliveries();
|
||||
var seen=new HashMap<String,NormalDeliveryFact>();
|
||||
if (context.departedAt()==null || now==null || context.departedAt().isAfter(now)) reasons.add("尚无有效实际发车事实");
|
||||
if (expected==null || expected.isEmpty()) reasons.add("缺少本车已安排派送单");
|
||||
for (var fact:actual) if (fact==null || fact.deliveryNo()==null || seen.putIfAbsent(fact.deliveryNo(),fact)!=null)
|
||||
reasons.add("派送事实缺失或重复");
|
||||
var expectedNos=expected==null?List.<String>of():expected.stream().map(DeliveryReference::deliveryNo).sorted().toList();
|
||||
if (new HashSet<>(expectedNos).size()!=expectedNos.size() || !seen.keySet().equals(new HashSet<>(expectedNos)))
|
||||
reasons.add("派送单未精确覆盖本车安排");
|
||||
long total=0; Instant completedAt=null;
|
||||
if (expected!=null) for (var reference:expected) {
|
||||
var fact=seen.get(reference.deliveryNo());
|
||||
if (fact==null) continue;
|
||||
if (!Objects.equals(fact.customerId(),reference.customerId()) || !Objects.equals(fact.groupKey(),reference.groupKey())
|
||||
|| !Objects.equals(fact.outboundNo(),reference.outboundNo()) || fact.originalPackages()!=reference.packageCount())
|
||||
reasons.add("派送客户、目的地或原实发来源不一致");
|
||||
total=Math.addExact(total,fact.originalPackages());
|
||||
// full签收字段是原运输事实;签后另退不重开,但签剩余/拒收/无签收永不冒充原全签。
|
||||
if (!fact.outboundCompleted() || fact.status()==null || !Set.of("DELIVERED","RETURN_PENDING","RETURNED").contains(fact.status()) || fact.originalPackages()<=0
|
||||
|| fact.signedPackages()!=fact.originalPackages() || fact.signedAt()==null)
|
||||
reasons.add("仍有货物未正常全部签收,异常或退回不能代替签收");
|
||||
else if (context.departedAt()==null || fact.signedAt().isBefore(context.departedAt())
|
||||
|| now==null || fact.signedAt().isAfter(now)) reasons.add("签收时间与真实发车事实不一致");
|
||||
else if (completedAt==null || fact.signedAt().isAfter(completedAt)) completedAt=fact.signedAt();
|
||||
}
|
||||
if (total<=0 || context.counts()==null || total!=context.counts().shippedPackageCount())
|
||||
reasons.add("签收单据未完整覆盖本趟真实实发箱数");
|
||||
var sorted=actual.stream().filter(Objects::nonNull).sorted(Comparator.comparing(NormalDeliveryFact::customerId,Comparator.nullsFirst(String::compareTo))
|
||||
.thenComparing(NormalDeliveryFact::deliveryNo,Comparator.nullsFirst(String::compareTo))).toList();
|
||||
boolean normal=reasons.isEmpty();
|
||||
return new NormalDeliveryFacts(context.dispatchNo(),hashes.hash(Arrays.asList(context.dispatchNo(),expectedNos,sorted)),
|
||||
normal,normal?completedAt:null,expectedNos,sorted,reasons.stream().distinct().toList());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user