提交
This commit is contained in:
+26
@@ -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);
|
||||
|
||||
+1
@@ -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
|
||||
) {
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ public record FbaAppointmentResponse(
|
||||
String customerId,
|
||||
String appointmentNo,
|
||||
LocalDateTime appointmentAt,
|
||||
LocalDateTime expectedLoadingAt,
|
||||
String appointmentStatus,
|
||||
String appointmentRemark,
|
||||
String fbaWarehouseCode,
|
||||
|
||||
+30
@@ -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
|
||||
) {
|
||||
}
|
||||
+31
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -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
|
||||
) {
|
||||
}
|
||||
+1
@@ -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;
|
||||
|
||||
+53
@@ -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 >=#{q.createdFrom}</if>" +
|
||||
"<if test='q.createdTo != null'> AND p.created_at <=#{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);
|
||||
}
|
||||
+27
-5
@@ -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(),
|
||||
|
||||
+14
@@ -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) {
|
||||
|
||||
+5
@@ -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,
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ public record InventoryLedgerView(
|
||||
String palletNo,
|
||||
String batchNo,
|
||||
String warehouseLocationCode,
|
||||
String skuCode,
|
||||
String businessType,
|
||||
String businessOrderNo,
|
||||
String outboundOrderNo,
|
||||
|
||||
+6
@@ -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
|
||||
) {
|
||||
|
||||
+28
@@ -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 > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_packages > 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 > 0</when>" +
|
||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_packages > 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<>'' 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 >=#{q.occurredFrom}</if>" +
|
||||
"<if test='q.occurredTo != null'> AND occurred_at <=#{q.occurredTo}</if>";
|
||||
|
||||
|
||||
+10
@@ -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;
|
||||
}
|
||||
+10
@@ -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;
|
||||
}
|
||||
+35
-8
@@ -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);
|
||||
|
||||
@@ -553,6 +553,7 @@ CREATE TABLE IF NOT EXISTS ll_inbound_fba_appointment (
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
appointment_no VARCHAR(64) NOT NULL COMMENT '预约单号',
|
||||
appointment_at DATETIME(3) NOT NULL COMMENT '预约时间',
|
||||
expected_loading_at DATETIME(3) NOT NULL COMMENT '预计装车时间',
|
||||
appointment_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '预约状态',
|
||||
appointment_remark VARCHAR(1000) NULL COMMENT '预约备注',
|
||||
fba_warehouse_code VARCHAR(128) NOT NULL COMMENT 'FBA仓库编码',
|
||||
|
||||
+2
@@ -23,10 +23,12 @@ class LastlegPlatformPermissionContractTest {
|
||||
assertPermission(LastlegOrderController.class, "export", "platform:lastleg:order:export");
|
||||
assertPermission(LastlegOrderController.class, "transferOne", "platform:lastleg:order:transfer");
|
||||
|
||||
assertPermission(InboundPlanController.class, "page", "platform:lastleg:inbound-plan:list");
|
||||
assertPermission(InboundPlanController.class, "detail", "platform:lastleg:inbound-plan:list");
|
||||
assertPermission(InboundPlanController.class, "fbaAppointmentDetail", "platform:lastleg:inbound-plan:list");
|
||||
assertPermission(InboundPlanController.class, "convert", "platform:lastleg:inbound-plan:create");
|
||||
assertPermission(InboundPlanController.class, "batchFbaAppointment", "platform:lastleg:inbound-plan:appointment");
|
||||
assertPermission(InboundPlanController.class, "updateFbaAppointment", "platform:lastleg:inbound-plan:appointment");
|
||||
|
||||
assertPermission(RelabelApplicationController.class, "page", "platform:lastleg:relabel:list");
|
||||
assertPermission(RelabelApplicationController.class, "detail", "platform:lastleg:relabel:list");
|
||||
|
||||
+4
-2
@@ -10,11 +10,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class InboundPlanSchemaContractTest {
|
||||
@Test
|
||||
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
|
||||
void planIsUniqueByTenantAndSourceOrderWhileContainerMayHaveMultiplePlans() throws IOException {
|
||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||
assertTrue(stream != null, "lastleg schema must be available");
|
||||
String schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertTrue(schema.contains("unique key uk_ll_inbound_plan_source_order (customer_id, source_order_id)"));
|
||||
assertTrue(schema.contains("key idx_ll_inbound_plan_container (customer_id, container_no)"));
|
||||
assertTrue(schema.contains("default 'pending_assignment'"));
|
||||
assertTrue(schema.contains("required_package_count"));
|
||||
assertTrue(schema.contains("planned_package_count"));
|
||||
@@ -23,6 +24,7 @@ class InboundPlanSchemaContractTest {
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pda_operation"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_pallet_merge"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment"));
|
||||
assertTrue(schema.contains("expected_loading_at datetime(3) not null comment '预计装车时间'"));
|
||||
assertTrue(schema.contains("unique key uk_ll_fba_appointment_no (customer_id, appointment_no)"));
|
||||
assertTrue(schema.contains("create table if not exists ll_inbound_fba_appointment_item"));
|
||||
assertTrue(schema.contains("fba_warehouse_code"));
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.lastleg.inboundplan.controller;
|
||||
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundPlanControllerTest {
|
||||
@Mock InboundPlanService service;
|
||||
@Mock InboundFbaAppointmentService appointmentService;
|
||||
private InboundPlanController controller;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
controller = new InboundPlanController();
|
||||
ReflectionTestUtils.setField(controller, "service", service);
|
||||
ReflectionTestUtils.setField(controller, "appointmentService", appointmentService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformPageRequiresAndPassesExplicitCustomerId() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(1, 20, null, null, null,
|
||||
null, null, null, null, null, null);
|
||||
when(service.page("100", query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||
|
||||
controller.page(platform(), "100", query);
|
||||
|
||||
verify(service).page("100", query);
|
||||
assertThatThrownBy(() -> controller.page(platform(), "", query))
|
||||
.isInstanceOf(ResponseStatusException.class)
|
||||
.satisfies(error -> org.assertj.core.api.Assertions.assertThat(
|
||||
((ResponseStatusException) error).getStatusCode().value()).isEqualTo(400));
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformAppointmentUpdateUsesExplicitCustomerScope() {
|
||||
UpdateFbaAppointmentRequest request = new UpdateFbaAppointmentRequest("100",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 30), LocalDateTime.of(2026, 8, 21, 8, 0),
|
||||
"PENDING", "dock");
|
||||
|
||||
controller.updateFbaAppointment(platform(), "APT-1", request);
|
||||
|
||||
verify(appointmentService).update("100", "APT-1", request);
|
||||
}
|
||||
|
||||
private static AuthenticatedUser platform() {
|
||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||
Set.of("platform:lastleg:inbound-plan:list"));
|
||||
}
|
||||
}
|
||||
+49
-1
@@ -1,12 +1,14 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||
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;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
|
||||
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -35,6 +37,7 @@ class InboundFbaAppointmentServiceTest {
|
||||
|
||||
private InboundFbaAppointmentService service;
|
||||
private final LocalDateTime appointmentAt = LocalDateTime.of(2026, 8, 20, 10, 30);
|
||||
private final LocalDateTime expectedLoadingAt = LocalDateTime.of(2026, 8, 21, 8, 0);
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
@@ -57,6 +60,7 @@ class InboundFbaAppointmentServiceTest {
|
||||
|
||||
assertEquals("APT-1", result.appointmentNo());
|
||||
assertEquals("ONT8", result.fbaWarehouseCode());
|
||||
assertEquals(expectedLoadingAt, result.expectedLoadingAt());
|
||||
assertEquals(2, result.selectedCount());
|
||||
verify(appointmentMapper).insert(any(InboundFbaAppointment.class));
|
||||
verify(itemMapper, org.mockito.Mockito.times(2)).insert(any(InboundFbaAppointmentItem.class));
|
||||
@@ -114,8 +118,51 @@ class InboundFbaAppointmentServiceTest {
|
||||
() -> service.batchCreate("100", "7", request(List.of(12L), "APT-1")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameAppointmentNumberWithDifferentExpectedLoadingTimeConflicts() {
|
||||
when(appointmentMapper.selectOne(any())).thenReturn(appointment(99L, "APT-1", "ONT8"));
|
||||
InboundFbaAppointmentItem item = new InboundFbaAppointmentItem();
|
||||
item.setLineId(11L);
|
||||
when(itemMapper.selectList(any())).thenReturn(List.of(item));
|
||||
BatchFbaAppointmentRequest changed = new BatchFbaAppointmentRequest("100", List.of(11L), "APT-1",
|
||||
appointmentAt, expectedLoadingAt.plusHours(1), "SUCCESS", "dock");
|
||||
|
||||
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||
() -> service.batchCreate("100", "7", changed));
|
||||
assertEquals(409, exception.getStatusCode().value());
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringSnowflakeLineIdsDeserializeWithoutPrecisionLoss() throws Exception {
|
||||
String json = "{\"customerId\":\"100\",\"lineIds\":[\"9007199254740993\"],"
|
||||
+ "\"appointmentNo\":\"APT-1\",\"appointmentAt\":\"2026-08-20T10:30:00\","
|
||||
+ "\"expectedLoadingAt\":\"2026-08-21T08:00:00\",\"appointmentStatus\":\"PENDING\"}";
|
||||
|
||||
BatchFbaAppointmentRequest request = new ObjectMapper().findAndRegisterModules()
|
||||
.readValue(json, BatchFbaAppointmentRequest.class);
|
||||
|
||||
assertEquals(List.of(9007199254740993L), request.lineIds());
|
||||
assertEquals(expectedLoadingAt, request.expectedLoadingAt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void updatesAppointmentDetailsWithoutReplacingSelectedLines() {
|
||||
InboundFbaAppointment existing = appointment(99L, "APT-1", "ONT8");
|
||||
when(appointmentMapper.selectOne(any())).thenReturn(existing);
|
||||
LocalDateTime loadingAt = expectedLoadingAt.plusHours(2);
|
||||
|
||||
var result = service.update("100", "APT-1", new UpdateFbaAppointmentRequest(
|
||||
"100", appointmentAt.plusDays(1), loadingAt, "PENDING", "updated"));
|
||||
|
||||
assertEquals(loadingAt, result.expectedLoadingAt());
|
||||
assertEquals("PENDING", result.appointmentStatus());
|
||||
verify(appointmentMapper).updateById(existing);
|
||||
verify(itemMapper, never()).insert(any(InboundFbaAppointmentItem.class));
|
||||
}
|
||||
|
||||
private BatchFbaAppointmentRequest request(List<Long> lineIds, String number) {
|
||||
return new BatchFbaAppointmentRequest("100", lineIds, number, appointmentAt, "SUCCESS", "dock");
|
||||
return new BatchFbaAppointmentRequest("100", lineIds, number, appointmentAt, expectedLoadingAt,
|
||||
"SUCCESS", "dock");
|
||||
}
|
||||
|
||||
private FbaAppointmentLineSnapshot line(Long id, String inboundCode, String fba) {
|
||||
@@ -128,6 +175,7 @@ class InboundFbaAppointmentServiceTest {
|
||||
value.setCustomerId("100");
|
||||
value.setAppointmentNo(number);
|
||||
value.setAppointmentAt(appointmentAt);
|
||||
value.setExpectedLoadingAt(expectedLoadingAt);
|
||||
value.setAppointmentStatus("SUCCESS");
|
||||
value.setAppointmentRemark("dock");
|
||||
value.setFbaWarehouseCode(fba);
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.lastleg.inboundplan.service;
|
||||
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanListResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class InboundPlanPaginationTest {
|
||||
@Mock InboundPlanQueryMapper queryMapper;
|
||||
private InboundPlanService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
service = new InboundPlanService();
|
||||
ReflectionTestUtils.setField(service, "queryMapper", queryMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryDefaultsPageAndSizeAndRejectsOversizedPages() {
|
||||
InboundPlanRequests.Query defaults = query(0, 0);
|
||||
assertThat(defaults.page()).isEqualTo(1);
|
||||
assertThat(defaults.size()).isEqualTo(20);
|
||||
|
||||
try (var validatorFactory = Validation.buildDefaultValidatorFactory()) {
|
||||
var violations = validatorFactory.getValidator().validate(query(1, 201));
|
||||
assertThat(violations).extracting(value -> value.getPropertyPath().toString()).contains("size");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void pagesAtLineGranularityAndPassesTenantAndAllFiltersToDatabase() {
|
||||
InboundPlanRequests.Query query = new InboundPlanRequests.Query(2, 10, "LIP", "IN-1",
|
||||
"PENDING_ASSIGNMENT", "ONT", "ONT8", "UNAPPOINTED", "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 1, 0, 0), LocalDateTime.of(2026, 8, 31, 23, 59));
|
||||
InboundPlanListResponse row = new InboundPlanListResponse(9007199254740993L, 2L, "LIP2", "100",
|
||||
3L, "IN-1", "FIRST_LEG", "W1", "ONT", "安大略仓", "ONT8", "客户A", 10,
|
||||
BigDecimal.ONE, 1, "PENDING_ASSIGNMENT", null, null, null, null, "UNSCHEDULED",
|
||||
LocalDateTime.of(2026, 8, 20, 10, 0));
|
||||
when(queryMapper.count("100", query)).thenReturn(11L);
|
||||
when(queryMapper.rows("100", query, 10, 10)).thenReturn(List.of(row));
|
||||
|
||||
var result = service.page("100", query);
|
||||
|
||||
assertThat(result.records()).containsExactly(row);
|
||||
assertThat(result.records().get(0).lineId()).isEqualTo(9007199254740993L);
|
||||
assertThat(result.records().get(0).appointmentStatus()).isNull();
|
||||
assertThat(result.pages()).isEqualTo(2);
|
||||
verify(queryMapper).count("100", query);
|
||||
verify(queryMapper).rows("100", query, 10, 10);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapperSqlProjectsLatestAppointmentAndTenantScopedDispatchState() throws Exception {
|
||||
String sql = String.join("", InboundPlanQueryMapper.class
|
||||
.getMethod("rows", String.class, InboundPlanRequests.Query.class, long.class, long.class)
|
||||
.getAnnotation(org.apache.ibatis.annotations.Select.class).value()) + InboundPlanQueryMapper.FROM_AND_FILTER;
|
||||
assertThat(sql).contains("a2.customer_id=#{customerId}", "ORDER BY a2.created_at DESC,ai2.id DESC LIMIT 1");
|
||||
assertThat(sql).contains("di.customer_id=#{customerId} AND di.order_id=l.order_id");
|
||||
assertThat(sql).contains("q.planNo", "q.inboundCode", "q.status", "q.warehouse",
|
||||
"q.fbaWarehouseCode", "q.appointmentStatus", "q.dispatchStatus", "q.createdFrom", "q.createdTo");
|
||||
assertThat(sql).contains("LIMIT #{limit} OFFSET #{offset}");
|
||||
}
|
||||
|
||||
private static InboundPlanRequests.Query query(long page, long size) {
|
||||
return new InboundPlanRequests.Query(page, size, null, null, null, null, null,
|
||||
null, null, null, null);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@ class InventoryQueryControllerTest {
|
||||
private final InventoryQueryService service = mock(InventoryQueryService.class);
|
||||
private final InventoryQueryController controller = controller();
|
||||
private final InventoryQueryRequests.BalanceQuery query = new InventoryQueryRequests.BalanceQuery(
|
||||
1, 20, null, null, null, null, null, null, null, null);
|
||||
1, 20, null, null, null, null, null, null, null, null, null, null, null, null);
|
||||
|
||||
@Test
|
||||
void merchantCannotOverrideCustomerScope() {
|
||||
|
||||
+19
-4
@@ -6,6 +6,8 @@ import com.baiyue.lastleg.inventory.query.dto.InventoryDashboardResponse;
|
||||
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 org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
@@ -58,9 +60,21 @@ class InventoryQueryServiceTest {
|
||||
balance.setCustomerId("100");
|
||||
when(mapper.countBalances("100", query)).thenReturn(1L);
|
||||
when(mapper.balances("100", query, 0, 20)).thenReturn(List.of(balance));
|
||||
InventorySkuFact sku = new InventorySkuFact();
|
||||
sku.setBalanceId(1L);
|
||||
sku.setSkuCode("SKU-1");
|
||||
sku.setProductName("测试产品");
|
||||
InventoryLocationFact location = new InventoryLocationFact();
|
||||
location.setBalanceId(1L);
|
||||
location.setWarehouseLocationCode(null);
|
||||
when(mapper.skuFacts("100", List.of(1L))).thenReturn(List.of(sku));
|
||||
when(mapper.locationFacts("100", List.of(1L))).thenReturn(List.of(location));
|
||||
|
||||
service.balances("100", query);
|
||||
var result = service.balances("100", query);
|
||||
|
||||
assertEquals(List.of("SKU-1"), result.records().get(0).skuCodes());
|
||||
assertEquals(List.of("测试产品"), result.records().get(0).productNames());
|
||||
assertEquals(List.of(), result.records().get(0).warehouseLocationCodes());
|
||||
verify(mapper).countBalances("100", query);
|
||||
verify(mapper).balances("100", query, 0, 20);
|
||||
}
|
||||
@@ -106,6 +120,7 @@ class InventoryQueryServiceTest {
|
||||
void reversedLedgerDateRangeIsRejected() {
|
||||
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
|
||||
1, 20, null, null, null, null, null, null, null, null, null,
|
||||
null, null,
|
||||
java.time.LocalDateTime.of(2026, 8, 18, 0, 0),
|
||||
java.time.LocalDateTime.of(2026, 8, 17, 0, 0));
|
||||
|
||||
@@ -120,12 +135,12 @@ class InventoryQueryServiceTest {
|
||||
|
||||
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
|
||||
|
||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,totalPackages"));
|
||||
assertFalse(csv.contains("skuCode"));
|
||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,skuCodes"));
|
||||
assertFalse(csv.contains("Quantity"));
|
||||
}
|
||||
|
||||
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null, status);
|
||||
return new InventoryQueryRequests.BalanceQuery(1, 20, null, null, null, null, null, null, null,
|
||||
null, null, null, null, status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3644,3 +3644,13 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
||||
PREPARE lastleg_unload_relay_029 FROM @ddl; EXECUTE lastleg_unload_relay_029; DEALLOCATE PREPARE lastleg_unload_relay_029;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_event_outbox' AND column_name='locked_at'), 'SELECT 1', 'ALTER TABLE ll_event_outbox ADD COLUMN locked_at DATETIME(3) NULL COMMENT ''当前投递租约获取时间'' AFTER locked_by'));
|
||||
PREPARE lastleg_unload_relay_029 FROM @ddl; EXECUTE lastleg_unload_relay_029; DEALLOCATE PREPARE lastleg_unload_relay_029;
|
||||
|
||||
-- ============================================================================
|
||||
-- 2026-08-20 / 030
|
||||
-- FBA预约预计装车时间。
|
||||
-- ============================================================================
|
||||
USE baiyue_lastleg;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_inbound_fba_appointment' AND column_name='expected_loading_at'), 'SELECT 1', 'ALTER TABLE ll_inbound_fba_appointment ADD COLUMN expected_loading_at DATETIME(3) NULL COMMENT ''预计装车时间'' AFTER appointment_at'));
|
||||
PREPARE lastleg_fba_appointment_030 FROM @ddl; EXECUTE lastleg_fba_appointment_030; DEALLOCATE PREPARE lastleg_fba_appointment_030;
|
||||
UPDATE ll_inbound_fba_appointment SET expected_loading_at=appointment_at WHERE expected_loading_at IS NULL;
|
||||
ALTER TABLE ll_inbound_fba_appointment MODIFY COLUMN expected_loading_at DATETIME(3) NOT NULL COMMENT '预计装车时间';
|
||||
|
||||
Reference in New Issue
Block a user