feat(appointments): unify pre-inbound planning and confirmed FBA bookings
This commit is contained in:
+90
-16
@@ -9,6 +9,10 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AppointmentSourceService {
|
||||
@@ -21,47 +25,105 @@ public class AppointmentSourceService {
|
||||
private final AppointmentExternalSourceGateway external;
|
||||
private final AppointmentWarehouseClient warehouses;
|
||||
private final String token;
|
||||
@jakarta.annotation.Resource private AppointmentCoverageMapper destinationFacts;
|
||||
public AppointmentSourceService(LastlegOrderMapper orders, ContainerShippingGateMapper shipping,
|
||||
AppointmentExternalSourceGateway external, AppointmentWarehouseClient warehouses,
|
||||
@Value("${baiyue.security.internal-service-token:}") String token) {
|
||||
this.orders=orders; this.shipping=shipping; this.external=external; this.warehouses=warehouses; this.token=token;
|
||||
}
|
||||
public Source resolve(String sourceKey,String customerId,String destinationKey) {
|
||||
if (blank(sourceKey) || sourceKey.length()>160 || blank(customerId) || customerId.length()>128) throw bad("请选择完整的真实货源");
|
||||
return resolveBatch(List.of(new AppointmentCoverageService.SourceRef(customerId,sourceKey,destinationKey))).get(0);
|
||||
}
|
||||
public void lockForWrite(List<AppointmentCoverageService.SourceRef> refs) {
|
||||
if(refs==null || refs.isEmpty() || refs.size()>100 || refs.stream().anyMatch(Objects::isNull)) throw bad("每批请选择1至100票真实货源");
|
||||
var bookings=refs.stream().filter(ref->ref.sourceKey()!=null && ref.sourceKey().startsWith("BOOKING:"))
|
||||
.map(ref->new AppointmentExternalSourceGateway.SourceRef(ref.customerId(),ref.sourceKey().substring(8))).toList();
|
||||
if(!bookings.isEmpty()) external.lockForAppointment(bookings);
|
||||
}
|
||||
public List<AppointmentCoverageService.SourceRef> canonicalOrders(List<AppointmentExternalSourceGateway.OrderRef> refs) {
|
||||
if(refs==null || refs.isEmpty() || refs.size()>100) throw bad("每批请选择1至100票真实货源");
|
||||
var bookings=external.resolveOrders(refs);
|
||||
var byOrder=bookings.stream().collect(Collectors.toMap(s->new AppointmentExternalSourceGateway.OrderRef(s.customerId(),s.orderId()),Function.identity()));
|
||||
return refs.stream().distinct().map(ref->{
|
||||
var booking=byOrder.get(ref);
|
||||
return new AppointmentCoverageService.SourceRef(ref.customerId(),booking==null?"ORDER:"+ref.orderId():"BOOKING:"+booking.bookingNo(),null);
|
||||
}).toList();
|
||||
}
|
||||
public String destinationTimezone(String code) {
|
||||
var destination=catalog().stream().filter(w->"US".equals(w.countryCode()) && "FBA".equals(w.warehouseType()) && code.equals(w.warehouseCode())).findFirst().orElseThrow(()->bad("目的仓不在美国FBA目录中"));
|
||||
try {
|
||||
var response=warehouses.activeDetail(token,destination.id());
|
||||
if(response==null || !response.successful() || response.data()==null || response.data().warehouse()==null
|
||||
|| !Objects.equals(response.data().warehouse().id(),destination.id()) || !Long.valueOf(0).equals(response.data().warehouse().tenantId())) throw new IllegalStateException();
|
||||
return response.data().warehouse().timezone();
|
||||
} catch(RuntimeException error) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"目的仓时区读取失败,请稍后核对",error); }
|
||||
}
|
||||
/** One catalog request and batched source reads, including mixed first-leg and external bookings. */
|
||||
public List<Source> resolveBatch(List<AppointmentCoverageService.SourceRef> requested) {
|
||||
if(requested==null || requested.isEmpty() || requested.size()>100) throw bad("每批请选择1至100票真实货源");
|
||||
var keys=new LinkedHashSet<String>(); var orderIds=new LinkedHashSet<Long>();
|
||||
var bookingRefs=new java.util.ArrayList<AppointmentExternalSourceGateway.SourceRef>();
|
||||
for(var ref:requested) {
|
||||
if(ref==null || blank(ref.sourceKey()) || ref.sourceKey().length()>160 || blank(ref.customerId()) || ref.customerId().length()>128) throw bad("请选择完整的真实货源");
|
||||
if(!keys.add(ref.sourceKey())) throw bad("同一来源货物不能重复选择");
|
||||
if(ref.sourceKey().startsWith("ORDER:")) orderIds.add(orderId(ref.sourceKey()));
|
||||
else if(ref.sourceKey().startsWith("BOOKING:") && !blank(ref.sourceKey().substring(8))) bookingRefs.add(new AppointmentExternalSourceGateway.SourceRef(ref.customerId(),ref.sourceKey().substring(8)));
|
||||
else throw bad("不支持的货源类型");
|
||||
}
|
||||
var bookings=bookingRefs.isEmpty()?List.<AppointmentExternalSourceGateway.Source>of():external.resolve(bookingRefs);
|
||||
var bookingMap=new java.util.HashMap<String,AppointmentExternalSourceGateway.Source>();
|
||||
for(var booking:bookings) {
|
||||
if(bookingMap.put(booking.customerId()+"\n"+booking.bookingNo(),booking)!=null) throw bad("外接来源重复,请先核对");
|
||||
if(booking.orderId()!=null) orderIds.add(booking.orderId());
|
||||
}
|
||||
var orderRows=orderIds.isEmpty()?List.<com.baiyue.lastleg.entity.LastlegOrder>of():orders.selectBatchIds(orderIds);
|
||||
var directOrders=requested.stream().filter(ref->ref.sourceKey().startsWith("ORDER:")).map(ref->new AppointmentExternalSourceGateway.OrderRef(ref.customerId(),orderId(ref.sourceKey()))).toList();
|
||||
if(!directOrders.isEmpty() && !external.resolveOrders(directOrders).isEmpty()) throw bad("该货物来自外接登记,请使用原登记来源继续预约");
|
||||
var orderMap=orderRows.stream().collect(Collectors.toMap(com.baiyue.lastleg.entity.LastlegOrder::getId,Function.identity()));
|
||||
var transferRefs=orderRows.stream().filter(o->"WAREHOUSE_TRANSFER".equals(o.getOrderType())).map(o->new AppointmentCoverageMapper.OrderRef(o.getCustomerId(),o.getId())).toList();
|
||||
var destinations=transferRefs.isEmpty()?Map.<Long,AppointmentCoverageMapper.Source>of():destinationFacts.sources(transferRefs).stream().collect(Collectors.toMap(AppointmentCoverageMapper.Source::orderId,Function.identity()));
|
||||
var containerIds=orderRows.stream().filter(o->"FIRST_LEG".equals(o.getOrderType())).map(com.baiyue.lastleg.entity.LastlegOrder::getSourceContainerBookingId).filter(Objects::nonNull).distinct().toList();
|
||||
var gates=containerIds.isEmpty()?Map.<String,Boolean>of():shipping.selectBatchIds(containerIds).stream().collect(Collectors.toMap(g->g.getContainerBookingId(),g->g.getNodeSequence()!=null && g.getNodeSequence()>=40));
|
||||
var catalog=catalog();
|
||||
var result=requested.stream().map(ref->resolve(ref.sourceKey(),ref.customerId(),ref.destinationKey(),orderMap,bookingMap,gates,catalog,destinations)).toList();
|
||||
var materialized=new java.util.HashSet<Long>();
|
||||
for(var source:result) if(source.orderId()!=null && !materialized.add(source.orderId())) throw bad("同一货物存在重复来源,请保留原登记来源");
|
||||
return result;
|
||||
}
|
||||
private Source resolve(String sourceKey,String customerId,String destinationKey,
|
||||
Map<Long,com.baiyue.lastleg.entity.LastlegOrder> orderMap,
|
||||
Map<String,AppointmentExternalSourceGateway.Source> bookingMap,Map<String,Boolean> gates,
|
||||
List<AppointmentWarehouseClient.Warehouse> catalog,Map<Long,AppointmentCoverageMapper.Source> destinations) {
|
||||
com.baiyue.lastleg.entity.LastlegOrder order=null;
|
||||
String bookingNo=null,origin,destination,version;
|
||||
Integer packages;
|
||||
boolean followup=true;
|
||||
List<String> reasons=List.of();
|
||||
if (sourceKey.startsWith("ORDER:")) {
|
||||
long id;
|
||||
try { id=Long.parseLong(sourceKey.substring(6)); if (id<=0 || !sourceKey.equals("ORDER:"+id)) throw new NumberFormatException(); }
|
||||
catch (NumberFormatException error) { throw bad("货源标识无效"); }
|
||||
order=orders.selectById(id);
|
||||
order=orderMap.get(orderId(sourceKey));
|
||||
if (order==null || !customerId.equals(order.getCustomerId())) throw missing();
|
||||
if (!"FIRST_LEG".equals(order.getOrderType())) throw bad("请从原外接登记继续跟进,避免重复建立货源");
|
||||
var gate=order.getSourceContainerBookingId()==null ? null : shipping.selectById(order.getSourceContainerBookingId());
|
||||
if (gate==null || gate.getNodeSequence()==null || gate.getNodeSequence()<40) throw bad("货物尚无已开船或后续实际船节点");
|
||||
origin=order.getOverseasWarehouseId(); destination="FBA:"+order.getFbaWarehouseCode();
|
||||
if (!"FBA".equals(order.getDestinationType())) throw bad("当前货源不是FBA送仓货物");
|
||||
if (!Set.of("FIRST_LEG","WAREHOUSE_TRANSFER").contains(Objects.toString(order.getOrderType(),""))) throw bad("当前货源类型不支持FBA预约");
|
||||
if ("FIRST_LEG".equals(order.getOrderType()) && (order.getSourceContainerBookingId()==null || !Boolean.TRUE.equals(gates.get(order.getSourceContainerBookingId())))) throw bad("货物尚无已开船或后续实际船节点");
|
||||
origin=order.getOverseasWarehouseId(); destination="FBA:"+destinationCode(order,destinations);
|
||||
if (!"FBA".equals(destinationType(order,destinations))) throw bad("当前货源不是FBA送仓货物");
|
||||
packages=order.getPackageCount(); version=order.getVersion()==null ? null : "ORDER:"+order.getVersion();
|
||||
} else if (sourceKey.startsWith("BOOKING:")) {
|
||||
bookingNo=sourceKey.substring(8);
|
||||
if (blank(bookingNo)) throw bad("外接登记号不能为空");
|
||||
var matches=external.resolve(List.of(new AppointmentExternalSourceGateway.SourceRef(customerId,bookingNo)));
|
||||
if (matches.size()!=1) throw missing();
|
||||
var source=matches.get(0);
|
||||
var source=bookingMap.get(customerId+"\n"+bookingNo);
|
||||
if (source==null) throw missing();
|
||||
if (!Objects.equals(source.customerId(),customerId) || !Objects.equals(source.bookingNo(),bookingNo)) throw missing();
|
||||
origin=source.warehouseId(); destination=source.destinationKey(); packages=source.packageCount();
|
||||
version=source.recordVersion()==null ? null : "BOOKING:"+source.recordVersion();
|
||||
followup=Set.of("CONFIRMED","RECEIVED","BOUND").contains(source.bookingStatus()==null ? "" : source.bookingStatus());
|
||||
if (!followup) reasons=List.of("当前登记尚未确认承运或已取消,仅可查看跟进记录");
|
||||
if (source.orderId()!=null) {
|
||||
order=orders.selectById(source.orderId());
|
||||
order=orderMap.get(source.orderId());
|
||||
if (order==null || !customerId.equals(order.getCustomerId())) throw missing();
|
||||
if (!Objects.equals(order.getOverseasWarehouseId(),origin)) throw bad("来货的接仓点与原登记不一致,请先核对");
|
||||
if (!"FBA".equals(order.getDestinationType()) || !Objects.equals(destination,"FBA:"+order.getFbaWarehouseCode())) throw bad("来货目的仓与原登记不一致,请先核对");
|
||||
if (!"FBA".equals(destinationType(order,destinations)) || !Objects.equals(destination,"FBA:"+destinationCode(order,destinations))) throw bad("来货目的仓与原登记不一致,请先核对");
|
||||
packages=order.getPackageCount();
|
||||
version=version+"/ORDER:"+order.getVersion();
|
||||
}
|
||||
} else throw bad("不支持的货源类型");
|
||||
if (order!=null && Set.of("CANCELLED","COMPLETED").contains(order.getCurrentStatus()==null ? "" : order.getCurrentStatus())) {
|
||||
@@ -69,7 +131,7 @@ public class AppointmentSourceService {
|
||||
}
|
||||
if (!blank(destinationKey) && !Objects.equals(destination,destinationKey)) throw bad("目的地已变化,请返回原业务重新核对");
|
||||
if (destination==null || !destination.startsWith("FBA:")) throw bad("当前仅支持FBA收货跟进来源");
|
||||
var catalog=catalog(); final String originId=origin; final String code=destination.substring(4);
|
||||
final String originId=origin; final String code=destination.substring(4);
|
||||
var warehouse=catalog.stream().filter(w -> w.id()!=null && w.id().toString().equals(originId) && "US".equals(w.countryCode())
|
||||
&& Set.of("OVERSEAS","OVERSEAS_PARTNER").contains(w.warehouseType()==null ? "" : w.warehouseType())).findFirst().orElseThrow(() -> bad("接仓点不在启用的美国仓目录中"));
|
||||
if (catalog.stream().noneMatch(w -> "FBA".equals(w.warehouseType()) && "US".equals(w.countryCode()) && code.equals(w.warehouseCode()))) throw bad("目的仓不在启用的美国FBA目录中");
|
||||
@@ -81,6 +143,18 @@ public class AppointmentSourceService {
|
||||
catch (RuntimeException error) { throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,"仓库目录暂不可用,请稍后重新核对",error); }
|
||||
}
|
||||
private static boolean blank(String value) { return value==null || value.isBlank(); }
|
||||
private static String destinationType(com.baiyue.lastleg.entity.LastlegOrder order,Map<Long,AppointmentCoverageMapper.Source> facts) {
|
||||
if(!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) return order.getDestinationType();
|
||||
var fact=facts.get(order.getId());return fact==null?null:fact.destinationType();
|
||||
}
|
||||
private static String destinationCode(com.baiyue.lastleg.entity.LastlegOrder order,Map<Long,AppointmentCoverageMapper.Source> facts) {
|
||||
if(!"WAREHOUSE_TRANSFER".equals(order.getOrderType())) return order.getFbaWarehouseCode();
|
||||
var fact=facts.get(order.getId());return fact==null?null:fact.fbaWarehouseCode();
|
||||
}
|
||||
private static long orderId(String key) {
|
||||
try { long id=Long.parseLong(key.substring(6)); if(id<=0 || !key.equals("ORDER:"+id)) throw new NumberFormatException(); return id; }
|
||||
catch(NumberFormatException error) { throw bad("货源标识无效"); }
|
||||
}
|
||||
private static ResponseStatusException bad(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST,message); }
|
||||
private static ResponseStatusException missing() { return new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到当前客户的货物来源"); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user