优化
This commit is contained in:
+13
@@ -7,6 +7,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
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.security.InternalServiceTokenGuard;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.baiyue.lastleg.service.LastlegOrderService;
|
||||
@@ -71,6 +72,18 @@ public class LastlegOrderController {
|
||||
return ApiResponse.success(orderService.page(platformScope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/container-summaries")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
@Operation(summary = "分页查询待分配柜级摘要",
|
||||
description = "按稳定 sourceContainerBookingId 聚合待分配尾程订单;平台跨客户查询。Required authority: "
|
||||
+ OverseasPermissions.LAST_MILE_ORDER_LIST)
|
||||
public ApiResponse<PageResult<PendingContainerSummary>> containerSummaries(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid LastlegOrderRequests.PendingContainerQuery query) {
|
||||
platformScope(user, null);
|
||||
return ApiResponse.success(orderService.pendingContainerPage(query));
|
||||
}
|
||||
|
||||
@GetMapping("/api/platform/v1/last-leg/orders/{id}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.LAST_MILE_ORDER_LIST + "')")
|
||||
@Operation(summary = "Get one inbound-code-level tail-leg order", description = "Required authority: " + OverseasPermissions.LAST_MILE_ORDER_LIST)
|
||||
|
||||
@@ -36,6 +36,19 @@ public final class LastlegOrderRequests {
|
||||
}
|
||||
}
|
||||
|
||||
public record PendingContainerQuery(
|
||||
@Min(1) long page,
|
||||
@Min(1) @Max(200) long size,
|
||||
String containerNo,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo
|
||||
) {
|
||||
public PendingContainerQuery {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
|
||||
public record TransferDestination(
|
||||
@NotEmpty @Size(max = 200) List<Long> orderIds,
|
||||
@NotBlank @Size(max = 128) String warehouseId,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.baiyue.lastleg.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 平台待分配尾程订单按来源货柜聚合的只读摘要。 */
|
||||
public record PendingContainerSummary(
|
||||
String sourceContainerBookingId,
|
||||
String containerNo,
|
||||
long orderCount,
|
||||
long inboundCodeCount,
|
||||
long customerCount,
|
||||
long totalPackageCount,
|
||||
long totalPalletCount,
|
||||
BigDecimal totalVolumeCbm,
|
||||
boolean eligible,
|
||||
String ineligibleReason
|
||||
) {
|
||||
}
|
||||
+46
-10
@@ -31,6 +31,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -73,7 +74,16 @@ public class InboundPlanService {
|
||||
return detail(customerId, existing);
|
||||
}
|
||||
validateOrders(orders);
|
||||
return createPlan(customerId, selectedOrderId, orders);
|
||||
}
|
||||
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders) {
|
||||
String containerNo = trimToNull(orders.get(0).containerNo());
|
||||
if (containerNo == null) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,
|
||||
"Inbound order container number is empty");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(IdWorker.getId());
|
||||
@@ -128,21 +138,47 @@ public class InboundPlanService {
|
||||
if (bookingId == null) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "来源柜预订 ID 不能为空");
|
||||
List<InboundPlanOrderMapper.ContainerOrderRef> refs = orderMapper.lockContainerOrders(bookingId);
|
||||
if (refs.isEmpty()) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "来源柜没有尾程订单");
|
||||
boolean hasEligible = refs.stream().anyMatch(ref -> "PENDING_ASSIGNMENT".equals(ref.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus()));
|
||||
if (!hasEligible) {
|
||||
List<InboundPlanResponse> replay = refs.stream().map(ref -> {
|
||||
InboundPlan existing = planMapper.findBySourceOrder(ref.customerId(), ref.orderId());
|
||||
if (existing == null) throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜转换状态不一致");
|
||||
return detail(ref.customerId(), existing);
|
||||
}).toList();
|
||||
return replay;
|
||||
List<InboundPlan> existingPlans = refs.stream()
|
||||
.map(ref -> planMapper.findBySourceOrder(ref.customerId(), ref.orderId()))
|
||||
.toList();
|
||||
long existingCount = existingPlans.stream().filter(Objects::nonNull).count();
|
||||
if (existingCount == refs.size()) {
|
||||
List<InboundPlanResponse> replay = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
replay.add(detail(refs.get(index).customerId(), existingPlans.get(index)));
|
||||
}
|
||||
return List.copyOf(replay);
|
||||
}
|
||||
if (existingCount != 0) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜仅部分尾程订单已有入库计划");
|
||||
}
|
||||
if (refs.stream().anyMatch(ref -> !("PENDING_ASSIGNMENT".equals(ref.currentStatus())
|
||||
|| "DESTINATION_ASSIGNED".equals(ref.currentStatus())))) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜存在不能转换的尾程订单");
|
||||
}
|
||||
return refs.stream().map(ref -> convert(ref.customerId(), ref.orderId())).toList();
|
||||
|
||||
List<List<SourceOrderSnapshot>> snapshots = new ArrayList<>(refs.size());
|
||||
String containerNo = null;
|
||||
for (InboundPlanOrderMapper.ContainerOrderRef ref : refs) {
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(ref.customerId(), ref.orderId());
|
||||
if (orders == null || orders.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜订单在锁定后不存在");
|
||||
}
|
||||
validateOrders(orders);
|
||||
String currentContainerNo = trimToNull(orders.get(0).containerNo());
|
||||
if (containerNo == null) containerNo = currentContainerNo;
|
||||
if (!Objects.equals(containerNo, currentContainerNo)) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "来源柜内尾程订单柜号不一致");
|
||||
}
|
||||
snapshots.add(orders);
|
||||
}
|
||||
|
||||
List<InboundPlanResponse> created = new ArrayList<>(refs.size());
|
||||
for (int index = 0; index < refs.size(); index++) {
|
||||
InboundPlanOrderMapper.ContainerOrderRef ref = refs.get(index);
|
||||
created.add(createPlan(ref.customerId(), ref.orderId(), snapshots.get(index)));
|
||||
}
|
||||
return List.copyOf(created);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,47 @@ package com.baiyue.lastleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.dto.LastlegOrderRequests;
|
||||
import com.baiyue.lastleg.dto.PendingContainerSummary;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderMapper extends BaseMapper<LastlegOrder> {
|
||||
String PENDING_CONTAINER_FILTER = " FROM ll_order o " +
|
||||
"WHERE o.source_container_booking_id IS NOT NULL AND o.source_container_booking_id<>'' " +
|
||||
"<if test='q.containerNo != null and q.containerNo != \"\"'>AND o.container_no LIKE CONCAT('%',#{q.containerNo},'%') </if>" +
|
||||
"<if test='q.createdFrom != null'>AND o.created_at >=#{q.createdFrom} </if>" +
|
||||
"<if test='q.createdTo != null'>AND o.created_at <=#{q.createdTo} </if>" +
|
||||
"GROUP BY o.source_container_booking_id " +
|
||||
"HAVING SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 1 ELSE 0 END)>0 ";
|
||||
|
||||
@Select("<script>SELECT o.source_container_booking_id AS sourceContainerBookingId," +
|
||||
"CASE WHEN COUNT(DISTINCT NULLIF(o.container_no,''))=1 THEN MAX(NULLIF(o.container_no,'')) ELSE NULL END AS containerNo," +
|
||||
"COUNT(*) AS orderCount,COUNT(DISTINCT o.inbound_code) AS inboundCodeCount," +
|
||||
"COUNT(DISTINCT o.customer_id) AS customerCount,COALESCE(SUM(o.package_count),0) AS totalPackageCount," +
|
||||
"COALESCE(SUM(o.pallet_count),0) AS totalPalletCount,COALESCE(SUM(o.volume_cbm),0) AS totalVolumeCbm," +
|
||||
"CASE WHEN SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 0 ELSE 1 END)=0 " +
|
||||
"AND COUNT(NULLIF(o.container_no,''))=COUNT(*) AND COUNT(DISTINCT NULLIF(o.container_no,''))=1 " +
|
||||
"THEN TRUE ELSE FALSE END AS eligible," +
|
||||
"CASE WHEN COUNT(NULLIF(o.container_no,''))<>COUNT(*) " +
|
||||
"OR COUNT(DISTINCT NULLIF(o.container_no,''))<>1 THEN '柜号缺失或不一致' " +
|
||||
"WHEN SUM(CASE WHEN o.current_status IN ('PENDING_ASSIGNMENT','DESTINATION_ASSIGNED') THEN 0 ELSE 1 END)>0 " +
|
||||
"THEN '柜内存在已生成计划或非待分配订单' ELSE NULL END AS ineligibleReason" +
|
||||
PENDING_CONTAINER_FILTER +
|
||||
"ORDER BY MAX(o.created_at) DESC,o.source_container_booking_id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<PendingContainerSummary> pagePendingContainers(@Param("q") LastlegOrderRequests.PendingContainerQuery query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*) FROM (SELECT o.source_container_booking_id" +
|
||||
PENDING_CONTAINER_FILTER + ") pending_containers</script>")
|
||||
long countPendingContainers(@Param("q") LastlegOrderRequests.PendingContainerQuery query);
|
||||
|
||||
@Select("SELECT * FROM ll_order WHERE customer_id=#{customerId} AND source_region=#{sourceRegion} " +
|
||||
"AND source_inbound_id=#{sourceInboundId} LIMIT 1")
|
||||
LastlegOrder findSource(@Param("customerId") String customerId,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
@@ -46,6 +47,21 @@ public class LastlegOrderService {
|
||||
page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<PendingContainerSummary> pendingContainerPage(
|
||||
LastlegOrderRequests.PendingContainerQuery 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 pages = total == 0 ? 0 : (total + query.size() - 1) / query.size();
|
||||
return new PageResult<>(records, query.page(), query.size(), total, pages);
|
||||
}
|
||||
|
||||
public LastlegOrderDetailResponse detail(String customerId, long orderId) {
|
||||
LastlegOrder order = findOwned(customerId, orderId);
|
||||
List<LastlegOrderDetail> cargo = detailMapper.selectList(new LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
|
||||
Reference in New Issue
Block a user