Files
baiyue-firstleg/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceService.java
T

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,"未找到当前客户的货物来源"); }
}