161 lines
14 KiB
Java
161 lines
14 KiB
Java
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<String> 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<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:")) {
|
|
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<AppointmentWarehouseClient.Warehouse> 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<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,"未找到当前客户的货物来源"); }
|
|
}
|