贴标申请,仓储转运

This commit is contained in:
2026-09-28 11:59:08 +08:00
parent 0e62b2f45b
commit 2bb98d2047
14 changed files with 278 additions and 38 deletions
@@ -19,7 +19,7 @@ public final class CargoDiscardRequests {
@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
public record Item(@NotNull Long orderId, @NotBlank @Size(max = 128) String skuCode,
public record Item(Long orderId, Long returnCartonId, @NotBlank @Size(max = 128) String skuCode,
@Positive int discardPackages) {}
public record Review(@NotBlank String decision, @Size(max = 1000) String remark) {}
@@ -7,7 +7,8 @@ import java.util.List;
public final class CargoDiscardResponses {
private CargoDiscardResponses() {}
public record Candidate(Long orderId, String inboundCode, String inboundOrderNo,
public record Candidate(Long orderId, String sourceType, Long returnCartonId, String cartonMark,
String inboundCode, String inboundOrderNo,
String overseasWarehouseId, String overseasWarehouseName,
LocalDateTime inboundCreatedAt, long availablePackages,
List<CandidateSku> skus) {}
@@ -14,6 +14,8 @@ public class CargoDiscardItem {
private Long applicationId;
private String customerId;
private Long orderId;
private String sourceType;
private Long returnCartonId;
private String inboundCode;
private Long profileId;
private Long skuInventoryId;
@@ -10,6 +10,8 @@ import com.baiyue.lastleg.discard.service.CargoDiscardService;
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -27,6 +29,7 @@ public class PdaCargoDiscardService {
@Resource private DropshipOrderProfileMapper profiles;
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private CargoDiscardService discardService;
@Resource private ReturnInventoryCartonMapper returnCartons;
public List<PdaCargoDiscardDtos.PalletTask> tasks(PdaWarehouseIdentity identity) {
return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>()
@@ -69,6 +72,9 @@ public class PdaCargoDiscardService {
item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED");
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item);
}
task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).map(items::selectById)
.filter(this::isReturnItem).map(CargoDiscardItem::getReturnCartonId).filter(Objects::nonNull).distinct()
.forEach(cartonId -> adjustments.finishReturnCarton(application.getCustomerId(), cartonId));
if (items.listApplication(application.getCustomerId(), application.getId()).stream()
.allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
int actualTotal = items.listApplication(application.getCustomerId(), application.getId()).stream()
@@ -83,17 +89,31 @@ public class PdaCargoDiscardService {
private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) {
return items.listApplication(application.getCustomerId(), application.getId()).stream()
.collect(Collectors.groupingBy(CargoDiscardItem::getProfileId, LinkedHashMap::new, Collectors.toList()))
.collect(Collectors.groupingBy(this::physicalGroupKey, LinkedHashMap::new, Collectors.toList()))
.entrySet().stream().map(entry -> {
DropshipOrderProfile profile = profiles.selectById(entry.getKey());
if (profile == null || !application.getCustomerId().equals(profile.getCustomerId())) throw conflict("废弃明细关联托盘不存在");
CargoDiscardItem first = entry.getValue().get(0);
boolean returnSource = isReturnItem(first);
DropshipOrderProfile profile = returnSource ? null : profiles.selectById(first.getProfileId());
ReturnInventoryCarton returnCarton = returnSource ? returnCartons.selectById(first.getReturnCartonId()) : null;
if (!returnSource && (profile == null || !application.getCustomerId().equals(profile.getCustomerId()))) throw conflict("废弃明细关联托盘不存在");
if (returnSource && (returnCarton == null || !application.getCustomerId().equals(returnCarton.getCustomerId()))) throw conflict("废弃明细关联退货箱不存在");
List<PdaCargoDiscardDtos.Sku> skus = entry.getValue().stream().map(i -> new PdaCargoDiscardDtos.Sku(
i.getId(), i.getSkuCode(), i.getProductName(), i.getDiscardPackages(), i.getActualDiscardPackages(),
i.getPdaStatus() == null ? "PENDING" : i.getPdaStatus())).toList();
String status = skus.stream().allMatch(s -> "COMPLETED".equals(s.status())) ? "COMPLETED" : "PENDING";
return new PdaCargoDiscardDtos.PalletTask(application.getDiscardNo(), application.getCustomerId(),
application.getOverseasWarehouseId(), profile.getPalletNo(), profile.getId(), status, skus);
application.getOverseasWarehouseId(), returnSource ? firstNonBlank(returnCarton.getPalletCode(), returnCarton.getCartonMark()) : profile.getPalletNo(),
returnSource ? returnCarton.getId() : profile.getId(), status, skus);
}).toList();
}
private String physicalGroupKey(CargoDiscardItem item) {
return isReturnItem(item) ? "RETURN:" + item.getReturnCartonId() : "DROPSHIP:" + item.getProfileId();
}
private boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
private static String firstNonBlank(String first, String second) {
return first != null && !first.isBlank() ? first : second;
}
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
}
@@ -15,6 +15,9 @@ import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipSkuInventoryMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -37,11 +40,14 @@ public class CargoDiscardInventoryAdjustmentService {
@Resource private DropshipOrderProfileMapper profiles;
@Resource private InventoryPositionMapper positions;
@Resource private InventoryBalanceMapper balances;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
@Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
CargoDiscardItem item, List<CargoDiscardCarton> links,
int actualPackages, String operatorId, LocalDateTime occurredAt) {
if (isReturnItem(item)) return discardReturnItem(customerId, item, actualPackages);
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
@@ -88,6 +94,36 @@ public class CargoDiscardInventoryAdjustmentService {
return new AdjustmentResult(before, actualPackages, after);
}
private AdjustmentResult discardReturnItem(String customerId, CargoDiscardItem item, int actualPackages) {
if (actualPackages < 0 || actualPackages > item.getDiscardPackages()) {
throw conflict("实际废弃数量超出退货库存申请数量");
}
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(item.getReturnCartonId(), item.getSkuCode());
if (sku == null || !customerId.equals(sku.getCustomerId())) throw conflict("退货库存 SKU 不存在");
long before = value(sku.getQuantity());
if (before < actualPackages || (actualPackages > 0 && returnCartonItems.deduct(sku.getId(), actualPackages) != 1)) {
throw conflict("退货库存数量不足");
}
return new AdjustmentResult(before, actualPackages, before - actualPackages);
}
@Transactional(propagation = Propagation.MANDATORY)
public void finishReturnCarton(String customerId, long cartonId) {
var carton = returnCartons.lockId(cartonId);
if (carton == null || !customerId.equals(carton.getCustomerId())) throw conflict("退货库存箱不存在");
if ("AVAILABLE".equals(carton.getStatus()) || "DISCARDED".equals(carton.getStatus())) return;
long remaining = returnCartonItems.totalQuantity(cartonId);
int skuTypes = returnCartonItems.skuTypes(cartonId);
String status = remaining == 0 ? "DISCARDED" : "AVAILABLE";
if (returnCartons.finishDiscard(cartonId, customerId, status, remaining, skuTypes) != 1) {
throw conflict("退货库存箱状态已变化");
}
}
private static boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
@Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
List<CargoDiscardItem> items,
@@ -22,6 +22,10 @@ import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonItemMapper;
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
@@ -64,6 +68,8 @@ public class CargoDiscardService {
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
@Resource private ReturnInventoryCartonMapper returnCartons;
@Resource private ReturnInventoryCartonItemMapper returnCartonItems;
@Resource private ObjectMapper objectMapper;
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
private BigDecimal feePerPackage = new BigDecimal("5.00");
@@ -75,9 +81,25 @@ public class CargoDiscardService {
CandidateAccumulator value = grouped.computeIfAbsent(row.getOrderId(), ignored -> new CandidateAccumulator(row));
value.add(row);
}
return grouped.values().stream()
List<CargoDiscardResponses.Candidate> result = grouped.values().stream()
.map(value -> value.response(inventory.countAvailableOrder(customerId, value.first.getOrderId())))
.toList();
.collect(java.util.stream.Collectors.toCollection(java.util.ArrayList::new));
String keyword = trim(inboundCode);
if (returnCartons == null || returnCartonItems == null) return result;
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, null)) {
if (StringUtils.hasText(keyword) && !containsIgnoreCase(carton.getSourceReturnNo(), keyword)
&& !containsIgnoreCase(carton.getCartonMark(), keyword)) continue;
List<CargoDiscardResponses.CandidateSku> skus = returnCartonItems.listCarton(carton.getId()).stream()
.filter(item -> value(item.getQuantity()) > 0)
.map(item -> new CargoDiscardResponses.CandidateSku(item.getSkuCode(), item.getProductNameZh(), value(item.getQuantity())))
.toList();
long available = skus.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
if (available <= 0) continue;
result.add(new CargoDiscardResponses.Candidate(null, "RETURN_CARTON", carton.getId(), carton.getCartonMark(),
carton.getSourceReturnNo(), carton.getSourceReturnNo(), carton.getOverseasWarehouseId(), null,
null, available, skus));
}
return result;
}
@Transactional
@@ -94,18 +116,39 @@ public class CargoDiscardService {
}
Set<String> requestedDimensions = new LinkedHashSet<>();
Map<Long, LastlegOrder> orderCache = new HashMap<>();
Map<Long, ReturnInventoryCarton> returnCartonCache = new HashMap<>();
String warehouseId = null;
String warehouseName = null;
int total = 0;
for (CargoDiscardRequests.Item input : request.items()) {
String dimension = input.orderId() + ":" + input.skuCode().trim();
boolean returnSource = input.returnCartonId() != null;
if (returnSource == (input.orderId() != null)) {
throw badRequest("Each discard item must identify exactly one inventory source");
}
String dimension = (returnSource ? "RETURN:" + input.returnCartonId() : "ORDER:" + input.orderId())
+ ":" + input.skuCode().trim();
if (!requestedDimensions.add(dimension)) throw badRequest("Duplicate order/SKU discard item: " + dimension);
LastlegOrder order = requireDropshipOrder(customerId, input.orderId());
orderCache.put(order.getId(), order);
String itemWarehouseId;
String itemWarehouseName;
if (returnSource) {
ReturnInventoryCarton carton = requireReturnCarton(customerId, input.returnCartonId());
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(carton.getId(), input.skuCode().trim());
if (sku == null || value(sku.getQuantity()) < input.discardPackages()) {
throw conflict("Insufficient return inventory for SKU " + input.skuCode().trim());
}
returnCartonCache.put(carton.getId(), carton);
itemWarehouseId = carton.getOverseasWarehouseId();
itemWarehouseName = null;
} else {
LastlegOrder order = requireDropshipOrder(customerId, input.orderId());
orderCache.put(order.getId(), order);
itemWarehouseId = order.getOverseasWarehouseId();
itemWarehouseName = order.getOverseasWarehouseName();
}
if (warehouseId == null) {
warehouseId = required(order.getOverseasWarehouseId(), "Order has no operating overseas warehouse");
warehouseName = trim(order.getOverseasWarehouseName());
} else if (!warehouseId.equals(order.getOverseasWarehouseId())) {
warehouseId = required(itemWarehouseId, "Inventory has no operating overseas warehouse");
warehouseName = trim(itemWarehouseName);
} else if (!warehouseId.equals(itemWarehouseId)) {
throw conflict("One cargo discard application cannot span operating warehouses");
}
total = Math.addExact(total, input.discardPackages());
@@ -116,7 +159,9 @@ public class CargoDiscardService {
application.setId(IdWorker.getId()); application.setCustomerId(customerId);
application.setDiscardNo(orderNoGenerator.next("DISCARD", warehouseId, application.getId()));
application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName);
application.setInboundCodes(orderCache.values().stream().map(LastlegOrder::getInboundCode)
application.setInboundCodes(java.util.stream.Stream.concat(
orderCache.values().stream().map(LastlegOrder::getInboundCode),
returnCartonCache.values().stream().map(ReturnInventoryCarton::getSourceReturnNo))
.distinct().sorted().collect(java.util.stream.Collectors.joining(",")));
application.setTotalPackages(total); application.setFeePerPackage(feePerPackage);
application.setTotalFee(feePerPackage.multiply(BigDecimal.valueOf(total)));
@@ -157,8 +202,14 @@ public class CargoDiscardService {
initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now);
orderHistories.insert(initialOrderHistory);
Set<Long> reservedReturnCartons = new LinkedHashSet<>();
for (CargoDiscardRequests.Item input : request.items()) {
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
if (input.returnCartonId() != null) {
reserveReturn(customerId, application, returnCartonCache.get(input.returnCartonId()), input, now,
reservedReturnCartons);
} else {
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
}
}
history(customerId, application.getId(), null, PENDING_REVIEW, operatorId, request.remark(), now);
return detail(customerId, application.getDiscardNo());
@@ -215,7 +266,7 @@ public class CargoDiscardService {
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
String historyRemark = request.remark();
if (REJECTED.equals(next)) {
release(customerId, links);
release(customerId, links, applicationItems);
}
LocalDateTime reviewedAt = now();
@@ -275,7 +326,7 @@ public class CargoDiscardService {
throw conflict("客户只能取消待审核的废弃申请");
}
List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId());
release(customerId, links);
release(customerId, links, items.listApplication(customerId, application.getId()));
LocalDateTime at = now();
String from = application.getStatus();
application.setStatus(CANCELLED); application.setReviewedBy(platform ? operatorId : application.getReviewedBy());
@@ -309,6 +360,7 @@ public class CargoDiscardService {
if (sku == null) throw conflict("SKU inventory disappeared while reserving cargo");
CargoDiscardItem item = new CargoDiscardItem();
item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId);
item.setSourceType("DROPSHIP");
item.setOrderId(order.getId()); item.setInboundCode(order.getInboundCode()); item.setProfileId(entry.getKey());
item.setSkuInventoryId(sku.getId()); item.setSkuCode(skuCode); item.setProductName(sku.getProductName());
item.setAvailablePackagesSnapshot(Math.toIntExact(inventory.countAvailableProfile(
@@ -326,14 +378,50 @@ public class CargoDiscardService {
}
}
private void release(String customerId, List<CargoDiscardCarton> links) {
private void release(String customerId, List<CargoDiscardCarton> links, List<CargoDiscardItem> applicationItems) {
for (CargoDiscardCarton link : links) {
if (inventory.changeCartonStatus(customerId, link.getCartonId(), "DISCARD_PENDING", "AVAILABLE") != 1) {
throw conflict("Reserved cargo status changed before rejection");
}
}
applicationItems.stream().filter(this::isReturnItem).map(CargoDiscardItem::getReturnCartonId)
.filter(java.util.Objects::nonNull).distinct().forEach(cartonId -> {
if (returnCartons.releaseDiscard(cartonId, customerId) != 1) {
throw conflict("Reserved return inventory status changed before rejection");
}
});
}
private void reserveReturn(String customerId, CargoDiscardApplication application, ReturnInventoryCarton carton,
CargoDiscardRequests.Item input, LocalDateTime now, Set<Long> reservedCartons) {
ReturnInventoryCartonItem sku = returnCartonItems.lockSku(carton.getId(), input.skuCode().trim());
if (sku == null || value(sku.getQuantity()) < input.discardPackages()) {
throw conflict("Available return inventory changed; retry the discard request");
}
if (reservedCartons.add(carton.getId()) && returnCartons.markDiscardPending(carton.getId(), customerId) != 1) {
throw conflict("Return inventory was reserved concurrently; retry the discard request");
}
CargoDiscardItem item = new CargoDiscardItem();
item.setId(IdWorker.getId()); item.setApplicationId(application.getId()); item.setCustomerId(customerId);
item.setSourceType("RETURN_CARTON"); item.setReturnCartonId(carton.getId()); item.setInboundCode(carton.getSourceReturnNo());
item.setSkuCode(sku.getSkuCode()); item.setProductName(sku.getProductNameZh());
item.setAvailablePackagesSnapshot(Math.toIntExact(value(sku.getQuantity())));
item.setDiscardPackages(input.discardPackages()); item.setCreatedAt(now); items.insert(item);
}
private ReturnInventoryCarton requireReturnCarton(String customerId, Long cartonId) {
ReturnInventoryCarton carton = returnCartons.lockId(cartonId);
if (carton == null || !customerId.equals(carton.getCustomerId())) throw notFound("Return inventory carton not found");
if (!"AVAILABLE".equals(carton.getStatus())) throw conflict("Return inventory carton is not available");
return carton;
}
private boolean isReturnItem(CargoDiscardItem item) {
return "RETURN_CARTON".equals(item.getSourceType()) || item.getReturnCartonId() != null;
}
private static long value(Long value) { return value == null ? 0L : value; }
private LastlegOrder requireDropshipOrder(String customerId, Long orderId) {
LastlegOrder order = orders.selectById(orderId);
if (order == null || !customerId.equals(order.getCustomerId())) throw notFound("Inbound order not found");
@@ -385,6 +473,9 @@ public class CargoDiscardService {
private static LocalDateTime now() { return LocalDateTime.now(); }
private static String trim(String value) { return value == null || value.isBlank() ? null : value.trim(); }
private static boolean containsIgnoreCase(String value, String keyword) {
return value != null && keyword != null && value.toLowerCase(java.util.Locale.ROOT).contains(keyword.toLowerCase(java.util.Locale.ROOT));
}
private static String required(String value, String message) { String result = trim(value); if (result == null) throw conflict(message); return result; }
private static ResponseStatusException badRequest(String message) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, message); }
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
@@ -401,7 +492,7 @@ public class CargoDiscardService {
private CargoDiscardResponses.Candidate response(long physicalPackages) {
List<CargoDiscardResponses.CandidateSku> skuViews = skus.values().stream()
.map(value -> new CargoDiscardResponses.CandidateSku(value.skuCode, value.productName, value.packages)).toList();
return new CargoDiscardResponses.Candidate(first.getOrderId(), first.getInboundCode(), first.getInboundOrderNo(),
return new CargoDiscardResponses.Candidate(first.getOrderId(), "DROPSHIP", null, null, first.getInboundCode(), first.getInboundOrderNo(),
first.getOverseasWarehouseId(), first.getOverseasWarehouseName(), first.getCreatedAt(), physicalPackages, skuViews);
}
}
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCartonItem;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@@ -14,4 +15,16 @@ public interface ReturnInventoryCartonItemMapper extends BaseMapper<ReturnInvent
List<ReturnInventoryCartonItem> listCarton(@Param("cartonId") long cartonId);
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} ORDER BY sku_code FOR UPDATE")
List<ReturnInventoryCartonItem> lockCarton(@Param("cartonId") long cartonId);
@Select("SELECT * FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} AND sku_code=#{skuCode} LIMIT 1 FOR UPDATE")
ReturnInventoryCartonItem lockSku(@Param("cartonId") long cartonId, @Param("skuCode") String skuCode);
@Update("UPDATE ll_return_inventory_carton_item SET quantity=quantity-#{quantity} WHERE id=#{id} AND quantity>=#{quantity}")
int deduct(@Param("id") long id, @Param("quantity") long quantity);
@Select("SELECT COALESCE(SUM(quantity),0) FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId}")
long totalQuantity(@Param("cartonId") long cartonId);
@Select("SELECT COUNT(*) FROM ll_return_inventory_carton_item WHERE carton_id=#{cartonId} AND quantity>0")
int skuTypes(@Param("cartonId") long cartonId);
}
@@ -25,7 +25,9 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
List<ReturnInventoryCarton> merchantPage(@Param("customerId") String customerId,
@Param("query") com.baiyue.lastleg.returnreceipt.dto.ReturnInventoryRequests.Query query, @Param("offset") long offset);
@Select("SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND overseas_warehouse_id=#{warehouseId} AND status='AVAILABLE' ORDER BY created_at DESC,id DESC LIMIT 500")
@Select("<script>SELECT * FROM ll_return_inventory_carton WHERE customer_id=#{customerId} AND status='AVAILABLE' " +
"<if test='warehouseId != null and warehouseId != \"\"'>AND overseas_warehouse_id=#{warehouseId} </if>" +
"ORDER BY created_at DESC,id DESC LIMIT 500</script>")
List<ReturnInventoryCarton> findAvailable(@Param("customerId") String customerId, @Param("warehouseId") String warehouseId);
@Select("SELECT * FROM ll_return_inventory_carton WHERE carton_mark=#{cartonMark} LIMIT 1")
ReturnInventoryCarton findMark(@Param("cartonMark") String cartonMark);
@@ -36,6 +38,17 @@ public interface ReturnInventoryCartonMapper extends BaseMapper<ReturnInventoryC
@Select("SELECT * FROM ll_return_inventory_carton WHERE id=#{id} FOR UPDATE")
ReturnInventoryCarton lockId(@Param("id") long id);
@Update("UPDATE ll_return_inventory_carton SET status='DISCARD_PENDING',updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='AVAILABLE'")
int markDiscardPending(@Param("id") long id, @Param("customerId") String customerId);
@Update("UPDATE ll_return_inventory_carton SET status=#{status},total_quantity=#{totalQuantity},sku_types=#{skuTypes},updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='DISCARD_PENDING'")
int finishDiscard(@Param("id") long id, @Param("customerId") String customerId,
@Param("status") String status, @Param("totalQuantity") long totalQuantity,
@Param("skuTypes") int skuTypes);
@Update("UPDATE ll_return_inventory_carton SET status='AVAILABLE',updated_at=NOW(3) WHERE id=#{id} AND customer_id=#{customerId} AND status='DISCARD_PENDING'")
int releaseDiscard(@Param("id") long id, @Param("customerId") String customerId);
@Select("SELECT COALESCE(SUM(ri.notified_package_count),0) FROM ll_relabel_item ri " +
"JOIN ll_relabel_application ra ON ra.id=ri.application_id " +
"WHERE ri.return_carton_id=#{cartonId} AND ri.sku_code=#{skuCode} " +
@@ -2074,8 +2074,9 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_application (
CREATE TABLE IF NOT EXISTS ll_cargo_discard_item (
id BIGINT NOT NULL COMMENT '主键ID', application_id BIGINT NOT NULL COMMENT '申请单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
order_id BIGINT NOT NULL COMMENT '订单ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NOT NULL COMMENT '托盘档案ID',
sku_inventory_id BIGINT NOT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称',
order_id BIGINT NULL COMMENT '一件代发订单ID', source_type VARCHAR(32) NOT NULL DEFAULT 'DROPSHIP' COMMENT '库存来源:一件代发DROPSHIP、退货箱RETURN_CARTON',
return_carton_id BIGINT NULL COMMENT '退货库存箱ID', inbound_code VARCHAR(128) NOT NULL COMMENT '入仓单号快照', profile_id BIGINT NULL COMMENT '托盘档案ID',
sku_inventory_id BIGINT NULL COMMENT 'SKU库存记录ID', sku_code VARCHAR(128) NOT NULL COMMENT 'SKU编码', product_name VARCHAR(255) NULL COMMENT '产品名称',
available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT NULL COMMENT '申请废弃箱数',
actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数', pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PDA处理状态:待处理PENDING、已完成COMPLETED',
pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间',
@@ -2636,17 +2637,17 @@ PREPARE return_receipt_carton_mark_stmt FROM @ddl; EXECUTE return_receipt_carton
CREATE TABLE IF NOT EXISTS ll_return_inventory_carton (
new_carton_mark VARCHAR(255) NULL COMMENT '换标后新箱唛',
id BIGINT NOT NULL COMMENT '退货库存箱ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID和隔离键', overseas_warehouse_id VARCHAR(128) NOT NULL COMMENT '海外仓ID', source_receipt_id BIGINT NULL COMMENT '来源PDA签收单ID', source_batch_no VARCHAR(128) NULL COMMENT '来源签收批次号', carton_mark VARCHAR(128) NOT NULL COMMENT '退货箱号和箱唛编码', status VARCHAR(32) NOT NULL COMMENT 'AVAILABLE可用或MERGED已合箱', sku_types INT NOT NULL COMMENT '箱内SKU种类数', total_quantity BIGINT NOT NULL COMMENT '箱内总件数', length DECIMAL(12,3) NULL COMMENT '箱长CM', width DECIMAL(12,3) NULL COMMENT '箱宽CM', height DECIMAL(12,3) NULL COMMENT '箱高CM', weight DECIMAL(12,3) NULL COMMENT '箱重KG', created_at DATETIME(3) NOT NULL COMMENT '创建时间', updated_at DATETIME(3) NOT NULL COMMENT '最后更新时间', merged_at DATETIME(3) NULL COMMENT '合箱完成时间',
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED'))
PRIMARY KEY(id), UNIQUE KEY uk_ll_return_inventory_carton_mark(carton_mark), UNIQUE KEY uk_ll_return_inventory_carton_receipt(source_receipt_id), KEY idx_ll_return_inventory_carton_customer(customer_id,overseas_warehouse_id,status,created_at), CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0)), CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'))
) ENGINE=InnoDB COMMENT='退货实物库存箱及箱唛';
SET @ll_return_carton_add_pallet = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='pallet_code'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN pallet_code VARCHAR(128) NULL COMMENT ''签收时绑定的托盘码'' AFTER source_return_no'));
PREPARE ll_return_carton_add_pallet_stmt FROM @ll_return_carton_add_pallet; EXECUTE ll_return_carton_add_pallet_stmt; DEALLOCATE PREPARE ll_return_carton_add_pallet_stmt;
-- 已合箱的来源箱保留审计记录,但其箱内库存必须归零;动态 SQL 兼容 Spring 的数据库初始化器。
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_quantity'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_quantity', 'SELECT 1'));
PREPARE return_inventory_carton_check_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_check_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_check_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED') AND sku_types>0 AND total_quantity>0) OR (status='MERGED' AND sku_types=0 AND total_quantity=0));
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_quantity CHECK((status IN('AVAILABLE','RELABEL_PENDING','OUTBOUNDED','DISCARD_PENDING') AND sku_types>0 AND total_quantity>0) OR (status IN('MERGED','DISCARDED') AND sku_types=0 AND total_quantity=0));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.table_constraints WHERE constraint_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND constraint_name='chk_ll_return_inventory_carton_status'), 'ALTER TABLE ll_return_inventory_carton DROP CHECK chk_ll_return_inventory_carton_status', 'SELECT 1'));
PREPARE return_inventory_carton_status_drop_stmt FROM @ddl; EXECUTE return_inventory_carton_status_drop_stmt; DEALLOCATE PREPARE return_inventory_carton_status_drop_stmt;
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED'));
ALTER TABLE ll_return_inventory_carton ADD CONSTRAINT chk_ll_return_inventory_carton_status CHECK(status IN('AVAILABLE','RELABEL_PENDING','MERGED','OUTBOUNDED','DISCARD_PENDING','DISCARDED'));
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_return_inventory_carton' AND column_name='source_return_no'), 'SELECT 1', 'ALTER TABLE ll_return_inventory_carton ADD COLUMN source_return_no VARCHAR(2000) NULL COMMENT ''来源退货单号'' AFTER source_batch_no'));
PREPARE return_inventory_carton_return_no_stmt FROM @ddl; EXECUTE return_inventory_carton_return_no_stmt; DEALLOCATE PREPARE return_inventory_carton_return_no_stmt;
ALTER TABLE ll_return_inventory_carton MODIFY COLUMN source_batch_no TEXT NULL COMMENT '实际消耗来源签收批次号,多个批次逗号分隔', MODIFY COLUMN source_return_no TEXT NULL COMMENT '实际消耗来源退货单号,多个单号逗号分隔';