This commit is contained in:
2026-08-21 14:42:40 +08:00
parent 5fd00d3151
commit e534784052
20 changed files with 274 additions and 34 deletions
@@ -38,23 +38,24 @@ public class SmartDispatchController {
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
public ApiResponse<List<DispatchPalletCandidate>> candidates(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam List<Long> planIds,
@RequestParam(required = false) String orderNo,
@RequestParam(required = false) String fbaWarehouseCode,
@RequestParam(required = false) String sourceStage) {
requirePlatform(user);
return ApiResponse.success(service.candidates(service.resolveSingleCustomerScope(), orderNo,
return ApiResponse.success(service.candidates(service.resolveCustomerScopeForPlans(planIds), planIds, orderNo,
fbaWarehouseCode, sourceStage));
}
@PostMapping("/pre-arrange")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
@Operation(summary = "Create a pending-departure dispatch and lock all selected pallets",
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH)
public ApiResponse<DispatchResponse> preArrange(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody DispatchRequests.PreArrange request) {
requirePlatform(user);
String scopedCustomer = service.resolveSingleCustomerScope();
String scopedCustomer = service.resolveCustomerScopeForPallets(request.planPalletIds());
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
}
@@ -15,6 +15,21 @@ public interface DispatchCandidateMapper {
"ORDER BY customer_id LIMIT 2")
List<String> findCustomerScopes();
@Select("<script>" +
"SELECT DISTINCT customer_id FROM ll_inbound_plan WHERE id IN " +
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND customer_id IS NOT NULL AND customer_id != '' ORDER BY customer_id LIMIT 2" +
"</script>")
List<String> findCustomerScopesByPlanIds(@Param("planIds") List<Long> planIds);
@Select("<script>" +
"SELECT DISTINCT ip.customer_id FROM ll_inbound_plan_pallet p " +
"JOIN ll_inbound_plan ip ON ip.id=p.plan_id WHERE p.id IN " +
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND ip.customer_id IS NOT NULL AND ip.customer_id != '' ORDER BY ip.customer_id LIMIT 2" +
"</script>")
List<String> findCustomerScopesByPlanPalletIds(@Param("planPalletIds") List<Long> planPalletIds);
@Select("<script>" +
"SELECT p.plan_id AS planId,p.id AS planPalletId,p.warehouse_pallet_id AS warehousePalletId," +
"MIN(l.order_id) AS orderId,MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
@@ -31,7 +46,8 @@ public interface DispatchCandidateMapper {
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
"LEFT JOIN ll_order o ON o.id=l.order_id " +
"WHERE ip.customer_id=#{customerId} " +
"WHERE ip.customer_id=#{customerId} AND p.plan_id IN " +
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
"<if test='sourceStage != null and sourceStage != \"\"'> AND " +
@@ -50,6 +66,7 @@ public interface DispatchCandidateMapper {
"ORDER BY p.plan_id,p.sequence_no" +
"</script>")
List<DispatchPalletCandidate> available(@Param("customerId") String customerId,
@Param("planIds") List<Long> planIds,
@Param("orderNo") String orderNo,
@Param("fbaWarehouseCode") String fbaWarehouseCode,
@Param("sourceStage") String sourceStage);
@@ -50,13 +50,26 @@ public class DispatchService {
return customerIds.isEmpty() ? null : customerIds.get(0);
}
public List<DispatchPalletCandidate> candidates(String customerId, String orderNo,
public String resolveCustomerScopeForPlans(List<Long> planIds) {
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
"Selected inbound plans");
}
public String resolveCustomerScopeForPallets(List<Long> planPalletIds) {
return requireSingleCustomerScope(
candidates.findCustomerScopesByPlanPalletIds(normalizeIds(planPalletIds, "Plan pallet IDs")),
"Selected pallets");
}
public List<DispatchPalletCandidate> candidates(String customerId, List<Long> planIds, String orderNo,
String fbaWarehouseCode, String sourceStage) {
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
}
if (!StringUtils.hasText(customerId)) return List.of();
return candidates.available(customerId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
List<Long> scopedPlanIds = normalizeIds(planIds, "Plan IDs");
return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
trimToNull(sourceStage));
}
@Transactional
@@ -278,4 +291,25 @@ public class DispatchService {
private static String trimToNull(String value) {
return StringUtils.hasText(value) ? value.trim() : null;
}
private static List<Long> normalizeIds(List<Long> ids, String label) {
if (ids == null || ids.isEmpty() || ids.stream().anyMatch(Objects::isNull)) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " must be non-empty");
}
List<Long> normalized = ids.stream().distinct().sorted().toList();
if (normalized.size() > 500) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " cannot exceed 500 items");
}
return normalized;
}
private static String requireSingleCustomerScope(List<String> customerIds, String source) {
if (customerIds == null || customerIds.isEmpty()) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, source + " were not found");
}
if (customerIds.size() > 1) {
throw new ResponseStatusException(HttpStatus.CONFLICT, source + " must belong to exactly one customer");
}
return customerIds.get(0);
}
}