优化
This commit is contained in:
+25
-5
@@ -1,20 +1,25 @@
|
||||
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.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundFbaAppointmentService;
|
||||
import com.baiyue.lastleg.inboundplan.service.InboundPlanService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@@ -25,15 +30,27 @@ public class InboundPlanController {
|
||||
@Resource private InboundPlanService service;
|
||||
@Resource private InboundFbaAppointmentService appointmentService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||
@Operation(summary = "Page inbound plans without PDA, internal-event or outbound workflow fields", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||
public ApiResponse<PageResult<InboundPlanSummary>> page(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId,
|
||||
@Valid InboundPlanRequests.Query query) {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@PostMapping("/convert")
|
||||
@Operation(summary = "Convert one inbound order to one inbound plan")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_CREATE + "')")
|
||||
@Operation(summary = "Convert one inbound order to one inbound plan", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_CREATE)
|
||||
public ApiResponse<InboundPlanResponse> convert(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody ConvertInboundPlanRequest request) {
|
||||
return ApiResponse.success(service.convert(scope(user, request.customerId()), request.orderId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{planNo}")
|
||||
@Operation(summary = "Get an inbound plan with lines, pallets and allocations")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||
@Operation(summary = "Get an inbound plan with lines, pallets and allocations", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||
public ApiResponse<InboundPlanResponse> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@RequestParam(required = false) String customerId) {
|
||||
@@ -41,7 +58,8 @@ public class InboundPlanController {
|
||||
}
|
||||
|
||||
@PostMapping("/{planNo}/pallets/supplement")
|
||||
@Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Add one printable supplemental pallet for a plan destination warehouse", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
|
||||
public ApiResponse<InboundPlanResponse.Pallet> supplementPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String planNo,
|
||||
@Valid @RequestBody SupplementPalletRequest request) {
|
||||
@@ -49,7 +67,8 @@ public class InboundPlanController {
|
||||
}
|
||||
|
||||
@PostMapping("/fba-appointments/batch")
|
||||
@Operation(summary = "Create one FBA appointment for selected inbound lines")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||
@Operation(summary = "Create one FBA appointment for selected inbound lines", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
|
||||
public ApiResponse<FbaAppointmentResponse> batchFbaAppointment(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody BatchFbaAppointmentRequest request) {
|
||||
@@ -59,7 +78,8 @@ public class InboundPlanController {
|
||||
}
|
||||
|
||||
@GetMapping("/fba-appointments/{appointmentNo}")
|
||||
@Operation(summary = "Get an FBA appointment and its selected inbound lines")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||
@Operation(summary = "Get an FBA appointment and its selected inbound lines", description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||
public ApiResponse<FbaAppointmentResponse> fbaAppointmentDetail(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String appointmentNo,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
|
||||
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 overseasWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String fbaWarehouseCode,
|
||||
String appointmentStatus,
|
||||
LocalDateTime createdFrom,
|
||||
LocalDateTime createdTo
|
||||
) {
|
||||
public Query {
|
||||
if (page == 0) page = 1;
|
||||
if (size == 0) size = 20;
|
||||
}
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package com.baiyue.lastleg.inboundplan.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Schema(description = "Inbound plan row for the platform browser API; outbound workflow fields are intentionally excluded")
|
||||
public record InboundPlanSummary(
|
||||
Long id,
|
||||
String planNo,
|
||||
Long sourceOrderId,
|
||||
String customerId,
|
||||
String customerName,
|
||||
String orderType,
|
||||
String inboundCode,
|
||||
String containerNo,
|
||||
String overseasWarehouseId,
|
||||
String overseasWarehouseName,
|
||||
String destinationWarehouseId,
|
||||
String destinationWarehouseCode,
|
||||
String destinationWarehouseName,
|
||||
String fbaWarehouseCode,
|
||||
Integer packageCount,
|
||||
BigDecimal totalVolumeCbm,
|
||||
Integer palletCount,
|
||||
String status,
|
||||
String appointmentStatus,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) {
|
||||
}
|
||||
+37
@@ -1,13 +1,50 @@
|
||||
package com.baiyue.lastleg.inboundplan.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
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 InboundPlanMapper extends BaseMapper<InboundPlan> {
|
||||
String PAGE_FROM = " FROM ll_inbound_plan p JOIN ll_order o ON o.id=p.source_order_id AND o.customer_id=p.customer_id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.plan_id=p.id AND l.order_id=p.source_order_id " +
|
||||
"WHERE p.customer_id=#{customerId}" +
|
||||
"<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.overseasWarehouseId != null and q.overseasWarehouseId != \"\"'> AND o.overseas_warehouse_id=#{q.overseasWarehouseId}</if>" +
|
||||
"<if test='q.destinationWarehouseCode != null and q.destinationWarehouseCode != \"\"'> AND l.destination_warehouse_code=#{q.destinationWarehouseCode}</if>" +
|
||||
"<if test='q.fbaWarehouseCode != null and q.fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{q.fbaWarehouseCode}</if>" +
|
||||
"<if test='q.appointmentStatus != null and q.appointmentStatus != \"\" and q.appointmentStatus != \"NOT_APPOINTED\"'> " +
|
||||
"AND EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id " +
|
||||
"WHERE ai.plan_id=p.id AND a.customer_id=#{customerId} AND a.appointment_status=#{q.appointmentStatus})</if>" +
|
||||
"<if test='q.appointmentStatus == \"NOT_APPOINTED\"'> AND NOT EXISTS (SELECT 1 FROM ll_inbound_fba_appointment_item ai " +
|
||||
"WHERE ai.plan_id=p.id)</if>" +
|
||||
"<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 p.id,p.plan_no,p.source_order_id,p.customer_id,o.customer_name,o.order_type," +
|
||||
"l.inbound_code,p.container_no,o.overseas_warehouse_id,o.overseas_warehouse_name," +
|
||||
"l.destination_warehouse_id,l.destination_warehouse_code,l.destination_warehouse_name,l.fba_warehouse_code," +
|
||||
"COALESCE(l.required_package_count,o.package_count,0) package_count,p.total_volume_cbm,p.pallet_count,p.status," +
|
||||
"COALESCE((SELECT a.appointment_status FROM ll_inbound_fba_appointment_item ai " +
|
||||
"JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id WHERE ai.plan_id=p.id " +
|
||||
"ORDER BY a.created_at DESC,a.id DESC LIMIT 1),'NOT_APPOINTED') appointment_status,p.created_at,p.updated_at" +
|
||||
PAGE_FROM + " ORDER BY p.created_at DESC,p.id DESC LIMIT #{limit} OFFSET #{offset}</script>")
|
||||
List<InboundPlanSummary> pageRows(@Param("customerId") String customerId,
|
||||
@Param("q") InboundPlanRequests.Query query,
|
||||
@Param("offset") long offset,
|
||||
@Param("limit") long limit);
|
||||
|
||||
@Select("<script>SELECT COUNT(*)" + PAGE_FROM + "</script>")
|
||||
long countRows(@Param("customerId") String customerId, @Param("q") InboundPlanRequests.Query query);
|
||||
|
||||
@Select("SELECT * FROM ll_inbound_plan WHERE customer_id=#{customerId} AND source_order_id=#{sourceOrderId} LIMIT 1")
|
||||
InboundPlan findBySourceOrder(@Param("customerId") String customerId, @Param("sourceOrderId") Long sourceOrderId);
|
||||
}
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/inbound-plans/{planNo}/pallets")
|
||||
@RequestMapping("/api/pda/v1/last-leg/inbound-plans/{planNo}/pallets")
|
||||
public class InboundPalletAssignmentController {
|
||||
@Resource private InboundPdaWorkflowService service;
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/inbound-plans/{planNo}/pda")
|
||||
@RequestMapping("/api/pda/v1/last-leg/inbound-plans/{planNo}")
|
||||
public class InboundPdaController {
|
||||
public static final String OPERATE_PERMISSION = "lastleg:pda:operate";
|
||||
@Resource private InboundPdaWorkflowService service;
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ public class InboundStockReceipt {
|
||||
private Long lineId;
|
||||
private Long orderId;
|
||||
private String inboundCode;
|
||||
private String skuCode;
|
||||
private String productNameZh;
|
||||
private String productNameEn;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
private Long warehouseLocationId;
|
||||
|
||||
+85
-5
@@ -2,6 +2,7 @@ package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanPallet;
|
||||
@@ -13,6 +14,7 @@ import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandResponse;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderDetailMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
@@ -34,6 +36,7 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class InboundStockReceiptService {
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private LastlegOrderDetailMapper orderDetails;
|
||||
@Resource private InboundStockReceiptMapper receipts;
|
||||
@Resource private InventoryCommandService inventory;
|
||||
@Resource private DropshipOrderService dropshipOrders;
|
||||
@@ -79,9 +82,22 @@ public class InboundStockReceiptService {
|
||||
if (pickedPackages != value(line.getRequiredPackageCount())) {
|
||||
throw conflict("PDA picked packages must equal the inbound order package total");
|
||||
}
|
||||
List<SkuRemainder> skuRemainders = "ONE_CLICK_DROPSHIP".equals(order.getOrderType())
|
||||
? List.of() : skuRemainders(order.getId(), pickedPackages);
|
||||
for (InboundPlanPalletAllocation allocation : lineAllocations) {
|
||||
created.add(persist(customerId, plan, task, line, allocation,
|
||||
requirePallet(palletById, allocation), order, receivedAt));
|
||||
InboundPlanPallet pallet = requirePallet(palletById, allocation);
|
||||
if ("ONE_CLICK_DROPSHIP".equals(order.getOrderType())) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order,
|
||||
SkuSnapshot.unspecified(value(allocation.getPickedPackageCount())), receivedAt));
|
||||
} else {
|
||||
long allocationPackages = value(allocation.getPickedPackageCount());
|
||||
for (SkuSnapshot sku : consume(skuRemainders, allocationPackages)) {
|
||||
created.add(persist(customerId, plan, task, line, allocation, pallet, order, sku, receivedAt));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (skuRemainders.stream().anyMatch(value -> value.remaining > 0)) {
|
||||
throw conflict("Inbound SKU package snapshots were not fully received");
|
||||
}
|
||||
}
|
||||
activateDropship(customerId, task.getId(), orderById, created);
|
||||
@@ -112,20 +128,21 @@ public class InboundStockReceiptService {
|
||||
|
||||
private InboundStockReceipt persist(String customerId, InboundPlan plan, InboundPdaTask task, InboundPlanLine line,
|
||||
InboundPlanPalletAllocation allocation, InboundPlanPallet pallet,
|
||||
LastlegOrder order, LocalDateTime receivedAt) {
|
||||
long packages = value(allocation.getPickedPackageCount());
|
||||
LastlegOrder order, SkuSnapshot sku, LocalDateTime receivedAt) {
|
||||
long packages = sku.packages();
|
||||
if (packages <= 0 || pallet.getWarehousePalletId() == null) {
|
||||
throw conflict("Stock-in requires picked packages and an assigned warehouse pallet");
|
||||
}
|
||||
String destinationCode = required(first(line.getDestinationWarehouseCode(), order.getFbaWarehouseCode()),
|
||||
"Destination warehouse code is required for stock-in");
|
||||
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId();
|
||||
String operationKey = "PDA_STOCK_IN:" + task.getId() + ":" + allocation.getId() + ":" + sku.skuCode();
|
||||
String overseasWarehouseId = required(order.getOverseasWarehouseId(),
|
||||
"Actual overseas warehouse is required for stock-in");
|
||||
InventoryCommandResponse result = inventory.stockIn(customerId, task.getOperatorId(),
|
||||
new InventoryCommandRequests.StockIn(null, order.getCustomerName(), overseasWarehouseId,
|
||||
null, order.getOverseasWarehouseName(), line.getDestinationWarehouseId(), destinationCode,
|
||||
line.getDestinationWarehouseName(), order.getId(), plan.getPlanNo(), line.getInboundCode(),
|
||||
sku.skuCode(), sku.productNameZh(), sku.productNameEn(),
|
||||
pallet.getWarehousePalletId(), pallet.getPalletNo(), plan.getPlanNo(),
|
||||
pallet.getWarehouseLocationId(), null, packages, order.getOrderNo(),
|
||||
order.getSourceEventId(), operationKey,
|
||||
@@ -140,6 +157,9 @@ public class InboundStockReceiptService {
|
||||
receipt.setLineId(line.getId());
|
||||
receipt.setOrderId(order.getId());
|
||||
receipt.setInboundCode(line.getInboundCode());
|
||||
receipt.setSkuCode(sku.skuCode());
|
||||
receipt.setProductNameZh(sku.productNameZh());
|
||||
receipt.setProductNameEn(sku.productNameEn());
|
||||
receipt.setWarehousePalletId(pallet.getWarehousePalletId());
|
||||
receipt.setPalletNo(pallet.getPalletNo());
|
||||
receipt.setWarehouseLocationId(pallet.getWarehouseLocationId());
|
||||
@@ -154,6 +174,46 @@ public class InboundStockReceiptService {
|
||||
return receipt;
|
||||
}
|
||||
|
||||
private List<SkuRemainder> skuRemainders(Long orderId, long expectedPackages) {
|
||||
List<LastlegOrderDetail> details = orderDetails.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<LastlegOrderDetail>()
|
||||
.eq(LastlegOrderDetail::getOrderId, orderId).orderByAsc(LastlegOrderDetail::getId));
|
||||
Map<String, SkuRemainder> grouped = new java.util.LinkedHashMap<>();
|
||||
for (LastlegOrderDetail detail : details) {
|
||||
String skuCode = trim(detail.getSkuCode());
|
||||
long packages = value(detail.getPackageCount());
|
||||
if (skuCode == null || packages <= 0) continue;
|
||||
SkuRemainder existing = grouped.get(skuCode);
|
||||
if (existing == null) {
|
||||
grouped.put(skuCode, new SkuRemainder(skuCode, trim(detail.getProductNameZh()),
|
||||
trim(detail.getProductNameEn()), packages));
|
||||
} else {
|
||||
existing.remaining += packages;
|
||||
}
|
||||
}
|
||||
long actualPackages = grouped.values().stream().mapToLong(value -> value.remaining).sum();
|
||||
if (grouped.isEmpty() || actualPackages != expectedPackages) {
|
||||
throw conflict("Inbound SKU package snapshots must exactly match the received package total");
|
||||
}
|
||||
return new java.util.ArrayList<>(grouped.values());
|
||||
}
|
||||
|
||||
private static List<SkuSnapshot> consume(List<SkuRemainder> remainders, long packages) {
|
||||
List<SkuSnapshot> result = new java.util.ArrayList<>();
|
||||
long pending = packages;
|
||||
for (SkuRemainder remainder : remainders) {
|
||||
if (pending == 0) break;
|
||||
if (remainder.remaining == 0) continue;
|
||||
long accepted = Math.min(pending, remainder.remaining);
|
||||
result.add(new SkuSnapshot(remainder.skuCode, remainder.productNameZh,
|
||||
remainder.productNameEn, accepted));
|
||||
remainder.remaining -= accepted;
|
||||
pending -= accepted;
|
||||
}
|
||||
if (pending != 0) throw conflict("PDA received packages exceed the frozen SKU package snapshots");
|
||||
return result;
|
||||
}
|
||||
|
||||
private void activateDropship(String customerId, Long taskId, Map<Long, LastlegOrder> orderById,
|
||||
List<InboundStockReceipt> stockReceipts) {
|
||||
Map<Long, List<InboundStockReceipt>> receiptsByOrder = stockReceipts.stream()
|
||||
@@ -190,4 +250,24 @@ public class InboundStockReceiptService {
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
|
||||
private static final class SkuRemainder {
|
||||
private final String skuCode;
|
||||
private final String productNameZh;
|
||||
private final String productNameEn;
|
||||
private long remaining;
|
||||
|
||||
private SkuRemainder(String skuCode, String productNameZh, String productNameEn, long remaining) {
|
||||
this.skuCode = skuCode;
|
||||
this.productNameZh = productNameZh;
|
||||
this.productNameEn = productNameEn;
|
||||
this.remaining = remaining;
|
||||
}
|
||||
}
|
||||
|
||||
private record SkuSnapshot(String skuCode, String productNameZh, String productNameEn, long packages) {
|
||||
private static SkuSnapshot unspecified(long packages) {
|
||||
return new SkuSnapshot("UNSPECIFIED", null, null, packages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.lastleg.inboundplan.pda.service;
|
||||
|
||||
import com.baiyue.common.event.EventEnvelope;
|
||||
import com.baiyue.common.event.EventEnvelopeValidator;
|
||||
import com.baiyue.lastleg.client.EventBridgeClient;
|
||||
import com.baiyue.lastleg.entity.LastlegEventOutbox;
|
||||
import com.baiyue.lastleg.repository.UnloadCompletedOutboxRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
/** 至少一次投递海外卸柜完成事件;国内事件桥按 eventId 幂等接收。 */
|
||||
@Component
|
||||
@Slf4j
|
||||
public class InboundUnloadCompletedOutboxRelay {
|
||||
@Resource private UnloadCompletedOutboxRepository repository;
|
||||
@Resource private EventBridgeClient client;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
@Value("${baiyue.security.internal-service-token}") private String internalToken;
|
||||
@Value("${baiyue.firstleg.outbox-relay.lease-seconds:60}") private long leaseSeconds;
|
||||
@Value("${baiyue.firstleg.outbox-relay.retry-seconds:30}") private long retrySeconds;
|
||||
private final String workerId = UUID.randomUUID().toString();
|
||||
|
||||
@Scheduled(fixedDelayString = "${baiyue.firstleg.outbox-relay.fixed-delay-ms:5000}")
|
||||
public void relay() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
LocalDateTime expiredLeaseBefore = now.minusSeconds(Math.max(leaseSeconds, 10));
|
||||
for (LastlegEventOutbox candidate : repository.pending(50, now, expiredLeaseBefore)) {
|
||||
if (!repository.claim(candidate.getId(), workerId, now, expiredLeaseBefore)) continue;
|
||||
LastlegEventOutbox row = repository.claimed(candidate.getId(), workerId);
|
||||
if (row == null) continue;
|
||||
try {
|
||||
EventEnvelope event = objectMapper.readValue(row.getPayloadJson(), EventEnvelope.class);
|
||||
EventEnvelopeValidator.requireValid(event);
|
||||
var response = client.receive(internalToken, event);
|
||||
if (response == null || !response.successful() || response.data() == null
|
||||
|| !event.eventId().equals(response.data().eventId())) {
|
||||
throw new IllegalStateException("国内事件桥未确认海外卸柜完成事件");
|
||||
}
|
||||
repository.published(row.getId(), workerId, LocalDateTime.now());
|
||||
} catch (Exception exception) {
|
||||
repository.retry(row.getId(), workerId, LocalDateTime.now().plusSeconds(Math.max(retrySeconds, 1)),
|
||||
exception.getMessage());
|
||||
log.warn("海外卸柜完成事件投递失败: eventKey={}, reason={}", row.getEventKey(), exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+16
@@ -2,7 +2,10 @@ 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.InboundPlanRequests;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanResponse;
|
||||
import com.baiyue.lastleg.inboundplan.dto.InboundPlanSummary;
|
||||
import com.baiyue.lastleg.inboundplan.dto.SupplementPalletRequest;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlan;
|
||||
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
|
||||
@@ -36,6 +39,19 @@ public class InboundPlanService {
|
||||
@Resource private InboundPlanPalletAllocationMapper allocationMapper;
|
||||
@Resource private InboundPlanOrderMapper orderMapper;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<InboundPlanSummary> page(String customerId, InboundPlanRequests.Query query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null
|
||||
&& query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "createdFrom must not be after createdTo");
|
||||
}
|
||||
long total = planMapper.countRows(customerId, query);
|
||||
List<InboundPlanSummary> records = total == 0 ? List.of()
|
||||
: planMapper.pageRows(customerId, query, Math.multiplyExact(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) {
|
||||
List<SourceOrderSnapshot> orders = orderMapper.lockOrder(customerId, selectedOrderId);
|
||||
|
||||
Reference in New Issue
Block a user