提交
This commit is contained in:
+26
@@ -1,12 +1,16 @@
|
|||||||
package com.baiyue.lastleg.inboundplan.controller;
|
package com.baiyue.lastleg.inboundplan.controller;
|
||||||
|
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
|
import com.baiyue.lastleg.inboundplan.dto.ConvertInboundPlanRequest;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
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.SupplementPalletRequest;
|
||||||
|
import com.baiyue.lastleg.inboundplan.dto.UpdateFbaAppointmentRequest;
|
||||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
@@ -26,6 +30,17 @@ public class InboundPlanController {
|
|||||||
@Resource private InboundPlanService service;
|
@Resource private InboundPlanService service;
|
||||||
@Resource private InboundFbaAppointmentService appointmentService;
|
@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")
|
@PostMapping("/convert")
|
||||||
@PreAuthorize("hasAuthority('platform:lastleg:inbound-plan:create')")
|
@PreAuthorize("hasAuthority('platform:lastleg:inbound-plan:create')")
|
||||||
/** 将入仓单转换为入仓计划。 */
|
/** 将入仓单转换为入仓计划。 */
|
||||||
@@ -78,6 +93,17 @@ public class InboundPlanController {
|
|||||||
return ApiResponse.success(appointmentService.detail(scope(user, customerId), appointmentNo));
|
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) {
|
private String scope(AuthenticatedUser user, String requestedCustomer) {
|
||||||
if (user == null || user.tenantId() == null) {
|
if (user == null || user.tenantId() == null) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ public record BatchFbaAppointmentRequest(
|
|||||||
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
|
@NotEmpty @Size(max = 500) List<@NotNull Long> lineIds,
|
||||||
@NotBlank @Size(max = 64) String appointmentNo,
|
@NotBlank @Size(max = 64) String appointmentNo,
|
||||||
@NotNull LocalDateTime appointmentAt,
|
@NotNull LocalDateTime appointmentAt,
|
||||||
|
@NotNull LocalDateTime expectedLoadingAt,
|
||||||
@NotBlank @Size(max = 32) String appointmentStatus,
|
@NotBlank @Size(max = 32) String appointmentStatus,
|
||||||
@Size(max = 1000) String appointmentRemark
|
@Size(max = 1000) String appointmentRemark
|
||||||
) {
|
) {
|
||||||
|
|||||||
+1
@@ -8,6 +8,7 @@ public record FbaAppointmentResponse(
|
|||||||
String customerId,
|
String customerId,
|
||||||
String appointmentNo,
|
String appointmentNo,
|
||||||
LocalDateTime appointmentAt,
|
LocalDateTime appointmentAt,
|
||||||
|
LocalDateTime expectedLoadingAt,
|
||||||
String appointmentStatus,
|
String appointmentStatus,
|
||||||
String appointmentRemark,
|
String appointmentRemark,
|
||||||
String fbaWarehouseCode,
|
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 customerId;
|
||||||
private String appointmentNo;
|
private String appointmentNo;
|
||||||
private LocalDateTime appointmentAt;
|
private LocalDateTime appointmentAt;
|
||||||
|
private LocalDateTime expectedLoadingAt;
|
||||||
private String appointmentStatus;
|
private String appointmentStatus;
|
||||||
private String appointmentRemark;
|
private String appointmentRemark;
|
||||||
private String fbaWarehouseCode;
|
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.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.FbaAppointmentResponse;
|
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.InboundFbaAppointment;
|
||||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
||||||
@@ -44,7 +45,7 @@ public class InboundFbaAppointmentService {
|
|||||||
|
|
||||||
InboundFbaAppointment existing = find(customerId, appointmentNo);
|
InboundFbaAppointment existing = find(customerId, appointmentNo);
|
||||||
if (existing != null) {
|
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);
|
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.
|
// The row locks serialize concurrent submissions selecting the same inbound lines.
|
||||||
existing = find(customerId, appointmentNo);
|
existing = find(customerId, appointmentNo);
|
||||||
if (existing != null) {
|
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();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -66,6 +67,7 @@ public class InboundFbaAppointmentService {
|
|||||||
appointment.setCustomerId(customerId);
|
appointment.setCustomerId(customerId);
|
||||||
appointment.setAppointmentNo(appointmentNo);
|
appointment.setAppointmentNo(appointmentNo);
|
||||||
appointment.setAppointmentAt(request.appointmentAt());
|
appointment.setAppointmentAt(request.appointmentAt());
|
||||||
|
appointment.setExpectedLoadingAt(request.expectedLoadingAt());
|
||||||
appointment.setAppointmentStatus(status);
|
appointment.setAppointmentStatus(status);
|
||||||
appointment.setAppointmentRemark(remark);
|
appointment.setAppointmentRemark(remark);
|
||||||
appointment.setFbaWarehouseCode(fbaWarehouseCode);
|
appointment.setFbaWarehouseCode(fbaWarehouseCode);
|
||||||
@@ -78,7 +80,7 @@ public class InboundFbaAppointmentService {
|
|||||||
} catch (DuplicateKeyException exception) {
|
} catch (DuplicateKeyException exception) {
|
||||||
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
|
InboundFbaAppointment concurrent = find(customerId, appointmentNo);
|
||||||
if (concurrent != null) {
|
if (concurrent != null) {
|
||||||
return verifyReplay(concurrent, lineIds, request.appointmentAt(), status, remark);
|
return verifyReplay(concurrent, lineIds, request.appointmentAt(), request.expectedLoadingAt(), status, remark);
|
||||||
}
|
}
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
@@ -108,12 +110,31 @@ public class InboundFbaAppointmentService {
|
|||||||
return response(appointment, items(appointment.getId()));
|
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,
|
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());
|
List<InboundFbaAppointmentItem> existingItems = items(existing.getId());
|
||||||
Set<Long> existingLineIds = existingItems.stream()
|
Set<Long> existingLineIds = existingItems.stream()
|
||||||
.map(InboundFbaAppointmentItem::getLineId).collect(java.util.stream.Collectors.toSet());
|
.map(InboundFbaAppointmentItem::getLineId).collect(java.util.stream.Collectors.toSet());
|
||||||
if (!Objects.equals(existing.getAppointmentAt(), appointmentAt)
|
if (!Objects.equals(existing.getAppointmentAt(), appointmentAt)
|
||||||
|
|| !Objects.equals(existing.getExpectedLoadingAt(), expectedLoadingAt)
|
||||||
|| !Objects.equals(existing.getAppointmentStatus(), status)
|
|| !Objects.equals(existing.getAppointmentStatus(), status)
|
||||||
|| !Objects.equals(existing.getAppointmentRemark(), remark)
|
|| !Objects.equals(existing.getAppointmentRemark(), remark)
|
||||||
|| existingLineIds.size() != requestedLineIds.size()
|
|| existingLineIds.size() != requestedLineIds.size()
|
||||||
@@ -149,7 +170,8 @@ public class InboundFbaAppointmentService {
|
|||||||
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
|
private FbaAppointmentResponse response(InboundFbaAppointment appointment,
|
||||||
List<InboundFbaAppointmentItem> items) {
|
List<InboundFbaAppointmentItem> items) {
|
||||||
return new FbaAppointmentResponse(appointment.getId(), appointment.getCustomerId(),
|
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.getAppointmentRemark(), appointment.getFbaWarehouseCode(), appointment.getSelectedCount(),
|
||||||
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
|
appointment.getCreatedBy(), appointment.getCreatedAt(), appointment.getUpdatedAt(),
|
||||||
items.stream().map(item -> new FbaAppointmentResponse.Item(item.getId(), item.getPlanId(),
|
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.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
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.InboundPlanResponse;
|
||||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
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.InboundPlanOrderMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletMapper;
|
||||||
|
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
import com.baiyue.lastleg.inboundplan.model.SourceOrderSnapshot;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -35,6 +39,16 @@ public class InboundPlanService {
|
|||||||
@Resource private InboundPlanPalletMapper palletMapper;
|
@Resource private InboundPlanPalletMapper palletMapper;
|
||||||
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
|
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
|
||||||
@Resource private InboundPlanOrderMapper orderMapper;
|
@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
|
@Transactional
|
||||||
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
public InboundPlanResponse convert(String customerId, Long selectedOrderId) {
|
||||||
|
|||||||
+5
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.lastleg.inventory.query.dto;
|
package com.baiyue.lastleg.inventory.query.dto;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public record InventoryBalanceView(
|
public record InventoryBalanceView(
|
||||||
Long id,
|
Long id,
|
||||||
@@ -15,6 +16,10 @@ public record InventoryBalanceView(
|
|||||||
Long inboundOrderId,
|
Long inboundOrderId,
|
||||||
String inboundOrderNo,
|
String inboundOrderNo,
|
||||||
String inboundCode,
|
String inboundCode,
|
||||||
|
List<String> skuCodes,
|
||||||
|
List<String> productNames,
|
||||||
|
List<Long> warehouseLocationIds,
|
||||||
|
List<String> warehouseLocationCodes,
|
||||||
long totalPackages,
|
long totalPackages,
|
||||||
long availablePackages,
|
long availablePackages,
|
||||||
long holdPackages,
|
long holdPackages,
|
||||||
|
|||||||
+1
@@ -13,6 +13,7 @@ public record InventoryLedgerView(
|
|||||||
String palletNo,
|
String palletNo,
|
||||||
String batchNo,
|
String batchNo,
|
||||||
String warehouseLocationCode,
|
String warehouseLocationCode,
|
||||||
|
String skuCode,
|
||||||
String businessType,
|
String businessType,
|
||||||
String businessOrderNo,
|
String businessOrderNo,
|
||||||
String outboundOrderNo,
|
String outboundOrderNo,
|
||||||
|
|||||||
+6
@@ -19,6 +19,10 @@ public final class InventoryQueryRequests {
|
|||||||
String inboundOrderNo,
|
String inboundOrderNo,
|
||||||
String inboundCode,
|
String inboundCode,
|
||||||
String businessOrderNo,
|
String businessOrderNo,
|
||||||
|
String skuCode,
|
||||||
|
String productName,
|
||||||
|
String batchNo,
|
||||||
|
String warehouseLocationCode,
|
||||||
String inventoryStatus
|
String inventoryStatus
|
||||||
) {
|
) {
|
||||||
public BalanceQuery {
|
public BalanceQuery {
|
||||||
@@ -42,6 +46,8 @@ public final class InventoryQueryRequests {
|
|||||||
String outboundOrderNo,
|
String outboundOrderNo,
|
||||||
String palletNo,
|
String palletNo,
|
||||||
String batchNo,
|
String batchNo,
|
||||||
|
String skuCode,
|
||||||
|
String warehouseLocationCode,
|
||||||
LocalDateTime occurredFrom,
|
LocalDateTime occurredFrom,
|
||||||
LocalDateTime occurredTo
|
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.entity.InventoryPosition;
|
||||||
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
|
import com.baiyue.lastleg.inventory.query.dto.InventoryQueryRequests;
|
||||||
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
|
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.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
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.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.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.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>" +
|
"<choose>" +
|
||||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages > 0</when>" +
|
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND available_packages > 0</when>" +
|
||||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND hold_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.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.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.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>" +
|
"<choose>" +
|
||||||
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages > 0</when>" +
|
"<when test='q.inventoryStatus == \"AVAILABLE\"'> AND b.available_packages > 0</when>" +
|
||||||
"<when test='q.inventoryStatus == \"HOLD\"'> AND b.hold_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,
|
long countBalances(@Param("customerId") String customerId,
|
||||||
@Param("q") InventoryQueryRequests.BalanceQuery query);
|
@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," +
|
@Select("<script>SELECT COUNT(*) dimension_count," +
|
||||||
"COALESCE(SUM(available_packages),0) available_packages," +
|
"COALESCE(SUM(available_packages),0) available_packages," +
|
||||||
"COALESCE(SUM(hold_packages),0) hold_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.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.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.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.occurredFrom != null'> AND occurred_at >=#{q.occurredFrom}</if>" +
|
||||||
"<if test='q.occurredTo != null'> AND occurred_at <=#{q.occurredTo}</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.dto.InventoryQueryRequests;
|
||||||
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
||||||
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
|
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 jakarta.annotation.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -19,7 +21,9 @@ import org.springframework.web.server.ResponseStatusException;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class InventoryQueryService {
|
public class InventoryQueryService {
|
||||||
@@ -51,8 +55,8 @@ public class InventoryQueryService {
|
|||||||
public PageResult<InventoryBalanceView> balances(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
public PageResult<InventoryBalanceView> balances(String customerId, InventoryQueryRequests.BalanceQuery query) {
|
||||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||||
long total = mapper.countBalances(customerId, query);
|
long total = mapper.countBalances(customerId, query);
|
||||||
List<InventoryBalanceView> records = total == 0 ? List.of() : mapper.balances(
|
List<InventoryBalanceView> records = total == 0 ? List.of() : balanceViews(customerId,
|
||||||
customerId, query, offset(query.page(), query.size()), query.size()).stream().map(this::balanceView).toList();
|
mapper.balances(customerId, query, offset(query.page(), query.size()), query.size()));
|
||||||
return page(records, query.page(), query.size(), total);
|
return page(records, query.page(), query.size(), total);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,12 +82,14 @@ public class InventoryQueryService {
|
|||||||
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
validateStatus(query.inventoryStatus(), BALANCE_STATUSES);
|
||||||
List<String> rows = new ArrayList<>();
|
List<String> rows = new ArrayList<>();
|
||||||
rows.add("customerId,customerName,overseasWarehouseCode,overseasWarehouseName,destinationWarehouseCode," +
|
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");
|
"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()),
|
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.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.totalPackages()), csv(v.availablePackages()), csv(v.holdPackages()), csv(v.allocatedPackages()),
|
||||||
csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()),
|
csv(v.pickedPackages()), csv(v.inTransitPackages()), csv(v.receivingPackages()),
|
||||||
csv(v.procurementPackages()))));
|
csv(v.procurementPackages()))));
|
||||||
@@ -105,7 +111,19 @@ public class InventoryQueryService {
|
|||||||
return ("\uFEFF" + String.join("\r\n", rows)).getBytes(StandardCharsets.UTF_8);
|
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 availablePackages = number(value.getAvailablePackages());
|
||||||
long holdPackages = number(value.getHoldPackages());
|
long holdPackages = number(value.getHoldPackages());
|
||||||
long allocatedPackages = number(value.getAllocatedPackages());
|
long allocatedPackages = number(value.getAllocatedPackages());
|
||||||
@@ -116,7 +134,12 @@ public class InventoryQueryService {
|
|||||||
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
|
return new InventoryBalanceView(value.getId(), value.getCustomerId(), value.getCustomerName(), value.getOverseasWarehouseId(),
|
||||||
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
|
value.getOverseasWarehouseCode(), value.getOverseasWarehouseName(), value.getDestinationWarehouseId(),
|
||||||
value.getDestinationWarehouseCode(), value.getDestinationWarehouseName(), value.getInboundOrderId(),
|
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,
|
+ inTransitPackages + receivingPackages + procurementPackages,
|
||||||
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
|
availablePackages, holdPackages, allocatedPackages, pickedPackages, inTransitPackages,
|
||||||
receivingPackages, procurementPackages, value.getUpdatedAt());
|
receivingPackages, procurementPackages, value.getUpdatedAt());
|
||||||
@@ -141,7 +164,7 @@ public class InventoryQueryService {
|
|||||||
return new InventoryLedgerView(value.getId(), value.getCustomerId(), value.getOverseasWarehouseId(),
|
return new InventoryLedgerView(value.getId(), value.getCustomerId(), value.getOverseasWarehouseId(),
|
||||||
value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(),
|
value.getDestinationWarehouseCode(), value.getInboundOrderId(), value.getInboundOrderNo(), value.getInboundCode(),
|
||||||
value.getPalletNo(), value.getBatchNo(),
|
value.getPalletNo(), value.getBatchNo(),
|
||||||
value.getWarehouseLocationCode(), value.getBusinessType(), value.getBusinessOrderNo(),
|
value.getWarehouseLocationCode(), value.getSkuCode(), value.getBusinessType(), value.getBusinessOrderNo(),
|
||||||
value.getOutboundOrderNo(), value.getStatusFrom(), value.getStatusTo(),
|
value.getOutboundOrderNo(), value.getStatusFrom(), value.getStatusTo(),
|
||||||
number(value.getBeforePackages()), number(value.getDeltaPackages()), number(value.getAfterPackages()),
|
number(value.getBeforePackages()), number(value.getDeltaPackages()), number(value.getAfterPackages()),
|
||||||
value.getOperatorId(), value.getRemark(), value.getOccurredAt());
|
value.getOperatorId(), value.getRemark(), value.getOccurredAt());
|
||||||
@@ -168,6 +191,10 @@ public class InventoryQueryService {
|
|||||||
return value == null ? 0 : value;
|
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) {
|
private static <T> PageResult<T> page(List<T> records, long page, long size, long total) {
|
||||||
long pages = total == 0 ? 0 : (total + size - 1) / size;
|
long pages = total == 0 ? 0 : (total + size - 1) / size;
|
||||||
return new PageResult<>(records, page, size, total, pages);
|
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',
|
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||||
appointment_no VARCHAR(64) NOT NULL COMMENT '预约单号',
|
appointment_no VARCHAR(64) NOT NULL COMMENT '预约单号',
|
||||||
appointment_at DATETIME(3) 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_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT '预约状态',
|
||||||
appointment_remark VARCHAR(1000) NULL COMMENT '预约备注',
|
appointment_remark VARCHAR(1000) NULL COMMENT '预约备注',
|
||||||
fba_warehouse_code VARCHAR(128) NOT NULL COMMENT 'FBA仓库编码',
|
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, "export", "platform:lastleg:order:export");
|
||||||
assertPermission(LastlegOrderController.class, "transferOne", "platform:lastleg:order:transfer");
|
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, "detail", "platform:lastleg:inbound-plan:list");
|
||||||
assertPermission(InboundPlanController.class, "fbaAppointmentDetail", "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, "convert", "platform:lastleg:inbound-plan:create");
|
||||||
assertPermission(InboundPlanController.class, "batchFbaAppointment", "platform:lastleg:inbound-plan:appointment");
|
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, "page", "platform:lastleg:relabel:list");
|
||||||
assertPermission(RelabelApplicationController.class, "detail", "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 {
|
class InboundPlanSchemaContractTest {
|
||||||
@Test
|
@Test
|
||||||
void planIsUniqueByTenantAndActualContainerNumber() throws IOException {
|
void planIsUniqueByTenantAndSourceOrderWhileContainerMayHaveMultiplePlans() throws IOException {
|
||||||
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
try (var stream = getClass().getClassLoader().getResourceAsStream("db/lastleg.sql")) {
|
||||||
assertTrue(stream != null, "lastleg schema must be available");
|
assertTrue(stream != null, "lastleg schema must be available");
|
||||||
String schema = new String(stream.readAllBytes(), StandardCharsets.UTF_8).toLowerCase();
|
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("default 'pending_assignment'"));
|
||||||
assertTrue(schema.contains("required_package_count"));
|
assertTrue(schema.contains("required_package_count"));
|
||||||
assertTrue(schema.contains("planned_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_pda_operation"));
|
||||||
assertTrue(schema.contains("create table if not exists ll_inbound_pallet_merge"));
|
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("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("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("create table if not exists ll_inbound_fba_appointment_item"));
|
||||||
assertTrue(schema.contains("fba_warehouse_code"));
|
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;
|
package com.baiyue.lastleg.inboundplan.service;
|
||||||
|
|
||||||
import com.baiyue.lastleg.inboundplan.dto.BatchFbaAppointmentRequest;
|
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.InboundFbaAppointment;
|
||||||
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
import com.baiyue.lastleg.inboundplan.entity.InboundFbaAppointmentItem;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentItemMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
|
import com.baiyue.lastleg.inboundplan.mapper.InboundFbaAppointmentQueryMapper;
|
||||||
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
|
import com.baiyue.lastleg.inboundplan.model.FbaAppointmentLineSnapshot;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
@@ -35,6 +37,7 @@ class InboundFbaAppointmentServiceTest {
|
|||||||
|
|
||||||
private InboundFbaAppointmentService service;
|
private InboundFbaAppointmentService service;
|
||||||
private final LocalDateTime appointmentAt = LocalDateTime.of(2026, 8, 20, 10, 30);
|
private final LocalDateTime appointmentAt = LocalDateTime.of(2026, 8, 20, 10, 30);
|
||||||
|
private final LocalDateTime expectedLoadingAt = LocalDateTime.of(2026, 8, 21, 8, 0);
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
@@ -57,6 +60,7 @@ class InboundFbaAppointmentServiceTest {
|
|||||||
|
|
||||||
assertEquals("APT-1", result.appointmentNo());
|
assertEquals("APT-1", result.appointmentNo());
|
||||||
assertEquals("ONT8", result.fbaWarehouseCode());
|
assertEquals("ONT8", result.fbaWarehouseCode());
|
||||||
|
assertEquals(expectedLoadingAt, result.expectedLoadingAt());
|
||||||
assertEquals(2, result.selectedCount());
|
assertEquals(2, result.selectedCount());
|
||||||
verify(appointmentMapper).insert(any(InboundFbaAppointment.class));
|
verify(appointmentMapper).insert(any(InboundFbaAppointment.class));
|
||||||
verify(itemMapper, org.mockito.Mockito.times(2)).insert(any(InboundFbaAppointmentItem.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")));
|
() -> 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) {
|
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) {
|
private FbaAppointmentLineSnapshot line(Long id, String inboundCode, String fba) {
|
||||||
@@ -128,6 +175,7 @@ class InboundFbaAppointmentServiceTest {
|
|||||||
value.setCustomerId("100");
|
value.setCustomerId("100");
|
||||||
value.setAppointmentNo(number);
|
value.setAppointmentNo(number);
|
||||||
value.setAppointmentAt(appointmentAt);
|
value.setAppointmentAt(appointmentAt);
|
||||||
|
value.setExpectedLoadingAt(expectedLoadingAt);
|
||||||
value.setAppointmentStatus("SUCCESS");
|
value.setAppointmentStatus("SUCCESS");
|
||||||
value.setAppointmentRemark("dock");
|
value.setAppointmentRemark("dock");
|
||||||
value.setFbaWarehouseCode(fba);
|
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 InventoryQueryService service = mock(InventoryQueryService.class);
|
||||||
private final InventoryQueryController controller = controller();
|
private final InventoryQueryController controller = controller();
|
||||||
private final InventoryQueryRequests.BalanceQuery query = new InventoryQueryRequests.BalanceQuery(
|
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
|
@Test
|
||||||
void merchantCannotOverrideCustomerScope() {
|
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.dto.InventoryQueryRequests;
|
||||||
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
import com.baiyue.lastleg.inventory.query.mapper.InventoryQueryMapper;
|
||||||
import com.baiyue.lastleg.inventory.query.model.InventoryAggregateProjection;
|
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.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
@@ -58,9 +60,21 @@ class InventoryQueryServiceTest {
|
|||||||
balance.setCustomerId("100");
|
balance.setCustomerId("100");
|
||||||
when(mapper.countBalances("100", query)).thenReturn(1L);
|
when(mapper.countBalances("100", query)).thenReturn(1L);
|
||||||
when(mapper.balances("100", query, 0, 20)).thenReturn(List.of(balance));
|
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).countBalances("100", query);
|
||||||
verify(mapper).balances("100", query, 0, 20);
|
verify(mapper).balances("100", query, 0, 20);
|
||||||
}
|
}
|
||||||
@@ -106,6 +120,7 @@ class InventoryQueryServiceTest {
|
|||||||
void reversedLedgerDateRangeIsRejected() {
|
void reversedLedgerDateRangeIsRejected() {
|
||||||
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
|
InventoryQueryRequests.LedgerQuery query = new InventoryQueryRequests.LedgerQuery(
|
||||||
1, 20, null, null, null, null, null, null, null, null, null,
|
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, 18, 0, 0),
|
||||||
java.time.LocalDateTime.of(2026, 8, 17, 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);
|
String csv = new String(service.exportBalances("100", query), StandardCharsets.UTF_8);
|
||||||
|
|
||||||
assertTrue(csv.contains("inboundOrderNo,inboundCode,totalPackages"));
|
assertTrue(csv.contains("inboundOrderNo,inboundCode,skuCodes"));
|
||||||
assertFalse(csv.contains("skuCode"));
|
|
||||||
assertFalse(csv.contains("Quantity"));
|
assertFalse(csv.contains("Quantity"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static InventoryQueryRequests.BalanceQuery balanceQuery(String status) {
|
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;
|
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'));
|
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;
|
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