暂存提交
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.dropship;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.baiyue.lastleg.dropship.mapper")
|
||||
public class DropshipMapperConfiguration {
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.dropship.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.service.DropshipOrderService;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/v1/lastleg/dropship")
|
||||
public class DropshipOrderController {
|
||||
@Resource private DropshipOrderService service;
|
||||
@Resource private InternalServiceTokenGuard tokenGuard;
|
||||
|
||||
@PostMapping("/receive")
|
||||
public ApiResponse<Integer> receive(@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody DropshipRequests.Receive request) {
|
||||
tokenGuard.verify(token);
|
||||
return ApiResponse.success(service.receive(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.dropship.dto;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.List;
|
||||
|
||||
public final class DropshipRequests {
|
||||
private DropshipRequests() {}
|
||||
public record Receive(@NotBlank String customerId, @NotNull Long orderId, @NotNull Long warehouseId,
|
||||
@NotBlank String idempotencyKey, @NotEmpty List<@Valid Pallet> pallets) {}
|
||||
public record Pallet(@NotBlank String palletNo, @NotEmpty List<@Valid SkuPackages> skuPackages) {}
|
||||
public record SkuPackages(@NotBlank String skuCode, String productName, @Positive long packageCount,
|
||||
@Positive long productQuantity,
|
||||
@NotEmpty List<@NotBlank String> cartonMarks) {}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_carton")
|
||||
public class DropshipCarton {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long profileId;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
private String skuCode;
|
||||
private String cartonMark;
|
||||
private Integer cartonSequence;
|
||||
private String status;
|
||||
private String outboundOrderNo;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_carton_ledger")
|
||||
public class DropshipCartonLedger {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long cartonId;
|
||||
private String cartonMark;
|
||||
private String skuCode;
|
||||
private String operationType;
|
||||
private Long beforePackages;
|
||||
private Long deltaPackages;
|
||||
private Long afterPackages;
|
||||
private String businessOrderNo;
|
||||
private String idempotencyKey;
|
||||
private String remark;
|
||||
private LocalDateTime occurredAt;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_order_profile")
|
||||
public class DropshipOrderProfile {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long warehousePalletId;
|
||||
private String palletNo;
|
||||
/** Immutable pallet number snapshot supplied by the first-leg handover event. */
|
||||
private String sourcePalletNo;
|
||||
private Long inventoryPositionId;
|
||||
private String status;
|
||||
private String idempotencyKey;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.dropship.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data @TableName("ll_dropship_sku_inventory")
|
||||
public class DropshipSkuInventory {
|
||||
@TableId(type = IdType.ASSIGN_ID) private Long id;
|
||||
private String customerId;
|
||||
private Long orderId;
|
||||
private Long profileId;
|
||||
private String skuCode;
|
||||
private String productName;
|
||||
private Long packageCount;
|
||||
private Long productQuantity;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
public interface DropshipCartonLedgerMapper extends BaseMapper<DropshipCartonLedger> {
|
||||
@Select("SELECT * FROM ll_dropship_carton_ledger WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
DropshipCartonLedger findIdempotency(@Param("customerId") String customerId, @Param("key") String key);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
public interface DropshipCartonMapper extends BaseMapper<DropshipCarton> {
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} AND carton_mark=#{cartonMark} LIMIT 1")
|
||||
DropshipCarton find(@Param("customerId") String customerId, @Param("orderId") Long orderId, @Param("cartonMark") String cartonMark);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND profile_id=#{profileId} ORDER BY carton_sequence")
|
||||
List<DropshipCarton> listProfile(@Param("customerId") String customerId, @Param("profileId") Long profileId);
|
||||
|
||||
@Select("SELECT * FROM ll_dropship_carton WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY profile_id,carton_sequence")
|
||||
List<DropshipCarton> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
public interface DropshipOrderProfileMapper extends BaseMapper<DropshipOrderProfile> {
|
||||
@Select("SELECT * FROM ll_dropship_order_profile WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY source_pallet_no")
|
||||
List<DropshipOrderProfile> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.baiyue.lastleg.dropship.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface DropshipSkuInventoryMapper extends BaseMapper<DropshipSkuInventory> {
|
||||
@Select("SELECT * FROM ll_dropship_sku_inventory WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY profile_id,sku_code")
|
||||
List<DropshipSkuInventory> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
package com.baiyue.lastleg.dropship.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.lastleg.dropship.dto.DropshipRequests;
|
||||
import com.baiyue.lastleg.dropship.entity.*;
|
||||
import com.baiyue.lastleg.dropship.mapper.*;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.inboundplan.pda.entity.InboundStockReceipt;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class DropshipOrderService {
|
||||
@Resource private LastlegOrderMapper orders;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private DropshipSkuInventoryMapper skuInventory;
|
||||
@Resource private DropshipCartonMapper cartons;
|
||||
@Resource private DropshipCartonLedgerMapper ledger;
|
||||
|
||||
@Transactional
|
||||
public int receive(DropshipRequests.Receive request) {
|
||||
LastlegOrder order = orders.selectById(request.orderId());
|
||||
if (order == null || !request.customerId().equals(order.getCustomerId())) throw conflict("Tail order not found");
|
||||
if (!"ONE_CLICK_DROPSHIP".equals(order.getOrderType())) throw conflict("Order is not a one-click dropship order");
|
||||
long totalPackages = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::packageCount).sum();
|
||||
long totalQuantity = request.pallets().stream().flatMap(value -> value.skuPackages().stream())
|
||||
.mapToLong(DropshipRequests.SkuPackages::productQuantity).sum();
|
||||
if (order.getPackageCount() != null && order.getPackageCount().longValue() != totalPackages) {
|
||||
throw conflict("SKU package total does not equal order package count");
|
||||
}
|
||||
if (order.getProductQuantity() != null && order.getProductQuantity().longValue() != totalQuantity) {
|
||||
throw conflict("SKU product quantity total does not equal order product quantity");
|
||||
}
|
||||
List<DropshipOrderProfile> existing = profiles.listOrder(request.customerId(), request.orderId());
|
||||
if (!existing.isEmpty()) {
|
||||
Set<String> existingPallets = existing.stream().map(DropshipOrderProfile::getSourcePalletNo)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
Set<String> requestedPallets = request.pallets().stream().map(value -> value.palletNo().trim())
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (!existingPallets.equals(requestedPallets)) throw conflict("Dropship order pallet snapshot conflicts with existing data");
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
validateMarks(request);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (DropshipRequests.Pallet inputPallet : request.pallets()) {
|
||||
DropshipOrderProfile profile = profile(request, order, inputPallet, now);
|
||||
profiles.insert(profile);
|
||||
int sequence = 0;
|
||||
for (DropshipRequests.SkuPackages input : inputPallet.skuPackages()) {
|
||||
DropshipSkuInventory inventory = new DropshipSkuInventory();
|
||||
inventory.setId(IdWorker.getId()); inventory.setCustomerId(request.customerId()); inventory.setOrderId(order.getId());
|
||||
inventory.setProfileId(profile.getId()); inventory.setSkuCode(input.skuCode()); inventory.setProductName(input.productName());
|
||||
inventory.setPackageCount(input.packageCount()); inventory.setProductQuantity(input.productQuantity());
|
||||
inventory.setCreatedAt(now); inventory.setUpdatedAt(now);
|
||||
skuInventory.insert(inventory);
|
||||
for (String mark : input.cartonMarks()) {
|
||||
DropshipCarton carton = new DropshipCarton();
|
||||
carton.setId(IdWorker.getId()); carton.setCustomerId(request.customerId()); carton.setOrderId(order.getId());
|
||||
carton.setProfileId(profile.getId()); carton.setWarehousePalletId(null); carton.setPalletNo(null);
|
||||
carton.setSkuCode(input.skuCode()); carton.setCartonMark(mark.trim()); carton.setCartonSequence(++sequence);
|
||||
carton.setStatus("PENDING_INBOUND"); carton.setVersion(0L); carton.setCreatedAt(now); carton.setUpdatedAt(now);
|
||||
cartons.insert(carton);
|
||||
ledger.insert(stockInFact(request, order, carton, now));
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.toIntExact(totalPackages);
|
||||
}
|
||||
|
||||
private void validateMarks(DropshipRequests.Receive request) {
|
||||
Set<String> marks = new HashSet<>();
|
||||
Set<String> palletNos = new HashSet<>();
|
||||
for (DropshipRequests.Pallet pallet : request.pallets()) {
|
||||
if (!palletNos.add(pallet.palletNo().trim())) throw conflict("Duplicate pallet number: " + pallet.palletNo());
|
||||
Set<String> skus = new HashSet<>();
|
||||
for (DropshipRequests.SkuPackages item : pallet.skuPackages()) {
|
||||
if (item.packageCount() != item.cartonMarks().size()) {
|
||||
throw conflict("SKU packageCount must equal cartonMarks count: " + item.skuCode());
|
||||
}
|
||||
if (!skus.add(item.skuCode().trim())) throw conflict("Duplicate SKU on pallet: " + item.skuCode());
|
||||
for (String mark : item.cartonMarks()) {
|
||||
if (!marks.add(mark.trim())) throw conflict("Duplicate carton mark: " + mark);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DropshipOrderProfile profile(DropshipRequests.Receive request, LastlegOrder order,
|
||||
DropshipRequests.Pallet input, LocalDateTime now) {
|
||||
DropshipOrderProfile profile = new DropshipOrderProfile();
|
||||
profile.setId(IdWorker.getId()); profile.setCustomerId(request.customerId()); profile.setOrderId(order.getId());
|
||||
profile.setSourcePalletNo(input.palletNo().trim());
|
||||
profile.setStatus("PENDING_INBOUND"); profile.setVersion(0L);
|
||||
profile.setIdempotencyKey(request.idempotencyKey() + ":" + input.palletNo().trim());
|
||||
profile.setCreatedAt(now); profile.setUpdatedAt(now);
|
||||
return profile;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void activateAfterStockIn(String customerId, Long orderId, Long taskId,
|
||||
List<InboundStockReceipt> receipts) {
|
||||
List<DropshipOrderProfile> profilesForOrder = profiles.listOrder(customerId, orderId);
|
||||
if (profilesForOrder.isEmpty()) return;
|
||||
if (profilesForOrder.stream().allMatch(profile -> "ACTIVE".equals(profile.getStatus()))) return;
|
||||
if (profilesForOrder.stream().anyMatch(profile -> "ACTIVE".equals(profile.getStatus()))) {
|
||||
throw conflict("Dropship pallet activation is incomplete; repair profiles before retrying");
|
||||
}
|
||||
Map<Long, List<InboundStockReceipt>> receiptsByPallet = receipts.stream()
|
||||
.filter(receipt -> receipt.getWarehousePalletId() != null)
|
||||
.collect(Collectors.groupingBy(InboundStockReceipt::getWarehousePalletId));
|
||||
if (receiptsByPallet.size() != profilesForOrder.size()) {
|
||||
throw conflict("PDA pallet count does not match dropship source pallet count");
|
||||
}
|
||||
List<DropshipOrderProfile> orderedProfiles = profilesForOrder.stream()
|
||||
.sorted(java.util.Comparator.comparing(DropshipOrderProfile::getSourcePalletNo)).toList();
|
||||
List<List<InboundStockReceipt>> orderedReceipts = receiptsByPallet.values().stream()
|
||||
.sorted(java.util.Comparator.comparing(value -> value.get(0).getWarehousePalletId())).toList();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (int index = 0; index < orderedProfiles.size(); index++) {
|
||||
DropshipOrderProfile profile = orderedProfiles.get(index);
|
||||
List<InboundStockReceipt> actual = orderedReceipts.get(index);
|
||||
List<DropshipCarton> profileCartons = cartons.listProfile(customerId, profile.getId());
|
||||
long expected = profileCartons.size();
|
||||
long received = actual.stream().mapToLong(value -> value.getReceivedPackages() == null ? 0 : value.getReceivedPackages()).sum();
|
||||
if (expected != received) throw conflict("PDA package count does not match dropship carton snapshot");
|
||||
Long positionId = actual.get(0).getInventoryPositionId();
|
||||
if (actual.stream().anyMatch(value -> !java.util.Objects.equals(positionId, value.getInventoryPositionId()))) {
|
||||
throw conflict("Dropship source pallet maps to multiple inventory positions");
|
||||
}
|
||||
InboundStockReceipt first = actual.get(0);
|
||||
profile.setWarehousePalletId(first.getWarehousePalletId()); profile.setPalletNo(first.getPalletNo());
|
||||
profile.setInventoryPositionId(positionId); profile.setStatus("ACTIVE"); profile.setVersion(value(profile.getVersion()) + 1);
|
||||
profile.setUpdatedAt(now); profiles.updateById(profile);
|
||||
for (DropshipCarton carton : profileCartons) {
|
||||
carton.setWarehousePalletId(first.getWarehousePalletId()); carton.setPalletNo(first.getPalletNo());
|
||||
carton.setStatus("AVAILABLE"); carton.setVersion(value(carton.getVersion()) + 1); carton.setUpdatedAt(now);
|
||||
cartons.updateById(carton);
|
||||
DropshipCartonLedger activation = stockInActivation(customerId, orderId, taskId, carton, now);
|
||||
ledger.insert(activation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private DropshipCartonLedger stockInActivation(String customerId, Long orderId, Long taskId,
|
||||
DropshipCarton carton, LocalDateTime now) {
|
||||
DropshipCartonLedger fact = new DropshipCartonLedger();
|
||||
fact.setId(IdWorker.getId()); fact.setCustomerId(customerId); fact.setOrderId(orderId);
|
||||
fact.setCartonId(carton.getId()); fact.setCartonMark(carton.getCartonMark()); fact.setSkuCode(carton.getSkuCode());
|
||||
fact.setOperationType("STOCK_IN"); fact.setBeforePackages(0L); fact.setDeltaPackages(1L); fact.setAfterPackages(1L);
|
||||
fact.setIdempotencyKey("PDA_STOCK_IN:" + taskId + ":" + carton.getId()); fact.setOccurredAt(now); fact.setCreatedAt(now);
|
||||
return fact;
|
||||
}
|
||||
|
||||
private DropshipCartonLedger stockInFact(DropshipRequests.Receive request, LastlegOrder order,
|
||||
DropshipCarton carton, LocalDateTime now) {
|
||||
DropshipCartonLedger fact = new DropshipCartonLedger();
|
||||
fact.setId(IdWorker.getId()); fact.setCustomerId(request.customerId()); fact.setOrderId(order.getId());
|
||||
fact.setCartonId(carton.getId()); fact.setCartonMark(carton.getCartonMark()); fact.setSkuCode(carton.getSkuCode());
|
||||
fact.setOperationType("SNAPSHOT_CREATED"); fact.setBeforePackages(0L); fact.setDeltaPackages(0L); fact.setAfterPackages(0L);
|
||||
fact.setBusinessOrderNo(order.getOrderNo());
|
||||
fact.setIdempotencyKey(request.idempotencyKey() + ":" + carton.getCartonMark());
|
||||
fact.setOccurredAt(now); fact.setCreatedAt(now);
|
||||
return fact;
|
||||
}
|
||||
|
||||
private static long value(Long value) { return value == null ? 0L : value; }
|
||||
|
||||
private static ResponseStatusException conflict(String message) {
|
||||
return new ResponseStatusException(HttpStatus.CONFLICT, message);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user