This commit is contained in:
2026-09-15 16:10:55 +08:00
parent 506525524b
commit c207a4d3b7
20 changed files with 693 additions and 93 deletions
@@ -10,6 +10,7 @@ import com.baiyue.lastleg.dto.LastlegOrderSummary;
import com.baiyue.lastleg.dto.LastlegOrderCommandDtos;
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
import com.baiyue.lastleg.dto.ContainerArrivalTimeView;
import com.baiyue.lastleg.financefee.entity.OverseasFeeSource;
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
import com.baiyue.lastleg.security.OverseasPermissions;
@@ -40,6 +41,7 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.util.List;
@RestController
@Tag(name = "Tail-leg orders")
@@ -112,6 +114,31 @@ public class LastlegOrderController {
return ApiResponse.success(orderService.containerProgressDetail(sourceContainerBookingId));
}
@PostMapping("/api/platform/v1/last-leg/orders/container-arrival-times/query")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
@Operation(summary = "批量查询整柜预计与实际到仓时间",
description = "预计到仓取平台人工维护的整柜冻结值;实际到仓取海外PDA首次开始收货时间。")
public ApiResponse<List<ContainerArrivalTimeView>> containerArrivalTimes(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody LastlegOrderRequests.ContainerArrivalTimeQuery request) {
platformScope(user, null);
return ApiResponse.success(orderService.containerArrivalTimes(request));
}
@PutMapping("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}/expected-arrival")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "修改整柜预计到仓时间",
description = "在入库计划生成前原子更新来源柜全部尾程订单;入库计划统一读取该柜时间。")
public ApiResponse<ContainerArrivalTimeView> changeExpectedWarehouseArrival(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String sourceContainerBookingId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody LastlegOrderRequests.ChangeExpectedWarehouseArrival request) {
platformScope(user, null);
return ApiResponse.success(orderService.changeExpectedWarehouseArrival(sourceContainerBookingId,
idempotencyKey, request, operatorName(user)));
}
@PutMapping("/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}/receiving-warehouse")
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_EDIT + "')")
@Operation(summary = "修改整柜海外接柜仓",
@@ -0,0 +1,11 @@
package com.baiyue.lastleg.dto;
import java.time.LocalDateTime;
/** 平台柜子工作台使用的整柜到仓时间事实。 */
public record ContainerArrivalTimeView(
String sourceContainerBookingId,
LocalDateTime estimatedWarehouseArrivalAt,
LocalDateTime actualWarehouseArrivalAt,
int version
) { }
@@ -61,6 +61,16 @@ public final class LastlegOrderRequests {
@Size(max = 500) String remark
) { }
public record ContainerArrivalTimeQuery(
@NotEmpty @Size(max = 200) List<@NotBlank @Size(max = 128) String> sourceContainerBookingIds
) { }
public record ChangeExpectedWarehouseArrival(
@NotNull LocalDateTime estimatedWarehouseArrivalAt,
@NotNull @Min(0) Integer expectedVersion,
@Size(max = 500) String remark
) { }
public record TransferDestination(
@NotEmpty @Size(max = 200) List<Long> orderIds,
@NotBlank @Size(max = 128) String warehouseId,
@@ -0,0 +1,21 @@
package com.baiyue.lastleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_container_arrival_time_operation")
public class ContainerArrivalTimeOperation {
@TableId(type = IdType.INPUT) private Long id;
private String sourceContainerBookingId;
private String idempotencyKey;
private String requestHash;
private Integer resultVersion;
private String operatorName;
private String remark;
private LocalDateTime createdAt;
}
@@ -52,6 +52,7 @@ public class LastlegOrder {
private LocalDateTime estimatedArrivalAt;
private LocalDateTime actualArrivalAt;
private LocalDateTime estimatedWarehouseAt;
private Integer arrivalTimeVersion;
private String inboundOrderNo;
private String outboundOrderNo;
private String deliveryOrderNo;
@@ -0,0 +1,13 @@
package com.baiyue.lastleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ContainerArrivalTimeOperationMapper extends BaseMapper<ContainerArrivalTimeOperation> {
@Select("SELECT * FROM ll_container_arrival_time_operation WHERE idempotency_key=#{key} LIMIT 1")
ContainerArrivalTimeOperation findByKey(@Param("key") String key);
}
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
import com.baiyue.lastleg.dto.ContainerArrivalTimeView;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -138,6 +139,29 @@ public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
@Select("SELECT * FROM ll_order WHERE source_container_booking_id=#{sourceContainerBookingId} ORDER BY customer_id,id FOR UPDATE")
List<LastlegOrder> lockContainerOrders(@Param("sourceContainerBookingId") String sourceContainerBookingId);
@Select("<script>SELECT o.source_container_booking_id AS sourceContainerBookingId," +
"CASE WHEN COUNT(o.estimated_warehouse_at)=COUNT(*) AND COUNT(DISTINCT o.estimated_warehouse_at)=1 " +
"THEN MAX(o.estimated_warehouse_at) ELSE NULL END AS estimatedWarehouseArrivalAt," +
"(SELECT MIN(s.receiving_at) FROM ll_receiving_container_session s " +
"WHERE s.source_container_booking_id=o.source_container_booking_id AND s.receiving_at IS NOT NULL) AS actualWarehouseArrivalAt," +
"CASE WHEN COUNT(DISTINCT o.arrival_time_version)=1 THEN MAX(o.arrival_time_version) ELSE -1 END AS version " +
"FROM ll_order o WHERE o.source_container_booking_id IN " +
"<foreach collection='bookingIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"GROUP BY o.source_container_booking_id</script>")
List<ContainerArrivalTimeView> findContainerArrivalTimes(@Param("bookingIds") List<String> bookingIds);
@Update("UPDATE ll_order SET estimated_warehouse_at=#{estimatedAt},arrival_time_version=arrival_time_version+1," +
"version=version+1,updated_at=NOW(3) WHERE source_container_booking_id=#{bookingId} " +
"AND arrival_time_version=#{expectedVersion}")
int updateContainerExpectedWarehouseArrival(@Param("bookingId") String bookingId,
@Param("estimatedAt") java.time.LocalDateTime estimatedAt,
@Param("expectedVersion") int expectedVersion);
@Select("SELECT COUNT(*) FROM ll_inbound_plan p JOIN ll_order o " +
"ON o.id=p.source_order_id AND o.customer_id=p.customer_id " +
"WHERE o.source_container_booking_id=#{bookingId}")
long countContainerPlans(@Param("bookingId") String bookingId);
@Select("SELECT COUNT(*) FROM ll_receiving_container_session WHERE source_container_booking_id=#{sourceContainerBookingId}")
long countReceivingSessions(@Param("sourceContainerBookingId") String sourceContainerBookingId);
@@ -11,12 +11,14 @@ import com.baiyue.lastleg.dto.LastlegOrderRequests;
import com.baiyue.lastleg.dto.LastlegOrderSummary;
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary;
import com.baiyue.lastleg.dto.ContainerArrivalTimeView;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.entity.LastlegOrderOperationLog;
import com.baiyue.lastleg.entity.LastlegOrderManualNode;
import com.baiyue.lastleg.entity.ReceivingWarehouseOperation;
import com.baiyue.lastleg.entity.ContainerArrivalTimeOperation;
import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanCarton;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper;
@@ -30,6 +32,7 @@ import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService;
import com.baiyue.lastleg.mapper.LastlegOrderOperationLogMapper;
import com.baiyue.lastleg.mapper.LastlegOrderManualNodeMapper;
import com.baiyue.lastleg.mapper.ReceivingWarehouseOperationMapper;
import com.baiyue.lastleg.mapper.ContainerArrivalTimeOperationMapper;
import com.baiyue.lastleg.model.LastlegOrderType;
import com.baiyue.lastleg.shipping.entity.ContainerShippingGate;
import com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper;
@@ -49,6 +52,7 @@ import java.time.ZoneOffset;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -77,6 +81,7 @@ public class LastlegOrderService {
@Resource private InventoryCommandService inventory;
@Resource private InboundOrderDetainedOutboxService detainedOutbox;
@Resource private ReceivingWarehouseOperationMapper receivingWarehouseOperations;
@Resource private ContainerArrivalTimeOperationMapper containerArrivalTimeOperations;
@Resource private WarehouseResourceGateway warehouseResourceGateway;
@Resource private ContainerShippingGateMapper containerShippingGates;
@@ -126,6 +131,76 @@ public class LastlegOrderService {
orderMapper.findContainerProgressItems(bookingId));
}
@Transactional(readOnly = true)
public List<ContainerArrivalTimeView> containerArrivalTimes(
LastlegOrderRequests.ContainerArrivalTimeQuery request) {
LinkedHashSet<String> bookingIds = new LinkedHashSet<>();
for (String value : request.sourceContainerBookingIds()) bookingIds.add(normalizeBookingId(value));
if (bookingIds.size() != request.sourceContainerBookingIds().size()) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订ID不能重复");
}
return orderMapper.findContainerArrivalTimes(List.copyOf(bookingIds));
}
@Transactional
public ContainerArrivalTimeView changeExpectedWarehouseArrival(String sourceContainerBookingId,
String idempotencyKey, LastlegOrderRequests.ChangeExpectedWarehouseArrival request,
String operatorName) {
String bookingId = normalizeBookingId(sourceContainerBookingId);
String key = normalizeIdempotencyKey(idempotencyKey);
String requestHash = hash(bookingId + "\n" + request.estimatedWarehouseArrivalAt() + "\n"
+ request.expectedVersion() + "\n" + Objects.toString(request.remark(), ""));
ContainerArrivalTimeOperation replay = containerArrivalTimeOperations.findByKey(key);
if (replay != null) {
if (!bookingId.equals(replay.getSourceContainerBookingId())
|| !requestHash.equals(replay.getRequestHash())) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Idempotency-Key 已用于其他预计到仓时间修改请求");
}
return requireContainerArrivalTime(bookingId);
}
List<LastlegOrder> orders = orderMapper.lockContainerOrders(bookingId);
if (orders.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
int version = requireSingleArrivalTimeVersion(orders);
if (version != request.expectedVersion()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "预计到仓时间版本已变化,请刷新后重试");
}
if (orderMapper.countContainerPlans(bookingId) > 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "该柜已生成入库计划,预计到仓时间不可修改");
}
int updated = orderMapper.updateContainerExpectedWarehouseArrival(bookingId,
request.estimatedWarehouseArrivalAt(), version);
if (updated != orders.size()) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间发生并发变化");
}
ContainerArrivalTimeOperation operation = new ContainerArrivalTimeOperation();
operation.setId(IdWorker.getId()); operation.setSourceContainerBookingId(bookingId);
operation.setIdempotencyKey(key); operation.setRequestHash(requestHash);
operation.setResultVersion(version + 1); operation.setOperatorName(operatorName);
operation.setRemark(request.remark() == null ? null : request.remark().trim());
operation.setCreatedAt(LocalDateTime.now()); containerArrivalTimeOperations.insert(operation);
return requireContainerArrivalTime(bookingId);
}
private ContainerArrivalTimeView requireContainerArrivalTime(String bookingId) {
List<ContainerArrivalTimeView> values = orderMapper.findContainerArrivalTimes(List.of(bookingId));
if (values.size() != 1) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜不存在");
if (values.get(0).version() < 0) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间版本不一致");
}
return values.get(0);
}
private int requireSingleArrivalTimeVersion(List<LastlegOrder> orders) {
Set<Integer> versions = orders.stream().map(LastlegOrder::getArrivalTimeVersion)
.map(value -> value == null ? 0 : value).collect(Collectors.toSet());
if (versions.size() != 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, "柜内预计到仓时间版本不一致");
}
return versions.iterator().next();
}
@Transactional
public ContainerPlanProgressSummary changeReceivingWarehouse(String sourceContainerBookingId,
String idempotencyKey, LastlegOrderRequests.ChangeReceivingWarehouse request, String operatorName) {