This commit is contained in:
2026-08-28 13:54:32 +08:00
parent fe68551124
commit 6b5f2f0565
29 changed files with 959 additions and 178 deletions
@@ -0,0 +1,93 @@
package com.baiyue.lastleg.service;
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
import feign.FeignException;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Objects;
/** 通过国内 Warehouse Service 权威主档校验尾程转仓 FBA 目的仓。 */
@Component
public class FbaWarehouseValidator {
private static final String INVALID_MESSAGE = "所选FBA仓不存在或已停用";
@Resource private DomesticWarehouseDirectoryClient client;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
public VerifiedFbaWarehouse required(String requestedId, String requestedCode, String requestedName) {
long warehouseId = parseId(requestedId);
var response = lookup(warehouseId);
if (response == null || !response.successful() || response.data() == null
|| response.data().warehouse() == null) {
throw invalid();
}
DomesticWarehouseDirectoryClient.Warehouse warehouse = response.data().warehouse();
if (!Objects.equals(warehouse.id(), warehouseId)
|| !Objects.equals(warehouse.tenantId(), 0L)
|| !"FBA".equals(warehouse.warehouseType())
|| !"ACTIVE".equals(warehouse.status())
|| !same(requestedCode, warehouse.warehouseCode())
|| !same(requestedName, warehouse.warehouseName())) {
throw invalid();
}
String region = firstRegion(response.data().addresses());
return new VerifiedFbaWarehouse(warehouse.id(), warehouse.warehouseCode().trim(),
warehouse.warehouseName().trim(), trimToNull(warehouse.countryCode()), region);
}
private com.baiyue.common.core.api.ApiResponse<DomesticWarehouseDirectoryClient.Detail> lookup(long warehouseId) {
try {
return client.activeWarehouse(warehouseId, internalToken);
} catch (FeignException exception) {
if (exception.status() >= 400 && exception.status() < 500) throw invalid();
throw unavailable(exception);
} catch (ResponseStatusException exception) {
throw exception;
} catch (RuntimeException exception) {
throw unavailable(exception);
}
}
private long parseId(String value) {
try {
long id = Long.parseLong(value == null ? "" : value.trim());
if (id <= 0) throw invalid();
return id;
} catch (NumberFormatException exception) {
throw invalid();
}
}
private boolean same(String requested, String actual) {
return StringUtils.hasText(requested) && StringUtils.hasText(actual)
&& requested.trim().equals(actual.trim());
}
private String firstRegion(List<DomesticWarehouseDirectoryClient.Address> addresses) {
if (addresses == null) return null;
return addresses.stream().filter(Objects::nonNull)
.map(DomesticWarehouseDirectoryClient.Address::stateProvince)
.map(FbaWarehouseValidator::trimToNull).filter(Objects::nonNull).findFirst().orElse(null);
}
private ResponseStatusException invalid() {
return new ResponseStatusException(HttpStatus.BAD_REQUEST, INVALID_MESSAGE);
}
private ResponseStatusException unavailable(RuntimeException cause) {
return new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
"国内FBA仓主数据暂不可用", cause);
}
private static String trimToNull(String value) {
return value == null || value.isBlank() ? null : value.trim();
}
public record VerifiedFbaWarehouse(long id, String code, String name, String countryCode, String region) { }
}
@@ -6,7 +6,8 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse;
import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.LastlegOrderSummary;
import com.baiyue.lastleg.dto.PendingContainerSummary;
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
@@ -39,6 +40,7 @@ public class LastlegOrderService {
@Resource private LastlegOrderDetailMapper detailMapper;
@Resource private LastlegOrderStatusHistoryMapper historyMapper;
@Resource private InboundPlanCartonMapper cartonMapper;
@Resource private FbaWarehouseValidator fbaWarehouseValidator;
public PageResult<LastlegOrderSummary> page(String customerId, LastlegOrderRequests.Query query) {
Page<LastlegOrder> page = orderMapper.selectPage(
@@ -48,20 +50,33 @@ public class LastlegOrderService {
}
@Transactional(readOnly = true)
public PageResult<PendingContainerSummary> pendingContainerPage(
LastlegOrderRequests.PendingContainerQuery query) {
public PageResult<ContainerPlanProgressSummary> containerProgressPage(
LastlegOrderRequests.ContainerProgressQuery query) {
if (query.createdFrom() != null && query.createdTo() != null
&& query.createdFrom().isAfter(query.createdTo())) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
"createdFrom must not be after createdTo");
}
long total = orderMapper.countPendingContainers(query);
List<PendingContainerSummary> records = total == 0 ? List.of()
: orderMapper.pagePendingContainers(query, (query.page() - 1) * query.size(), query.size());
long total = orderMapper.countContainerProgress(query);
List<ContainerPlanProgressSummary> records = total == 0 ? List.of()
: orderMapper.pageContainerProgress(query, (query.page() - 1) * query.size(), query.size());
long pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
return new PageResult<>(records, query.page(), query.size(), total, pages);
}
@Transactional(readOnly = true)
public ContainerPlanProgressDetail containerProgressDetail(String sourceContainerBookingId) {
String bookingId = sourceContainerBookingId == null ? null : sourceContainerBookingId.trim();
if (!StringUtils.hasText(bookingId)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能为空");
}
ContainerPlanProgressSummary summary = orderMapper.findContainerProgress(bookingId);
if (summary == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
}
return new ContainerPlanProgressDetail(summary, orderMapper.findContainerProgressItems(bookingId));
}
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
LastlegOrder order = findOwned(customerId, orderId);
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
@@ -83,6 +98,9 @@ public class LastlegOrderService {
if (ids.isEmpty()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "At least one order is required");
}
FbaWarehouseValidator.VerifiedFbaWarehouse target = fbaWarehouseValidator.required(
request.warehouseId(), request.warehouseCode(), request.warehouseName());
String warehouseId = Long.toString(target.id());
List<LastlegOrder> orders = orderMapper.selectList(new LambdaQueryWrapper<LastlegOrder>()
.eq(StringUtils.hasText(customerId), LastlegOrder::getCustomerId, customerId)
.in(LastlegOrder::getId, ids).last("FOR UPDATE"));
@@ -95,25 +113,25 @@ public class LastlegOrderService {
LocalDateTime now = LocalDateTime.now();
for (LastlegOrder order : orders) {
String fromStatus = order.getCurrentStatus();
order.setTransferWarehouseId(request.warehouseId());
order.setTransferWarehouseCode(request.warehouseCode());
order.setTransferWarehouseName(request.warehouseName());
order.setTransferWarehouseId(warehouseId);
order.setTransferWarehouseCode(target.code());
order.setTransferWarehouseName(target.name());
order.setCurrentStatus(DESTINATION_ASSIGNED);
order.setUpdatedAt(now);
orderMapper.updateById(order);
LastlegOrderDetail detailUpdate = new LastlegOrderDetail();
detailUpdate.setDestinationWarehouseId(request.warehouseId());
detailUpdate.setDestinationWarehouseCode(request.warehouseCode());
detailUpdate.setDestinationWarehouseName(request.warehouseName());
detailUpdate.setDestinationWarehouseId(warehouseId);
detailUpdate.setDestinationWarehouseCode(target.code());
detailUpdate.setDestinationWarehouseName(target.name());
detailMapper.update(detailUpdate, new LambdaQueryWrapper<LastlegOrderDetail>()
.eq(LastlegOrderDetail::getCustomerId, order.getCustomerId())
.eq(LastlegOrderDetail::getOrderId, order.getId()));
InboundPlanCarton cartonUpdate = new InboundPlanCarton();
cartonUpdate.setDestinationWarehouseId(request.warehouseId());
cartonUpdate.setDestinationWarehouseCode(request.warehouseCode());
cartonUpdate.setDestinationWarehouseName(request.warehouseName());
cartonUpdate.setDestinationWarehouseId(warehouseId);
cartonUpdate.setDestinationWarehouseCode(target.code());
cartonUpdate.setDestinationWarehouseName(target.name());
cartonMapper.update(cartonUpdate, new LambdaQueryWrapper<InboundPlanCarton>()
.eq(InboundPlanCarton::getCustomerId, order.getCustomerId())
.eq(InboundPlanCarton::getOrderId, order.getId()));
@@ -124,7 +142,7 @@ public class LastlegOrderService {
history.setFromStatus(fromStatus);
history.setToStatus(DESTINATION_ASSIGNED);
history.setRemark(StringUtils.hasText(request.remark())
? request.remark() : "Destination warehouse assigned: " + request.warehouseCode());
? request.remark() : "Destination warehouse assigned: " + target.code());
history.setCreatedAt(now);
historyMapper.insert(history);
}
@@ -190,6 +190,7 @@ public class LoadingCompletedConsumer {
order.setContainerStatus("待转柜"); order.setAbnormal(false); order.setDetained(false);
order.setPackageCount(source.packageCount()); order.setPalletCount(dropshipPallets.size());
order.setProductQuantity(source.productQuantity()); order.setVolumeCbm(source.volumeCbm());
order.setEstimatedWarehouseAt(parsePlannedArrival(payload.estimatedWarehouseArrivalAt()));
order.setVolumeWeightKg(source.items().stream().map(FirstLegLoadingPlanCompletedV1.Item::dimensionalWeightKg)
.filter(Objects::nonNull).reduce(BigDecimal.ZERO,BigDecimal::add));
order.setCurrentStatus("PENDING_ASSIGNMENT"); order.setSourceEventId(event.eventId());
@@ -237,6 +238,15 @@ public class LoadingCompletedConsumer {
}
}
private LocalDateTime parsePlannedArrival(String value) {
if (value == null || value.isBlank()) return null;
try {
return LocalDateTime.parse(value.trim());
} catch (java.time.format.DateTimeParseException exception) {
throw new IllegalArgumentException("事件计划到仓时间 estimatedWarehouseArrivalAt 无效", exception);
}
}
private void createInboundOrderSnapshot(EventEnvelope event, FirstLegLoadingPlanCompletedV1 payload,
FirstLegLoadingPlanCompletedV1.Order source, LastlegOrder order,
String orderHash) {