feat(lastleg): connect appointment followups and fleet intake

This commit is contained in:
2026-10-09 22:04:49 +08:00
parent 1202cafadf
commit 0d75e311d0
54 changed files with 2315 additions and 25 deletions
@@ -0,0 +1,86 @@
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;
@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;
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("请选择完整的真实货源");
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);
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送仓货物");
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);
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());
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("来货目的仓与原登记不一致,请先核对");
packages=order.getPackageCount();
}
} 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收货跟进来源");
var catalog=catalog(); 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 ResponseStatusException bad(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST,message); }
private static ResponseStatusException missing() { return new ResponseStatusException(HttpStatus.NOT_FOUND,"未找到当前客户的货物来源"); }
}