package com.baiyue.lastleg.appointmentplanning; import com.baiyue.lastleg.mapper.LastlegOrderMapper; import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; 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 { public record Source(String sourceKey, String customerId, Long orderId, String inboundCode, String bookingNo, String containerNo, String originWarehouseId, String originWarehouseName, String destinationKey, String destinationWarehouseCode, Integer packages, String sourceVersion, boolean canFollowup, List blockReasons) { } private final LastlegOrderMapper orders; private final ContainerShippingGateMapper shipping; 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) { return resolveBatch(List.of(new AppointmentCoverageService.SourceRef(customerId,sourceKey,destinationKey))).get(0); } public void lockForWrite(List 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 canonicalOrders(List 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 resolveBatch(List requested) { if(requested==null || requested.isEmpty() || requested.size()>100) throw bad("每批请选择1至100票真实货源"); var keys=new LinkedHashSet(); var orderIds=new LinkedHashSet(); var bookingRefs=new java.util.ArrayList(); 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.of():external.resolve(bookingRefs); var bookingMap=new java.util.HashMap(); 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.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.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.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(); 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 orderMap, Map bookingMap,Map gates, List catalog,Map destinations) { com.baiyue.lastleg.entity.LastlegOrder order=null; String bookingNo=null,origin,destination,version; Integer packages; boolean followup=true; List reasons=List.of(); if (sourceKey.startsWith("ORDER:")) { order=orderMap.get(orderId(sourceKey)); if (order==null || !customerId.equals(order.getCustomerId())) throw missing(); 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 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=orderMap.get(source.orderId()); if (order==null || !customerId.equals(order.getCustomerId())) throw missing(); if (!Objects.equals(order.getOverseasWarehouseId(),origin)) 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())) { followup=false; reasons=List.of("该货物已取消或完成,仅可查看跟进记录"); } if (!blank(destinationKey) && !Objects.equals(destination,destinationKey)) throw bad("目的地已变化,请返回原业务重新核对"); if (destination==null || !destination.startsWith("FBA:")) throw bad("当前仅支持FBA收货跟进来源"); 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目录中"); return new Source(sourceKey,customerId,order==null ? null : order.getId(),order==null ? null : order.getInboundCode(),bookingNo, order==null ? null : order.getContainerNo(),origin,warehouse.warehouseName(),destination,code,packages,version,followup,reasons); } private List catalog() { try { var result=warehouses.catalog(token); if (result==null || !result.successful() || result.data()==null) throw new IllegalStateException(); return result.data().stream().filter(Objects::nonNull).toList(); } 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 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 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,"未找到当前客户的货物来源"); } }