暂存提交

This commit is contained in:
2026-08-19 16:24:02 +08:00
parent f5c4bf130a
commit 6acb6178da
354 changed files with 25000 additions and 198 deletions
@@ -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 {
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
@@ -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);
}
}