This commit is contained in:
2026-08-25 09:52:49 +08:00
parent b211121365
commit b67217332a
37 changed files with 1009 additions and 343 deletions
@@ -7,9 +7,11 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
import com.baiyue.lastleg.trucking.entity.UsTruckOrderOperation;
import com.baiyue.lastleg.trucking.entity.UsTruckOrderPallet;
import com.baiyue.lastleg.trucking.entity.UsTruckOrderStatusHistory;
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderMapper;
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderOperationMapper;
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderPalletMapper;
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderStatusHistoryMapper;
import com.baiyue.lastleg.security.OverseasPermissions;
@@ -30,6 +32,7 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HexFormat;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
@@ -55,20 +58,12 @@ public class UsTruckOrderService {
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, WAITING_INBOUND, RECEIVING,
WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, PARTIALLY_ALLOCATED, ALLOCATED,
PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, REJECTED, CANCELLED);
private static final java.util.Map<String, Set<String>> TRANSITIONS = java.util.Map.ofEntries(
java.util.Map.entry(PENDING_REVIEW, Set.of(WAITING_INBOUND, REJECTED, CANCELLED)),
java.util.Map.entry(WAITING_INBOUND, Set.of(RECEIVING, CANCELLED)),
java.util.Map.entry(RECEIVING, Set.of(WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, CANCELLED)),
java.util.Map.entry(PALLET_OVERAGE_REVIEW, Set.of(WAITING_CONSOLIDATION, CANCELLED)),
java.util.Map.entry(WAITING_CONSOLIDATION, Set.of(PARTIALLY_ALLOCATED, ALLOCATED, CANCELLED)),
java.util.Map.entry(PARTIALLY_ALLOCATED, Set.of(ALLOCATED, CANCELLED)),
java.util.Map.entry(ALLOCATED, Set.of(PENDING_DISPATCH, CANCELLED)),
java.util.Map.entry(PENDING_DISPATCH, Set.of(DISPATCHED, CANCELLED)),
java.util.Map.entry(DISPATCHED, Set.of(IN_TRANSIT)),
java.util.Map.entry(IN_TRANSIT, Set.of(COMPLETED)));
private static final Set<String> CANCELLABLE = Set.of(PENDING_REVIEW, WAITING_INBOUND, RECEIVING,
PALLET_OVERAGE_REVIEW, WAITING_CONSOLIDATION, PARTIALLY_ALLOCATED, ALLOCATED, PENDING_DISPATCH);
private static final Set<String> TRANSPORT_TYPES = Set.of("PORT_PICKUP", "WAREHOUSE_TO_WAREHOUSE", "WAREHOUSE_TO_DOOR");
@Resource private UsTruckOrderMapper orders;
@Resource private UsTruckOrderOperationMapper operations;
@Resource private UsTruckOrderPalletMapper pallets;
@Resource private UsTruckOrderStatusHistoryMapper histories;
@Resource private ObjectMapper objectMapper;
@@ -90,7 +85,7 @@ public class UsTruckOrderService {
order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress()));
order.setContainerType(StringUtils.hasText(request.containerType()) ? norm(request.containerType()) : null);
order.setPalletCount(request.palletCount()); order.setPackageCount(value(request.packageCount()));
order.setReceivedPalletCount(0); order.setAllocatedPalletCount(0); order.setShippedPalletCount(0);
order.setReceivedPalletCount(0); order.setAllocatedPalletCount(0);
order.setTotalWeightKg(request.totalWeightKg()); order.setTotalVolumeCbm(request.totalVolumeCbm());
order.setBudgetAmount(request.budgetAmount()); order.setCurrency(defaultCurrency(request.currency()));
order.setContactName(request.contactName().trim()); order.setContactInfo(request.contactInfo().trim());
@@ -103,7 +98,7 @@ public class UsTruckOrderService {
if (concurrent != null) return replay(customerId, concurrent, hash);
throw e;
}
history(order, null, PENDING_REVIEW, operatorId, request.remark(), now);
history(order, "CREATE", null, PENDING_REVIEW, operatorId, key, request.remark(), now);
return detail(customerId, order.getOrderNo());
}
@@ -150,7 +145,7 @@ public class UsTruckOrderService {
setText(request.contactName(), order::setContactName); setText(request.contactInfo(), order::setContactInfo);
if (request.requestedDate() != null) order.setRequestedDate(request.requestedDate()); setPresent(request.remark(), order::setRemark);
touch(order); orders.updateById(order);
history(order, order.getStatus(), order.getStatus(), operatorId, "UPDATE", LocalDateTime.now());
history(order, "UPDATE", order.getStatus(), order.getStatus(), operatorId, null, "UPDATE", LocalDateTime.now());
return detail(customerId, orderNo);
}
@@ -158,34 +153,37 @@ public class UsTruckOrderService {
public UsTruckOrderResponses.Detail changeStatus(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.ChangeStatus request) {
UsTruckOrder order = lock(customerId, orderNo);
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
String next = norm(request.status()); if (!STATUSES.contains(next)) throw badRequest("Unsupported truck order status");
String from = order.getStatus();
if (!next.equals(from)) {
if (!TRANSITIONS.getOrDefault(from, Set.of()).contains(next)) {
throw conflict("Unsupported truck order transition: " + from + " -> " + next);
}
order.setStatus(next);
if (PENDING_REVIEW.equals(from) && WAITING_INBOUND.equals(next)) createPlannedPallets(order);
touch(order); orders.updateById(order);
history(order, from, next, operatorId, request.remark(), LocalDateTime.now());
UsTruckOrderResponses.Detail replay = replayOperation(order, "CANCEL", request.idempotencyKey(), request);
if (replay != null) return replay;
requireVersion(order, request.version());
if (!CANCELLED.equals(norm(request.status())) || !CANCELLABLE.contains(order.getStatus())) {
throw conflict("Only cancellable truck orders can use the generic status endpoint");
}
return detail(customerId, orderNo);
return transition(order, operatorId, "CANCEL", request.idempotencyKey(), request,
order.getStatus(), CANCELLED, request.remark());
}
@Transactional
public UsTruckOrderResponses.Detail review(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.Review request) {
UsTruckOrder order = lock(customerId, orderNo);
UsTruckOrderResponses.Detail replay = replayOperation(order, "REVIEW", request.idempotencyKey(), request);
if (replay != null) return replay;
requireVersion(order, request.version());
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("Only pending-review truck orders can be reviewed");
String next = "APPROVE".equals(norm(request.decision())) ? WAITING_INBOUND : REJECTED;
if (WAITING_INBOUND.equals(next)) createPlannedPallets(order);
return transition(order, operatorId, "REVIEW", request.idempotencyKey(), request,
PENDING_REVIEW, next, request.remark());
}
@Transactional
public UsTruckOrderResponses.Detail receivePallets(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.ReceivePallets request) {
UsTruckOrder order = lock(customerId, orderNo);
String key = request.idempotencyKey().trim();
String requestHash = hash(request);
if (key.equals(order.getReceiptIdempotencyKey())) {
if (!requestHash.equals(order.getReceiptRequestHash())) {
throw conflict("Receipt idempotency key was reused with different pallet data");
}
return detail(customerId, orderNo);
}
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
UsTruckOrderResponses.Detail replay = replayOperation(order, "RECEIVE_PALLETS", request.idempotencyKey(), request);
if (replay != null) return replay;
requireVersion(order, request.version());
if (!Set.of(WAITING_INBOUND, RECEIVING).contains(order.getStatus())) {
throw conflict("Only waiting or receiving orders can receive pallets");
}
@@ -220,18 +218,118 @@ public class UsTruckOrderService {
}
}
order.setReceivedPalletCount(actual);
order.setReceiptIdempotencyKey(key);
order.setReceiptRequestHash(requestHash);
String from = order.getStatus();
String next = actual > planned ? PALLET_OVERAGE_REVIEW
: actual == planned ? WAITING_CONSOLIDATION : RECEIVING;
order.setStatus(next);
touch(order);
orders.updateById(order);
history(order, from, next, operatorId, request.remark(), now);
recordOperation(order, "RECEIVE_PALLETS", request.idempotencyKey(), request, operatorId, now);
history(order, "RECEIVE_PALLETS", from, next, operatorId, request.idempotencyKey(), request.remark(), now);
return detail(customerId, orderNo);
}
@Transactional
public UsTruckOrderResponses.Detail reviewPalletOverage(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.ReviewPalletOverage request) {
UsTruckOrder order = lock(customerId, orderNo);
UsTruckOrderResponses.Detail replay = replayOperation(order, "REVIEW_PALLET_OVERAGE",
request.idempotencyKey(), request);
if (replay != null) return replay;
requireVersion(order, request.version());
if (!PALLET_OVERAGE_REVIEW.equals(order.getStatus())) {
throw conflict("Only pallet-overage-review orders can be reviewed");
}
boolean approve = "APPROVE".equals(norm(request.decision()));
List<UsTruckOrderPallet> rows = pallets.listOrder(customerId, order.getId());
LocalDateTime now = LocalDateTime.now();
int accepted = value(order.getPalletCount());
for (UsTruckOrderPallet pallet : rows) {
if (!"OVERAGE_PENDING_REVIEW".equals(pallet.getStatus())) continue;
pallet.setStatus(approve ? "RECEIVED" : "OVERAGE_REJECTED");
pallet.setUpdatedAt(now);
pallets.updateById(pallet);
}
if (approve) {
accepted = value(order.getReceivedPalletCount());
order.setPalletCount(accepted);
} else {
order.setReceivedPalletCount(accepted);
}
order.setStatus(WAITING_CONSOLIDATION);
touch(order);
orders.updateById(order);
recordOperation(order, "REVIEW_PALLET_OVERAGE", request.idempotencyKey(), request, operatorId, now);
history(order, "REVIEW_PALLET_OVERAGE", PALLET_OVERAGE_REVIEW, WAITING_CONSOLIDATION,
operatorId, request.idempotencyKey(), request.remark(), now);
return detail(customerId, orderNo);
}
@Transactional
public UsTruckOrderResponses.Detail allocatePallets(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.AllocatePallets request) {
UsTruckOrder order = lock(customerId, orderNo);
UsTruckOrderResponses.Detail replay = replayOperation(order, "ALLOCATE_PALLETS",
request.idempotencyKey(), request);
if (replay != null) return replay;
requireVersion(order, request.version());
if (!Set.of(WAITING_CONSOLIDATION, PARTIALLY_ALLOCATED).contains(order.getStatus())) {
throw conflict("Only waiting-consolidation or partially-allocated orders can allocate pallets");
}
Set<Long> requested = new HashSet<>(request.palletIds());
if (requested.size() != request.palletIds().size()) throw badRequest("palletIds must be unique");
List<UsTruckOrderPallet> rows = pallets.listOrder(customerId, order.getId());
List<UsTruckOrderPallet> selected = rows.stream().filter(row -> requested.contains(row.getId())).toList();
if (selected.size() != requested.size() || selected.stream().anyMatch(row -> !"RECEIVED".equals(row.getStatus()))) {
throw conflict("Selected pallets must belong to the order and be received but unallocated");
}
LocalDateTime now = LocalDateTime.now();
for (UsTruckOrderPallet pallet : selected) {
pallet.setStatus("ALLOCATED");
pallet.setUpdatedAt(now);
pallets.updateById(pallet);
}
int allocated = (int) rows.stream().filter(row -> "ALLOCATED".equals(row.getStatus())).count()
+ selected.size();
int accepted = value(order.getReceivedPalletCount());
if (accepted < 1 || allocated > accepted) throw conflict("Allocated pallet count exceeds accepted pallets");
String from = order.getStatus();
String next = allocated == accepted ? ALLOCATED : PARTIALLY_ALLOCATED;
order.setAllocatedPalletCount(allocated);
order.setStatus(next);
touch(order);
orders.updateById(order);
recordOperation(order, "ALLOCATE_PALLETS", request.idempotencyKey(), request, operatorId, now);
history(order, "ALLOCATE_PALLETS", from, next, operatorId,
request.idempotencyKey(), request.remark(), now);
return detail(customerId, orderNo);
}
@Transactional
public UsTruckOrderResponses.Detail prepareDispatch(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.Command request) {
return command(customerId, orderNo, operatorId, "PREPARE_DISPATCH", ALLOCATED,
PENDING_DISPATCH, request);
}
@Transactional
public UsTruckOrderResponses.Detail dispatch(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.Command request) {
return command(customerId, orderNo, operatorId, "DISPATCH", PENDING_DISPATCH, DISPATCHED, request);
}
@Transactional
public UsTruckOrderResponses.Detail startTransit(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.Command request) {
return command(customerId, orderNo, operatorId, "START_TRANSIT", DISPATCHED, IN_TRANSIT, request);
}
@Transactional
public UsTruckOrderResponses.Detail complete(String customerId, String orderNo, String operatorId,
UsTruckOrderRequests.Command request) {
return command(customerId, orderNo, operatorId, "COMPLETE", IN_TRANSIT, COMPLETED, request);
}
private UsTruckOrder lock(String customerId, String orderNo) { UsTruckOrder o = orders.lockNo(customerId, orderNo); if (o == null) throw notFound(); return o; }
private UsTruckOrderResponses.Detail replay(String customerId, UsTruckOrder order, String hash) { if (!hash.equals(order.getRequestHash())) throw conflict("Idempotency key was reused with different truck data"); return detail(customerId, order.getOrderNo()); }
private UsTruckOrderResponses.Detail detail(UsTruckOrder o, List<UsTruckOrderStatusHistory> hs) {