This commit is contained in:
2026-08-20 18:06:07 +08:00
parent c9a05b108b
commit a38396ce3f
26 changed files with 532 additions and 21 deletions
@@ -1,12 +1,16 @@
package com.baiyue.lastleg.inboundplan.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
import io.swagger.v3.oas.annotations.Operation;
@@ -26,6 +30,17 @@ public class InboundPlanController {
@Resource private InboundPlanService service;
@Resource private InboundFbaAppointmentService appointmentService;
/** 按入仓计划明细分页查询可预约的入库订单行。 */
@GetMapping
@PreAuthorize("hasAuthority('platform:lastleg:inbound-plan:list')")
@Operation(summary = "分页查询入库订单")
public ApiResponse<PageResult<InboundPlanListResponse>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam String customerId,
@Valid InboundPlanRequests.Query query) {
return ApiResponse.success(service.page(scope(user, customerId), query));
}
@PostMapping("/convert")
@PreAuthorize("hasAuthority('platform:lastleg:inbound-plan:create')")
/** 将入仓单转换为入仓计划。 */
@@ -78,6 +93,17 @@ public class InboundPlanController {
return ApiResponse.success(appointmentService.detail(scope(user, customerId), appointmentNo));
}
@PutMapping("/fba-appointments/{appointmentNo}")
@PreAuthorize("hasAuthority('platform:lastleg:inbound-plan:appointment')")
@Operation(summary = "编辑 FBA 预约")
public ApiResponse<FbaAppointmentResponse> updateFbaAppointment(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String appointmentNo,
@Valid @RequestBody UpdateFbaAppointmentRequest request) {
return ApiResponse.success(appointmentService.update(scope(user, request.customerId()),
appointmentNo, request));
}
private String scope(AuthenticatedUser user, String requestedCustomer) {
if (user == null || user.tenantId() == null) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
@@ -13,6 +13,7 @@ public record BatchFbaAppointmentRequest(
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
@NotBlank @Size(max = 64) String appointmentNo,
@NotNull LocalDateTime appointmentAt,
@NotNull LocalDateTime expectedLoadingAt,
@NotBlank @Size(max = 32) String appointmentStatus,
@Size(max = 1000) String appointmentRemark
) {
@@ -8,6 +8,7 @@ public record FbaAppointmentResponse(
String customerId,
String appointmentNo,
LocalDateTime appointmentAt,
LocalDateTime expectedLoadingAt,
String appointmentStatus,
String appointmentRemark,
String fbaWarehouseCode,
@@ -0,0 +1,30 @@
package com.baiyue.lastleg.inboundplan.dto;
import java.math.BigDecimal;
import java.time.LocalDateTime;
public record InboundPlanListResponse(
Long lineId,
Long planId,
String planNo,
String customerId,
Long orderId,
String inboundCode,
String orderType,
String warehouseId,
String warehouseCode,
String warehouseName,
String fbaWarehouseCode,
String customerName,
Integer packageCount,
BigDecimal volumeCbm,
Integer palletCount,
String status,
String appointmentNo,
String appointmentStatus,
LocalDateTime appointmentAt,
LocalDateTime expectedLoadingAt,
String dispatchStatus,
LocalDateTime createdAt
) {
}
@@ -0,0 +1,31 @@
package com.baiyue.lastleg.inboundplan.dto;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDateTime;
public final class InboundPlanRequests {
private InboundPlanRequests() {
}
public record Query(
@Min(1) long page,
@Min(1) @Max(200) long size,
String planNo,
String inboundCode,
String status,
String warehouse,
String fbaWarehouseCode,
@Pattern(regexp = "UNAPPOINTED|PENDING|SUCCESS|FAILED|CANCELLED") String appointmentStatus,
@Pattern(regexp = "UNSCHEDULED|SCHEDULED") String dispatchStatus,
LocalDateTime createdFrom,
LocalDateTime createdTo
) {
public Query {
if (page == 0) page = 1;
if (size == 0) size = 20;
}
}
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.inboundplan.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.time.LocalDateTime;
public record UpdateFbaAppointmentRequest(
String customerId,
@NotNull LocalDateTime appointmentAt,
@NotNull LocalDateTime expectedLoadingAt,
@NotBlank @Size(max = 32) String appointmentStatus,
@Size(max = 1000) String appointmentRemark
) {
}
@@ -15,6 +15,7 @@ public class InboundFbaAppointment {
private String customerId;
private String appointmentNo;
private LocalDateTime appointmentAt;
private LocalDateTime expectedLoadingAt;
private String appointmentStatus;
private String appointmentRemark;
private String fbaWarehouseCode;
@@ -0,0 +1,53 @@
package com.baiyue.lastleg.inboundplan.mapper;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface InboundPlanQueryMapper {
String FROM_AND_FILTER = " FROM ll_inbound_plan_line l" +
" JOIN ll_inbound_plan p ON p.id=l.plan_id AND p.customer_id=#{customerId}" +
" JOIN ll_order o ON o.id=l.order_id AND o.customer_id=#{customerId}" +
" LEFT JOIN ll_inbound_fba_appointment_item ai ON ai.line_id=l.id" +
" AND ai.id=(SELECT ai2.id FROM ll_inbound_fba_appointment_item ai2" +
" JOIN ll_inbound_fba_appointment a2 ON a2.id=ai2.appointment_id" +
" WHERE ai2.line_id=l.id AND a2.customer_id=#{customerId}" +
" ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1)" +
" LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id AND a.customer_id=#{customerId}" +
" WHERE 1=1" +
"<if test='q.planNo != null and q.planNo != \"\"'> AND p.plan_no LIKE CONCAT('%',#{q.planNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.status != null and q.status != \"\"'> AND p.status=#{q.status}</if>" +
"<if test='q.warehouse != null and q.warehouse != \"\"'> AND (l.destination_warehouse_id LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_code LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_name LIKE CONCAT('%',#{q.warehouse},'%'))</if>" +
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%')</if>" +
"<choose><when test='q.appointmentStatus == \"UNAPPOINTED\"'> AND ai.id IS NULL</when>" +
"<when test='q.appointmentStatus != null and q.appointmentStatus != \"\"'> AND a.appointment_status=#{q.appointmentStatus}</when></choose>" +
"<choose><when test='q.dispatchStatus == \"SCHEDULED\"'> AND EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)</when>" +
"<when test='q.dispatchStatus == \"UNSCHEDULED\"'> AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)</when></choose>" +
"<if test='q.createdFrom != null'> AND p.created_at &gt;=#{q.createdFrom}</if>" +
"<if test='q.createdTo != null'> AND p.created_at &lt;=#{q.createdTo}</if>";
@Select("<script>SELECT l.id lineId,p.id planId,p.plan_no planNo,p.customer_id customerId," +
"l.order_id orderId,l.inbound_code inboundCode,o.order_type orderType," +
"l.destination_warehouse_id warehouseId,l.destination_warehouse_code warehouseCode," +
"l.destination_warehouse_name warehouseName,l.fba_warehouse_code fbaWarehouseCode," +
"o.customer_name customerName,l.required_package_count packageCount,l.volume_cbm volumeCbm," +
"l.planned_pallet_count palletCount,p.status status,a.appointment_no appointmentNo," +
"a.appointment_status appointmentStatus,a.appointment_at appointmentAt," +
"a.expected_loading_at expectedLoadingAt," +
"CASE WHEN EXISTS (SELECT 1 FROM ll_dispatch_item di WHERE di.customer_id=#{customerId} AND di.order_id=l.order_id)" +
" THEN 'SCHEDULED' ELSE 'UNSCHEDULED' END dispatchStatus,p.created_at createdAt" +
FROM_AND_FILTER + " ORDER BY p.created_at DESC,l.id DESC LIMIT #{limit} OFFSET #{offset}</script>")
List<InboundPlanListResponse> rows(@Param("customerId") String customerId,
@Param("q") InboundPlanRequests.Query query,
@Param("offset") long offset,
@Param("limit") long limit);
@Select("<script>SELECT COUNT(*)" + FROM_AND_FILTER + "</script>")
long count(@Param("customerId") String customerId, @Param("q") InboundPlanRequests.Query query);
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointment;
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
@@ -44,7 +45,7 @@ public class InboundFbaAppointmentService {
InboundFbaAppointment existing = find(customerId, appointmentNo);
if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
}
List<FbaAppointmentLineSnapshot> lines = queryMapper.lockLines(customerId, lineIds);
@@ -57,7 +58,7 @@ public class InboundFbaAppointmentService {
// The row locks serialize concurrent submissions selecting the same inbound lines.
existing = find(customerId, appointmentNo);
if (existing != null) {
return verifyReplay(existing, lineIds, request.appointmentAt(), status, remark);
return verifyReplay(existing, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
}
LocalDateTime now = LocalDateTime.now();
@@ -66,6 +67,7 @@ public class InboundFbaAppointmentService {
appointment.setCustomerId(customerId);
appointment.setAppointmentNo(appointmentNo);
appointment.setAppointmentAt(request.appointmentAt());
appointment.setExpectedLoadingAt(request.expectedLoadingAt());
appointment.setAppointmentStatus(status);
appointment.setAppointmentRemark(remark);
appointment.setFbaWarehouseCode(fbaWarehouseCode);
@@ -78,7 +80,7 @@ public class InboundFbaAppointmentService {
} catch (DuplicateKeyException exception) {
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
if (concurrent != null) {
return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark);
return verifyReplay(concurrent, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
}
throw exception;
}
@@ -108,12 +110,31 @@ public class InboundFbaAppointmentService {
return response(appointment, items(appointment.getId()));
}
@Transactional
public FbaAppointmentResponse update(String customerId, String appointmentNo,
UpdateFbaAppointmentRequest request) {
InboundFbaAppointment appointment = find(customerId,
requiredTrimmed(appointmentNo, "Appointment number is required"));
if (appointment == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "FBA appointment not found");
}
appointment.setAppointmentAt(request.appointmentAt());
appointment.setExpectedLoadingAt(request.expectedLoadingAt());
appointment.setAppointmentStatus(normalizeStatus(request.appointmentStatus()));
appointment.setAppointmentRemark(trimToNull(request.appointmentRemark()));
appointment.setUpdatedAt(LocalDateTime.now());
appointmentMapper.updateById(appointment);
return response(appointment, items(appointment.getId()));
}
private FbaAppointmentResponse verifyReplay(InboundFbaAppointment existing, List<Long> requestedLineIds,
LocalDateTime appointmentAt, String status, String remark) {
LocalDateTime appointmentAt, LocalDateTime expectedLoadingAt,
String status, String remark) {
List<InboundFbaAppointmentItem> existingItems = items(existing.getId());
Set<Long> existingLineIds = existingItems.stream()
.map(InboundFbaAppointmentItem::getLineId).collect(java.util.stream.Collectors.toSet());
if (!Objects.equals(existing.getAppointmentAt(), appointmentAt)
|| !Objects.equals(existing.getExpectedLoadingAt(), expectedLoadingAt)
|| !Objects.equals(existing.getAppointmentStatus(), status)
|| !Objects.equals(existing.getAppointmentRemark(), remark)
|| existingLineIds.size() != requestedLineIds.size()
@@ -149,7 +170,8 @@ public class InboundFbaAppointmentService {
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
List<InboundFbaAppointmentItem> items) {
return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getAppointmentStatus(),
appointment.getAppointmentNo(), appointment.getAppointmentAt(), appointment.getExpectedLoadingAt(),
appointment.getAppointmentStatus(),
appointment.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
items.stream().map(item -> new FbaAppointmentResponse.Item(item.getId(), item.getPlanId(),
@@ -2,6 +2,9 @@ package com.baiyue.lastleg.inboundplan.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
@@ -13,6 +16,7 @@ import com.baiyue.lastleg.inboundplan.mapper.InboundPlanMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanOrderMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
@@ -35,6 +39,16 @@ public class InboundPlanService {
@Resource private InboundPlanPalletMapper palletMapper;
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
@Resource private InboundPlanOrderMapper orderMapper;
@Resource private InboundPlanQueryMapper queryMapper;
@Transactional(readOnly = true)
public PageResult<InboundPlanListResponse> page(String customerId, InboundPlanRequests.Query query) {
long total = queryMapper.count(customerId, query);
List<InboundPlanListResponse> records = total == 0 ? List.of() : queryMapper.rows(
customerId, 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
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
@@ -1,6 +1,7 @@
package com.baiyue.lastleg.inventory.query.dto;
import java.time.LocalDateTime;
import java.util.List;
public record InventoryBalanceView(
Long id,
@@ -15,6 +16,10 @@ public record InventoryBalanceView(
Long inboundOrderId,
String inboundOrderNo,
String inboundCode,
List<String> skuCodes,
List<String> productNames,
List<Long> warehouseLocationIds,
List<String> warehouseLocationCodes,
long totalPackages,
long availablePackages,
long holdPackages,
@@ -13,6 +13,7 @@ public record InventoryLedgerView(
String palletNo,
String batchNo,
String warehouseLocationCode,
String skuCode,
String businessType,
String businessOrderNo,
String outboundOrderNo,
@@ -19,6 +19,10 @@ public final class InventoryQueryRequests {
String inboundOrderNo,
String inboundCode,
String businessOrderNo,
String skuCode,
String productName,
String batchNo,
String warehouseLocationCode,
String inventoryStatus
) {
public BalanceQuery {
@@ -42,6 +46,8 @@ public final class InventoryQueryRequests {
String outboundOrderNo,
String palletNo,
String batchNo,
String skuCode,
String warehouseLocationCode,
LocalDateTime occurredFrom,
LocalDateTime occurredTo
) {
@@ -5,6 +5,8 @@ import com.baiyue.lastleg.inventory.entity.InventoryLedger;
import com.baiyue.lastleg.inventory.entity.InventoryPosition;
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
import com.baiyue.lastleg.inventory.query.model.InventoryLocationFact;
import com.baiyue.lastleg.inventory.query.model.InventorySkuFact;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -19,6 +21,10 @@ public interface InventoryQueryMapper {
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=ll_inventory_balance.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_order_detail d WHERE d.order_id=ll_inventory_balance.inbound_order_id AND d.sku_code LIKE CONCAT('%',#{q.skuCode},'%'))</if>" +
"<if test='q.productName != null and q.productName != \"\"'> AND EXISTS (SELECT 1 FROM ll_order_detail d WHERE d.order_id=ll_inventory_balance.inbound_order_id AND (d.product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR d.product_name_en LIKE CONCAT('%',#{q.productName},'%')))</if>" +
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=#{customerId} AND p.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p WHERE p.balance_id=ll_inventory_balance.id AND p.customer_id=#{customerId} AND p.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
"<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages &gt; 0</when>" +
@@ -37,6 +43,10 @@ public interface InventoryQueryMapper {
"<if test='q.inboundOrderNo != null and q.inboundOrderNo != \"\"'> AND b.inbound_order_no LIKE CONCAT('%',#{q.inboundOrderNo},'%')</if>" +
"<if test='q.inboundCode != null and q.inboundCode != \"\"'> AND b.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%')</if>" +
"<if test='q.businessOrderNo != null and q.businessOrderNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_ledger l WHERE l.balance_id=b.id AND l.customer_id=#{customerId} AND l.business_order_no LIKE CONCAT('%',#{q.businessOrderNo},'%'))</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_order_detail d WHERE d.order_id=b.inbound_order_id AND d.sku_code LIKE CONCAT('%',#{q.skuCode},'%'))</if>" +
"<if test='q.productName != null and q.productName != \"\"'> AND EXISTS (SELECT 1 FROM ll_order_detail d WHERE d.order_id=b.inbound_order_id AND (d.product_name_zh LIKE CONCAT('%',#{q.productName},'%') OR d.product_name_en LIKE CONCAT('%',#{q.productName},'%')))</if>" +
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=#{customerId} AND p2.batch_no LIKE CONCAT('%',#{q.batchNo},'%'))</if>" +
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND EXISTS (SELECT 1 FROM ll_inventory_position p2 WHERE p2.balance_id=b.id AND p2.customer_id=#{customerId} AND p2.warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%'))</if>" +
"<choose>" +
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages &gt; 0</when>" +
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages &gt; 0</when>" +
@@ -58,6 +68,22 @@ public interface InventoryQueryMapper {
long countBalances(@Param("customerId") String customerId,
@Param("q") InventoryQueryRequests.BalanceQuery query);
@Select("<script>SELECT DISTINCT b.id balance_id,d.sku_code," +
"COALESCE(NULLIF(d.product_name_zh,''),d.product_name_en) product_name " +
"FROM ll_inventory_balance b JOIN ll_order_detail d ON d.order_id=b.inbound_order_id " +
"WHERE b.customer_id=#{customerId} AND b.id IN " +
"<foreach collection='balanceIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"AND d.sku_code IS NOT NULL AND d.sku_code&lt;&gt;'' ORDER BY b.id,d.sku_code</script>")
List<InventorySkuFact> skuFacts(@Param("customerId") String customerId,
@Param("balanceIds") List<Long> balanceIds);
@Select("<script>SELECT DISTINCT balance_id,warehouse_location_id,warehouse_location_code " +
"FROM ll_inventory_position WHERE customer_id=#{customerId} AND balance_id IN " +
"<foreach collection='balanceIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
"ORDER BY balance_id,warehouse_location_code</script>")
List<InventoryLocationFact> locationFacts(@Param("customerId") String customerId,
@Param("balanceIds") List<Long> balanceIds);
@Select("<script>SELECT COUNT(*) dimension_count," +
"COALESCE(SUM(available_packages),0) available_packages," +
"COALESCE(SUM(hold_packages),0) hold_packages," +
@@ -103,6 +129,8 @@ public interface InventoryQueryMapper {
"<if test='q.outboundOrderNo != null and q.outboundOrderNo != \"\"'> AND outbound_order_no LIKE CONCAT('%',#{q.outboundOrderNo},'%')</if>" +
"<if test='q.palletNo != null and q.palletNo != \"\"'> AND pallet_no LIKE CONCAT('%',#{q.palletNo},'%')</if>" +
"<if test='q.batchNo != null and q.batchNo != \"\"'> AND batch_no LIKE CONCAT('%',#{q.batchNo},'%')</if>" +
"<if test='q.skuCode != null and q.skuCode != \"\"'> AND sku_code LIKE CONCAT('%',#{q.skuCode},'%')</if>" +
"<if test='q.warehouseLocationCode != null and q.warehouseLocationCode != \"\"'> AND warehouse_location_code LIKE CONCAT('%',#{q.warehouseLocationCode},'%')</if>" +
"<if test='q.occurredFrom != null'> AND occurred_at &gt;=#{q.occurredFrom}</if>" +
"<if test='q.occurredTo != null'> AND occurred_at &lt;=#{q.occurredTo}</if>";
@@ -0,0 +1,10 @@
package com.baiyue.lastleg.inventory.query.model;
import lombok.Data;
@Data
public class InventoryLocationFact {
private Long balanceId;
private Long warehouseLocationId;
private String warehouseLocationCode;
}
@@ -0,0 +1,10 @@
package com.baiyue.lastleg.inventory.query.model;
import lombok.Data;
@Data
public class InventorySkuFact {
private Long balanceId;
private String skuCode;
private String productName;
}
@@ -11,6 +11,8 @@ import com.baiyue.lastleg.inventory.query.dto.InventoryPositionView;
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
import com.baiyue.lastleg.inventory.query.model.InventoryLocationFact;
import com.baiyue.lastleg.inventory.query.model.InventorySkuFact;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -19,7 +21,9 @@ import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Service
public class InventoryQueryService {
@@ -51,8 +55,8 @@ public class InventoryQueryService {
public PageResult<InventoryBalanceView> balances(String customerId, InventoryQueryRequests.BalanceQuery query) {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
long total = mapper.countBalances(customerId, query);
List<InventoryBalanceView> records = total == 0 ? List.of() : mapper.balances(
customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList();
List<InventoryBalanceView> records = total == 0 ? List.of() : balanceViews(customerId,
mapper.balances(customerId, query, offset(query.page(), query.size()), query.size()));
return page(records, query.page(), query.size(), total);
}
@@ -78,12 +82,14 @@ public class InventoryQueryService {
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
List<String> rows = new ArrayList<>();
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,totalPackages,availablePackages," +
"destinationWarehouseName,inboundOrderId,inboundOrderNo,inboundCode,skuCodes,productNames," +
"warehouseLocationCodes,totalPackages,availablePackages," +
"holdPackages,allocatedPackages,pickedPackages,inTransitPackages,receivingPackages,procurementPackages");
mapper.balances(customerId, query, 0, EXPORT_LIMIT).stream().map(this::balanceView).forEach(v -> rows.add(
balanceViews(customerId, mapper.balances(customerId, query, 0, EXPORT_LIMIT)).forEach(v -> rows.add(
String.join(",", csv(v.customerId()), csv(v.customerName()), csv(v.overseasWarehouseCode()), csv(v.overseasWarehouseName()),
csv(v.destinationWarehouseCode()), csv(v.destinationWarehouseName()), csv(v.inboundOrderId()),
csv(v.inboundOrderNo()), csv(v.inboundCode()),
csv(v.inboundOrderNo()), csv(v.inboundCode()), csv(String.join("|", v.skuCodes())),
csv(String.join("|", v.productNames())), csv(String.join("|", v.warehouseLocationCodes())),
csv(v.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()),
csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()),
csv(v.procurementPackages()))));
@@ -105,7 +111,19 @@ public class InventoryQueryService {
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
}
private InventoryBalanceView balanceView(InventoryBalance value) {
private List<InventoryBalanceView> balanceViews(String customerId, List<InventoryBalance> balances) {
if (balances.isEmpty()) return List.of();
List<Long> ids = balances.stream().map(InventoryBalance::getId).toList();
Map<Long, List<InventorySkuFact>> skus = mapper.skuFacts(customerId, ids).stream()
.collect(Collectors.groupingBy(InventorySkuFact::getBalanceId));
Map<Long, List<InventoryLocationFact>> locations = mapper.locationFacts(customerId, ids).stream()
.collect(Collectors.groupingBy(InventoryLocationFact::getBalanceId));
return balances.stream().map(value -> balanceView(value,
skus.getOrDefault(value.getId(), List.of()), locations.getOrDefault(value.getId(), List.of()))).toList();
}
private InventoryBalanceView balanceView(InventoryBalance value, List<InventorySkuFact> skus,
List<InventoryLocationFact> locations) {
long availablePackages = number(value.getAvailablePackages());
long holdPackages = number(value.getHoldPackages());
long allocatedPackages = number(value.getAllocatedPackages());
@@ -116,7 +134,12 @@ public class InventoryQueryService {
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
value.getInboundOrderNo(), value.getInboundCode(), availablePackages + holdPackages + allocatedPackages + pickedPackages
value.getInboundOrderNo(), value.getInboundCode(),
skus.stream().map(InventorySkuFact::getSkuCode).filter(InventoryQueryService::hasText).distinct().toList(),
skus.stream().map(InventorySkuFact::getProductName).filter(InventoryQueryService::hasText).distinct().toList(),
locations.stream().map(InventoryLocationFact::getWarehouseLocationId).filter(java.util.Objects::nonNull).distinct().toList(),
locations.stream().map(InventoryLocationFact::getWarehouseLocationCode).filter(InventoryQueryService::hasText).distinct().toList(),
availablePackages + holdPackages + allocatedPackages + pickedPackages
+ inTransitPackages + receivingPackages + procurementPackages,
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
receivingPackages, procurementPackages, value.getUpdatedAt());
@@ -141,7 +164,7 @@ public class InventoryQueryService {
return new InventoryLedgerView(value.getId(), value.getCustomerId(), value.getOverseasWarehouseId(),
value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(),
value.getPalletNo(), value.getBatchNo(),
value.getWarehouseLocationCode(), value.getBusinessType(), value.getBusinessOrderNo(),
value.getWarehouseLocationCode(), value.getSkuCode(), value.getBusinessType(), value.getBusinessOrderNo(),
value.getOutboundOrderNo(), value.getStatusFrom(), value.getStatusTo(),
number(value.getBeforePackages()), number(value.getDeltaPackages()), number(value.getAfterPackages()),
value.getOperatorId(), value.getRemark(), value.getOccurredAt());
@@ -168,6 +191,10 @@ public class InventoryQueryService {
return value == null ? 0 : value;
}
private static boolean hasText(String value) {
return value != null && !value.isBlank();
}
private static <T> PageResult<T> page(List<T> records, long page, long size, long total) {
long pages = total == 0 ? 0 : (total + size - 1) / size;
return new PageResult<>(records, page, size, total, pages);