提交
This commit is contained in:
+6
-2
@@ -29,12 +29,16 @@ public final class CargoDiscardResponses {
|
||||
}
|
||||
}
|
||||
public record Box(Long cartonId, String cartonMark, String palletNo, List<CandidateSku> skus) {}
|
||||
public record CandidateSku(String skuCode, String productName, long availablePackages) {}
|
||||
public record CandidateSku(String skuCode, String productName, long availablePackages, Long availablePieces) {
|
||||
/** 无件数事实时保留未知,不按箱规推算。 */
|
||||
public CandidateSku(String skuCode, String productName, long availablePackages) { this(skuCode, productName, availablePackages, null); }
|
||||
}
|
||||
|
||||
public record Summary(Long id, String customerId, String discardNo, String inboundCodes,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
int totalPackages, BigDecimal totalFee, String currency, String status,
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, String outboundNo, LocalDateTime customerConfirmedAt) {}
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, String outboundNo, LocalDateTime customerConfirmedAt,
|
||||
List<String> skuCodes, int skuCount, int requestedPackages, Integer actualPackages) {}
|
||||
|
||||
public record Item(Long id, Long orderId, String inboundCode, String skuCode,
|
||||
String productName, int availablePackagesSnapshot, int discardPackages, List<String> cartonMarks,
|
||||
|
||||
+11
@@ -19,4 +19,15 @@ public interface CargoDiscardDimensionMapper {
|
||||
WHERE i.customer_id=#{customerId} AND i.application_id=#{applicationId}
|
||||
""")
|
||||
List<String> palletNos(@Param("customerId") String customerId, @Param("applicationId") Long applicationId);
|
||||
|
||||
/** 仅读取所选转运实收箱的真实件数,其他来源保留未知。 */
|
||||
@Select("""
|
||||
SELECT CASE WHEN l.source_type='TRANSFER' THEN c.sku_items_json ELSE NULL END
|
||||
FROM ll_cargo_discard_item i
|
||||
JOIN ll_cargo_discard_carton l ON l.customer_id=i.customer_id AND l.application_id=i.application_id
|
||||
AND (l.item_id=i.id OR (i.source_type='INVENTORY' AND JSON_CONTAINS(i.carton_marks_json,JSON_QUOTE(l.carton_mark))))
|
||||
LEFT JOIN ll_warehouse_transfer_carton c ON c.id=l.carton_id AND c.customer_id=l.customer_id
|
||||
WHERE i.customer_id=#{customerId} AND i.id=#{itemId}
|
||||
""")
|
||||
List<String> selectedCartonPieces(@Param("customerId") String customerId, @Param("itemId") Long itemId);
|
||||
}
|
||||
|
||||
+4
-4
@@ -25,16 +25,16 @@ public interface CargoDiscardInventoryMapper {
|
||||
@Select("SELECT p.* FROM ll_inventory_position p WHERE p.customer_id=#{customerId} AND p.id=#{positionId} FOR UPDATE")
|
||||
com.baiyue.lastleg.inventory.entity.InventoryPosition lockGenericPosition(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||
|
||||
/** 按实收托盘及 SKU 关联已清点箱,避免漏箱或混入其他托盘的箱号。 */
|
||||
/** 按实收托盘关联已清点箱;混装物理余额允许箱内不同 SKU,仍严格限定客户、批次、仓和托盘。 */
|
||||
@Select("""
|
||||
SELECT c.id,c.carton_mark,c.sku_code,'INBOUND' source_type
|
||||
SELECT c.id,c.carton_mark,c.sku_code,'INBOUND' source_type,NULL sku_items_json
|
||||
FROM ll_inbound_plan_carton c
|
||||
JOIN ll_inventory_balance b ON b.customer_id=c.customer_id AND b.inbound_order_id=c.order_id
|
||||
JOIN ll_inventory_position p ON p.customer_id=b.customer_id AND p.balance_id=b.id AND p.pallet_id=c.warehouse_pallet_id
|
||||
WHERE p.customer_id=#{customerId} AND p.id=#{positionId} AND c.status='AVAILABLE' AND c.received_at IS NOT NULL
|
||||
AND COALESCE(b.destination_warehouse_code,'')=COALESCE(c.destination_warehouse_code,'')
|
||||
UNION ALL
|
||||
SELECT c.id,c.carton_mark,i.sku_code,'TRANSFER' source_type
|
||||
SELECT c.id,c.carton_mark,i.sku_code,'TRANSFER' source_type,c.sku_items_json
|
||||
FROM ll_inventory_position p
|
||||
JOIN ll_inventory_balance b ON b.customer_id=p.customer_id AND b.id=p.balance_id
|
||||
JOIN ll_warehouse_transfer_order t ON t.customer_id=p.customer_id AND t.inbound_order_id=b.inbound_order_id
|
||||
@@ -44,7 +44,7 @@ public interface CargoDiscardInventoryMapper {
|
||||
JOIN ll_warehouse_transfer_carton c ON c.customer_id=t.customer_id AND c.transfer_order_id=t.id
|
||||
AND c.transfer_pallet_id=tp.id AND c.status='COUNTED'
|
||||
JOIN ll_warehouse_transfer_item i ON i.customer_id=c.customer_id AND i.id=c.transfer_item_id
|
||||
AND i.transfer_order_id=t.id AND COALESCE(i.sku_code,'')=COALESCE(b.sku_code,'')
|
||||
AND i.transfer_order_id=t.id AND (b.sku_code='MIXED' OR COALESCE(i.sku_code,'')=COALESCE(b.sku_code,''))
|
||||
WHERE p.customer_id=#{customerId} AND p.id=#{positionId}
|
||||
ORDER BY id
|
||||
""")
|
||||
|
||||
+8
-1
@@ -12,15 +12,22 @@ public final class CargoDiscardQuantities {
|
||||
/** 退货混装箱按箱 ID 去重,其余来源累加已选择的物理箱数。 */
|
||||
public static int boxes(List<CargoDiscardItem> lines, boolean actual) {
|
||||
var returnBoxes = new HashSet<Long>();
|
||||
var inventoryBoxes = new HashSet<String>();
|
||||
int total = 0;
|
||||
for (var line : lines) {
|
||||
int quantity = actual ? java.util.Objects.requireNonNullElse(line.getActualDiscardPackages(), 0) : line.getDiscardPackages();
|
||||
if (line.getReturnCartonId() != null) {
|
||||
if (quantity > 0) returnBoxes.add(line.getReturnCartonId());
|
||||
} else if ("INVENTORY".equals(line.getSourceType()) && line.getCartonMarksJson() != null && quantity > 0) {
|
||||
try {
|
||||
var marks = new com.fasterxml.jackson.databind.ObjectMapper().readTree(line.getCartonMarksJson());
|
||||
if (!marks.isArray() || marks.size() != quantity) throw new IllegalStateException("废弃箱唛快照与数量不一致");
|
||||
for (var mark : marks) inventoryBoxes.add(line.getInboundCode() + ":" + mark.asText());
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw new IllegalStateException("废弃箱唛快照无法读取", e); }
|
||||
} else {
|
||||
total = Math.addExact(total, quantity);
|
||||
}
|
||||
}
|
||||
return Math.addExact(total, returnBoxes.size());
|
||||
return Math.addExact(Math.addExact(total, returnBoxes.size()), inventoryBoxes.size());
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -9,4 +9,5 @@ public class CargoDiscardStockCartonRow {
|
||||
private String cartonMark;
|
||||
private String skuCode;
|
||||
private String sourceType;
|
||||
private String skuItemsJson;
|
||||
}
|
||||
|
||||
+2
-1
@@ -21,7 +21,8 @@ public final class PdaCargoDiscardDtos {
|
||||
}
|
||||
/** 明细数量按来源标识单位:退货库存为件,其余箱级库存为箱。 */
|
||||
public record Sku(Long itemId, String skuCode, String productName, int plannedPackages,
|
||||
Integer actualPackages, String status, String quantityUnit) { }
|
||||
Integer actualPackages, String status, String quantityUnit,
|
||||
boolean actualQuantityEditable) { }
|
||||
public record PalletTask(String discardNo, String customerId, String overseasWarehouseId,
|
||||
String palletNo, Long profileId, String status, List<Sku> skus, String outboundNo,
|
||||
String scanMode, List<String> cartonMarks) { }
|
||||
|
||||
+44
-35
@@ -1,17 +1,11 @@
|
||||
package com.baiyue.lastleg.discard.pda.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.lastleg.discard.dto.CargoDiscardResponses;
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos;
|
||||
import com.baiyue.lastleg.discard.service.CargoDiscardInventoryAdjustmentService;
|
||||
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;
|
||||
@@ -24,28 +18,24 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class PdaCargoDiscardService {
|
||||
@Resource private com.baiyue.lastleg.discard.service.CargoDiscardPhotoService photoService;
|
||||
@Resource private CargoDiscardApplicationMapper applications;
|
||||
@Resource private com.baiyue.lastleg.discard.repository.CargoDiscardPdaRepository repository;
|
||||
@Resource private com.baiyue.lastleg.discard.service.CargoDiscardOutboundService outboundService;
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardCartonMapper cartons;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
|
||||
@Resource private CargoDiscardService discardService;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
@Resource private com.baiyue.lastleg.discard.service.CargoDiscardStockService stockService;
|
||||
|
||||
/** 只读取绑定仓已审核申请的实际托盘任务。 */
|
||||
public List<PdaCargoDiscardDtos.PalletTask> tasks(PdaWarehouseIdentity identity) {
|
||||
return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(CargoDiscardApplication::getOverseasWarehouseId, String.valueOf(identity.overseasWarehouseId()))
|
||||
.eq(CargoDiscardApplication::getStatus, CargoDiscardService.APPROVED)
|
||||
.orderByAsc(CargoDiscardApplication::getCreatedAt))
|
||||
return repository.approved(String.valueOf(identity.overseasWarehouseId()))
|
||||
.stream().flatMap(a -> palletTasks(a).stream()).toList();
|
||||
}
|
||||
|
||||
/** 扫码读取当前仓库唯一可操作任务。 */
|
||||
public PdaCargoDiscardDtos.PalletTask resolve(PdaWarehouseIdentity identity, String palletNo) {
|
||||
return resolve(identity, palletNo, null);
|
||||
}
|
||||
|
||||
/** 扫码时限定已选择申请,防止同托盘多任务误操作。 */
|
||||
public PdaCargoDiscardDtos.PalletTask resolve(PdaWarehouseIdentity identity, String palletNo, String discardNo) {
|
||||
var matches = tasks(identity).stream().filter(t -> t.palletNo().equals(palletNo.trim()) || t.cartonMarks().contains(palletNo.trim()))
|
||||
.filter(t -> discardNo == null || discardNo.isBlank() || t.discardNo().equals(discardNo.trim())).toList();
|
||||
@@ -54,10 +44,11 @@ public class PdaCargoDiscardService {
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "该仓库没有此托盘的待废弃任务"));
|
||||
}
|
||||
|
||||
/** 仓库按无箱唛对应任务的实点数结算,其余任务继续核平所选箱。 */
|
||||
@Transactional
|
||||
public PdaCargoDiscardDtos.PalletTask confirm(PdaWarehouseIdentity identity, String discardNo,
|
||||
String palletNo, PdaCargoDiscardDtos.Confirm request) {
|
||||
CargoDiscardApplication application = applications.findNoByWarehouse(discardNo,
|
||||
CargoDiscardApplication application = repository.lockApplication(discardNo,
|
||||
String.valueOf(identity.overseasWarehouseId()));
|
||||
if (application == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "废弃任务不存在或不属于当前仓库");
|
||||
boolean terminal = CargoDiscardService.PDA_COMPLETED.equals(application.getStatus())
|
||||
@@ -71,8 +62,12 @@ public class PdaCargoDiscardService {
|
||||
if (!actuals.keySet().equals(task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).collect(Collectors.toSet()))) {
|
||||
throw conflict("必须提交该托盘全部 SKU 的实际废弃数量");
|
||||
}
|
||||
if (task.skus().stream().anyMatch(line -> !Objects.equals(actuals.get(line.itemId()), line.plannedPackages())))
|
||||
throw conflict("必须完成本次废弃单所选全部箱子,不能漏出");
|
||||
for (var line : task.skus()) {
|
||||
int actual = actuals.get(line.itemId());
|
||||
if (actual < 0 || actual > line.plannedPackages()) throw conflict("实际废弃数量不能超过申请预留数量");
|
||||
if (!line.actualQuantityEditable() && actual != line.plannedPackages())
|
||||
throw conflict("必须完成本次废弃单所选全部箱子,不能漏出");
|
||||
}
|
||||
if ("CARTON".equals(task.scanMode()) && !Boolean.TRUE.equals(request.confirmAllCartons())) {
|
||||
var scanned = request.scannedCartonMarks() == null ? List.<String>of() : request.scannedCartonMarks();
|
||||
if (scanned.size() != task.cartonMarks().size() || !new HashSet<>(scanned).equals(new HashSet<>(task.cartonMarks())))
|
||||
@@ -80,7 +75,7 @@ public class PdaCargoDiscardService {
|
||||
}
|
||||
List<CargoDiscardItem> palletItems = new ArrayList<>();
|
||||
for (PdaCargoDiscardDtos.Sku line : task.skus()) {
|
||||
CargoDiscardItem item = items.selectById(line.itemId());
|
||||
CargoDiscardItem item = repository.item(line.itemId());
|
||||
if (item == null || !application.getCustomerId().equals(item.getCustomerId())
|
||||
|| !Objects.equals(application.getId(), item.getApplicationId())
|
||||
|| !physicalCode(application, item).equals(task.palletNo()))
|
||||
@@ -90,14 +85,14 @@ public class PdaCargoDiscardService {
|
||||
throw conflict("已完成托盘的实际数量不能修改");
|
||||
palletItems.add(item);
|
||||
}
|
||||
if (terminal && items.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||
if (terminal && repository.items(application.getCustomerId(), application.getId()).stream()
|
||||
.anyMatch(item -> !"COMPLETED".equals(item.getPdaStatus())
|
||||
|| !application.getCustomerId().equals(item.getCustomerId())
|
||||
|| !Objects.equals(application.getId(), item.getApplicationId())))
|
||||
throw conflict("废弃申请终态存在未完成明细,不能恢复作业结果");
|
||||
photoService.confirm(application, task.palletNo(), request.photoFileIds(),
|
||||
palletItems.stream().allMatch(item -> "COMPLETED".equals(item.getPdaStatus())));
|
||||
// A completed pallet is an immutable result, including the final pallet after application completion.
|
||||
// 已完成托盘保持原始实点结果,终态重放不得再次扣减或释放库存。
|
||||
if (palletItems.stream().allMatch(item -> "COMPLETED".equals(item.getPdaStatus()))) return task;
|
||||
if (terminal) throw conflict("废弃终态不能执行未完成托盘");
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -105,20 +100,25 @@ public class PdaCargoDiscardService {
|
||||
if ("COMPLETED".equals(item.getPdaStatus())) {
|
||||
continue;
|
||||
}
|
||||
List<CargoDiscardCarton> links = cartons.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||
List<CargoDiscardCarton> links = repository.cartons(application.getCustomerId(), application.getId()).stream()
|
||||
.filter(c -> c.getItemId().equals(item.getId())).toList();
|
||||
adjustments.discardPalletItem(application.getCustomerId(), application, item, links,
|
||||
actuals.get(item.getId()), identity.operatorKey(), now);
|
||||
// 未知箱唛对应关系时只按当前仓位冻结桶结算,不猜测被废弃的具体箱唛。
|
||||
if ("INVENTORY".equals(item.getSourceType()) && stockService.actualQuantityEditable(item)) {
|
||||
stockService.discard(application.getCustomerId(), identity.operatorKey(), application, item, actuals.get(item.getId()));
|
||||
} else {
|
||||
adjustments.discardPalletItem(application.getCustomerId(), application, item, links,
|
||||
actuals.get(item.getId()), identity.operatorKey(), now);
|
||||
}
|
||||
item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED");
|
||||
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item);
|
||||
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); repository.saveItem(item);
|
||||
}
|
||||
task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).map(items::selectById)
|
||||
task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).map(repository::item)
|
||||
.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()
|
||||
if (repository.items(application.getCustomerId(), application.getId()).stream()
|
||||
.allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
|
||||
int actualTotal = com.baiyue.lastleg.discard.model.CargoDiscardQuantities.boxes(
|
||||
items.listApplication(application.getCustomerId(), application.getId()), true);
|
||||
repository.items(application.getCustomerId(), application.getId()), true);
|
||||
application.setTotalPackages(actualTotal);
|
||||
application.setTotalFee(application.getFeePerPackage().multiply(java.math.BigDecimal.valueOf(actualTotal)));
|
||||
discardService.updateLinkedStatus(application.getCustomerId(), application, CargoDiscardService.APPROVED,
|
||||
@@ -130,6 +130,7 @@ public class PdaCargoDiscardService {
|
||||
}
|
||||
|
||||
@Transactional
|
||||
/** 当前申请行锁内保存现场照片,已完成托盘禁止补写。 */
|
||||
public CargoDiscardResponses.Photo uploadPhoto(PdaWarehouseIdentity identity, String discardNo, String palletNo,
|
||||
long operatorUserId, org.springframework.web.multipart.MultipartFile file) {
|
||||
CargoDiscardApplication application = photoApplication(identity, discardNo, palletNo, true);
|
||||
@@ -140,19 +141,22 @@ public class PdaCargoDiscardService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
/** 读取绑定仓当前托盘的照片。 */
|
||||
public List<CargoDiscardResponses.Photo> photos(PdaWarehouseIdentity identity, String discardNo, String palletNo) {
|
||||
return photoService.list(photoApplication(identity, discardNo, palletNo, false), palletNo.trim(), false);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
/** 下载当前申请和托盘拥有的照片。 */
|
||||
public com.baiyue.lastleg.discard.gateway.CargoDiscardPhotoFileGateway.DownloadedFile downloadPhoto(
|
||||
PdaWarehouseIdentity identity, String discardNo, String palletNo, long fileId) {
|
||||
return photoService.download(photoApplication(identity, discardNo, palletNo, false), palletNo.trim(), fileId, false);
|
||||
}
|
||||
|
||||
/** 照片访问必须同时满足绑定仓和申请托盘范围。 */
|
||||
private CargoDiscardApplication photoApplication(PdaWarehouseIdentity identity, String discardNo, String palletNo, boolean lock) {
|
||||
var warehouseId = String.valueOf(identity.overseasWarehouseId());
|
||||
var application = lock ? applications.findNoByWarehouse(discardNo, warehouseId) : applications.readNoByWarehouse(discardNo, warehouseId);
|
||||
var application = lock ? repository.lockApplication(discardNo, warehouseId) : repository.readApplication(discardNo, warehouseId);
|
||||
if (application == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "废弃任务不存在或不属于当前仓库");
|
||||
if (palletTasks(application).stream().noneMatch(t -> t.palletNo().equals(palletNo.trim())))
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "托盘不属于该废弃单");
|
||||
@@ -162,13 +166,13 @@ public class PdaCargoDiscardService {
|
||||
private static final String LOOSE = "散箱";
|
||||
/** 按实托汇总本单所选箱唛,退货件数与物理箱数分别呈现。 */
|
||||
private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) {
|
||||
var allItems = items.listApplication(application.getCustomerId(), application.getId());
|
||||
var allItems = repository.items(application.getCustomerId(), application.getId());
|
||||
var grouped = allItems.stream().collect(Collectors.groupingBy(i -> physicalCode(application, i), LinkedHashMap::new, Collectors.toList()));
|
||||
return grouped.entrySet().stream().map(entry -> {
|
||||
CargoDiscardItem first = entry.getValue().get(0);
|
||||
var 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(),
|
||||
isReturnItem(i) ? "件" : "箱")).toList();
|
||||
isReturnItem(i) ? "件" : "箱", "INVENTORY".equals(i.getSourceType()) && stockService.actualQuantityEditable(i))).toList();
|
||||
String status = skus.stream().allMatch(i -> "COMPLETED".equals(i.status())) ? "COMPLETED" : "PENDING";
|
||||
var marks = entry.getValue().stream().flatMap(i -> cartonMarks(application, i).stream()).distinct().sorted().toList();
|
||||
// 退货箱只代表本单选择的箱子,不能因为有托盘号就当成整托废弃。
|
||||
@@ -180,6 +184,7 @@ public class PdaCargoDiscardService {
|
||||
status, skus, outboundService.outboundNo(application), cartonScan ? "CARTON" : "PALLET", marks);
|
||||
}).toList();
|
||||
}
|
||||
/** 相关托盘只从客户库存事实读取,不从请求推断。 */
|
||||
private String physicalCode(CargoDiscardApplication application, CargoDiscardItem item) {
|
||||
String code;
|
||||
if ("INVENTORY".equals(item.getSourceType())) {
|
||||
@@ -187,31 +192,35 @@ public class PdaCargoDiscardService {
|
||||
if (!application.getOverseasWarehouseId().equals(position.getOverseasWarehouseId())) throw conflict("废弃库存不属于当前仓库");
|
||||
code = position.getPalletNo();
|
||||
} else if (isReturnItem(item)) {
|
||||
var carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
var carton = repository.returnCarton(item.getReturnCartonId());
|
||||
if (carton == null || !application.getCustomerId().equals(carton.getCustomerId())) throw conflict("废弃明细关联退货箱不存在");
|
||||
code = carton.getPalletCode();
|
||||
} else {
|
||||
var profile = profiles.selectById(item.getProfileId());
|
||||
var profile = repository.profile(item.getProfileId());
|
||||
if (profile == null || !application.getCustomerId().equals(profile.getCustomerId())) throw conflict("废弃明细关联托盘不存在");
|
||||
code = profile.getPalletNo();
|
||||
}
|
||||
return code == null || code.isBlank() ? LOOSE : code.trim();
|
||||
}
|
||||
/** 仅返回保存的真实箱唛,未知对应关系不补造箱码。 */
|
||||
private List<String> cartonMarks(CargoDiscardApplication application, CargoDiscardItem item) {
|
||||
if ("INVENTORY".equals(item.getSourceType())) return stockService.cartonMarks(item);
|
||||
if (isReturnItem(item)) {
|
||||
var carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
var carton = repository.returnCarton(item.getReturnCartonId());
|
||||
String mark = com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton);
|
||||
return mark == null ? List.of() : List.of(mark);
|
||||
}
|
||||
return cartons.listApplication(application.getCustomerId(), application.getId()).stream()
|
||||
return repository.cartons(application.getCustomerId(), application.getId()).stream()
|
||||
.filter(c -> Objects.equals(c.getItemId(), item.getId())).map(CargoDiscardCarton::getCartonMark).filter(Objects::nonNull).toList();
|
||||
}
|
||||
/** 退货库存沿用独立按件结算规则。 */
|
||||
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); }
|
||||
}
|
||||
|
||||
+21
@@ -12,8 +12,29 @@ import java.util.List;
|
||||
public class CargoDiscardDimensionRepository {
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardDimensionMapper dimensions;
|
||||
@Resource private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
||||
/** 读取一票客户隔离明细。 */
|
||||
public List<CargoDiscardItem> items(String customer, Long application) { return items.listApplication(customer, application); }
|
||||
/** 读取一票托盘身份,保留无事实行。 */
|
||||
public List<String> pallets(String customer, Long application) { return dimensions.palletNos(customer, application); }
|
||||
/** 完整箱级事实才可提供件数,缺箱或未知件数不得固定换算。 */
|
||||
public Long pieces(String customer, CargoDiscardItem item) {
|
||||
var values = dimensions.selectedCartonPieces(customer, item.getId());
|
||||
if (values.isEmpty() || values.size() != item.getDiscardPackages()) return null;
|
||||
long total = 0;
|
||||
for (var value : values) {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
try {
|
||||
var contents = objectMapper.readTree(value);
|
||||
if (!contents.isArray()) return null;
|
||||
long selected = 0;
|
||||
for (var content : contents) {
|
||||
if (item.getSkuCode().equals(content.path("skuCode").asText())) selected += content.path("quantity").asLong();
|
||||
}
|
||||
if (selected <= 0) return null;
|
||||
total += selected;
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) { return null; }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.lastleg.discard.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipOrderProfile;
|
||||
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 封装 PDA 废弃任务读取和作业快照持久化,保留绑定仓与申请行锁。 */
|
||||
@Repository
|
||||
public class CargoDiscardPdaRepository {
|
||||
@Resource private CargoDiscardApplicationMapper applications;
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardCartonMapper cartons;
|
||||
@Resource private DropshipOrderProfileMapper profiles;
|
||||
@Resource private ReturnInventoryCartonMapper returnCartons;
|
||||
|
||||
/** 按已验证绑定仓读取已审核作业,客户归属由申请事实保留。 */
|
||||
public List<CargoDiscardApplication> approved(String warehouseId) {
|
||||
return applications.selectList(new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(CargoDiscardApplication::getOverseasWarehouseId, warehouseId)
|
||||
.eq(CargoDiscardApplication::getStatus, "APPROVED")
|
||||
.orderByAsc(CargoDiscardApplication::getCreatedAt));
|
||||
}
|
||||
/** 写操作按申请串行,保证扣减、释放、实点结果及终态同事务。 */
|
||||
public CargoDiscardApplication lockApplication(String no, String warehouse) { return applications.findNoByWarehouse(no, warehouse); }
|
||||
/** 照片只读入口仍限制绑定仓。 */
|
||||
public CargoDiscardApplication readApplication(String no, String warehouse) { return applications.readNoByWarehouse(no, warehouse); }
|
||||
/** 按申请所属客户读取全部明细。 */
|
||||
public List<CargoDiscardItem> items(String customer, Long application) { return items.listApplication(customer, application); }
|
||||
/** 读取明细后由服务复验客户与申请。 */
|
||||
public CargoDiscardItem item(Long id) { return items.selectById(id); }
|
||||
/** 保存一次实际作业结果。 */
|
||||
public void saveItem(CargoDiscardItem item) { items.updateById(item); }
|
||||
/** 按客户和申请读取已选箱快照。 */
|
||||
public List<CargoDiscardCarton> cartons(String customer, Long application) { return cartons.listApplication(customer, application); }
|
||||
/** 读取原托盘档案供服务核验归属。 */
|
||||
public DropshipOrderProfile profile(Long id) { return profiles.selectById(id); }
|
||||
/** 读取退货箱档案供服务核验归属。 */
|
||||
public ReturnInventoryCarton returnCarton(Long id) { return returnCartons.selectById(id); }
|
||||
}
|
||||
+3
-2
@@ -11,9 +11,10 @@ import org.springframework.stereotype.Repository;
|
||||
public class CargoDiscardQuantityRepository {
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
|
||||
/** 列表和详情均按所选实物箱展示;空明细保留申请自身快照。 */
|
||||
/** 完工后列表和详情按实际箱数展示,作业前保留申请预留数量。 */
|
||||
public int boxes(CargoDiscardApplication application) {
|
||||
var lines = items.listApplication(application.getCustomerId(), application.getId());
|
||||
return lines.isEmpty() ? application.getTotalPackages() : CargoDiscardQuantities.boxes(lines, false);
|
||||
boolean completed = "PDA_COMPLETED".equals(application.getStatus()) || "COMPLETED".equals(application.getStatus());
|
||||
return lines.isEmpty() ? application.getTotalPackages() : CargoDiscardQuantities.boxes(lines, completed);
|
||||
}
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package com.baiyue.lastleg.discard.repository;
|
||||
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow;
|
||||
import com.baiyue.lastleg.inventory.entity.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
/** 封装普通库存废弃的客户隔离读取、锁箱和申请快照保存。 */
|
||||
@Repository
|
||||
public class CargoDiscardStockRepository {
|
||||
@Resource private CargoDiscardInventoryMapper stock;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardCartonMapper links;
|
||||
|
||||
/** 读取客户当前可用且没有转运占用的库存仓位。 */
|
||||
public List<InventoryPosition> candidates(String customer, String keyword) { return stock.genericPositions(customer, keyword); }
|
||||
/** 读取余额,服务继续校验其客户与仓库归属。 */
|
||||
public InventoryBalance balance(Long id) { return balances.selectById(id); }
|
||||
/** 读取客户指定仓位。 */
|
||||
public InventoryPosition position(String customer, Long id) { return positions.findTenantPosition(customer, id); }
|
||||
/** 锁定源仓位以串行创建预留。 */
|
||||
public InventoryPosition lockPosition(String customer, Long id) { return stock.lockGenericPosition(customer, id); }
|
||||
/** 读取已有的真实箱唛与托盘对应事实。 */
|
||||
public List<CargoDiscardStockCartonRow> cartons(String customer, Long position) { return stock.genericCartons(customer, position); }
|
||||
/** 排除须走一件代发逐箱预留的仓位。 */
|
||||
public boolean dropshipPosition(String customer, Long position) { return stock.isDropshipPosition(customer, position) > 0; }
|
||||
/** 读取散箱实收流水中的箱码快照。 */
|
||||
public List<String> looseMarks(String customer, Long position) { return stock.looseCartonMarks(customer, position); }
|
||||
/** 保存废弃申请的库存来源明细。 */
|
||||
public void saveItem(CargoDiscardItem item) { items.insert(item); }
|
||||
/** 保存申请所选实物箱快照。 */
|
||||
public void saveCarton(CargoDiscardCarton carton) { links.insert(carton); }
|
||||
/** 读取申请全部所选箱,服务按当前明细筛选。 */
|
||||
public List<CargoDiscardCarton> selectedCartons(String customer, Long application) { return links.listApplication(customer, application); }
|
||||
/** 读取同申请各 SKU 的仓位和箱唛快照,用于按物理仓位消除重复库存命令。 */
|
||||
public List<CargoDiscardItem> applicationItems(String customer, Long application) { return items.listApplication(customer, application); }
|
||||
/** 普通实收箱用原状态条件更新,防止并发占用。 */
|
||||
public int changeInboundCarton(String customer, Long id, String from, String to) { return stock.changeGenericCartonStatus(customer, id, from, to); }
|
||||
/** 转运实收箱保留已清点状态语义。 */
|
||||
public int changeTransferCarton(String customer, Long id, String from, String to) { return stock.changeTransferCartonStatus(customer, id, from, to); }
|
||||
}
|
||||
+12
-3
@@ -58,6 +58,7 @@ public class CargoDiscardOutboundService {
|
||||
var snapshots = new java.util.ArrayList<OutboundPalletItem>();
|
||||
var palletCodes = new java.util.HashSet<String>();
|
||||
var returnBoxIds = new java.util.HashSet<Long>();
|
||||
var inventoryBoxMarks = new java.util.HashSet<String>();
|
||||
for (var line : lines) {
|
||||
boolean returned = line.getReturnCartonId() != null || "RETURN_CARTON".equals(line.getSourceType());
|
||||
String palletNo;
|
||||
@@ -88,7 +89,7 @@ public class CargoDiscardOutboundService {
|
||||
snapshot.setId(line.getId()); snapshot.setCustomerId(application.getCustomerId()); snapshot.setOutboundOrderId(outbound.getId());
|
||||
snapshot.setPalletNo(palletNo); snapshot.setSkuCode(line.getSkuCode()); snapshot.setInboundCode(line.getInboundCode());
|
||||
snapshot.setOrderRefs(application.getDiscardNo()); snapshot.setCartonMarks(cartonMarks);
|
||||
snapshot.setPackageCount(returned ? (returnBoxIds.add(line.getReturnCartonId()) ? 1 : 0) : line.getDiscardPackages());
|
||||
snapshot.setPackageCount(returned ? (returnBoxIds.add(line.getReturnCartonId()) ? 1 : 0) : physicalBoxes(line, inventoryBoxMarks));
|
||||
snapshot.setPickedPackageCount(0); snapshot.setStatus("WAIT_PICK");
|
||||
if (returned) snapshot.setReturnSkuQuantity((long) line.getDiscardPackages());
|
||||
snapshot.setVolumeWeightKg(BigDecimal.ZERO); snapshot.setVolumeCbm(BigDecimal.ZERO); snapshot.setCreatedAt(at);
|
||||
@@ -99,11 +100,13 @@ public class CargoDiscardOutboundService {
|
||||
}
|
||||
|
||||
/** 同步实物箱的拣货进度,退货 SKU 件数不进入箱数汇总。 */
|
||||
/** 同步废弃实物出库投影,混装 SKU 明细不能重复累计物理箱。 */
|
||||
public void sync(CargoDiscardApplication application) {
|
||||
OutboundOrder outbound = outbounds.lockByIdempotency(application.getCustomerId(), key(application));
|
||||
if (outbound == null) throw conflict("废弃出库单不存在,请联系平台核对审核结果");
|
||||
var lines = items.listApplication(application.getCustomerId(), application.getId());
|
||||
var returnBoxIds = new java.util.HashSet<Long>();
|
||||
var inventoryBoxMarks = new java.util.HashSet<String>();
|
||||
for (var snapshot : pallets.findByOutbound(application.getCustomerId(), outbound.getId())) {
|
||||
var line = lines.stream().filter(i -> Objects.equals(i.getId(), snapshot.getId())).findFirst()
|
||||
.orElseThrow(() -> conflict("废弃出库明细关联失效"));
|
||||
@@ -114,11 +117,11 @@ public class CargoDiscardOutboundService {
|
||||
throw conflict("废弃明细关联退货箱不存在");
|
||||
snapshot.setCartonMarks(com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton));
|
||||
}
|
||||
int boxes = returned ? (returnBoxIds.add(line.getReturnCartonId()) ? 1 : 0) : line.getDiscardPackages();
|
||||
int boxes = returned ? (returnBoxIds.add(line.getReturnCartonId()) ? 1 : 0) : physicalBoxes(line, inventoryBoxMarks);
|
||||
snapshot.setPackageCount(boxes);
|
||||
if ("COMPLETED".equals(line.getPdaStatus())) {
|
||||
snapshot.setPickedPackageCount(returned ? (Objects.requireNonNullElse(line.getActualDiscardPackages(), 0) > 0 ? boxes : 0)
|
||||
: Objects.requireNonNullElse(line.getActualDiscardPackages(), 0));
|
||||
: "INVENTORY".equals(line.getSourceType()) && !stockService.cartonMarks(line).isEmpty() ? boxes : Objects.requireNonNullElse(line.getActualDiscardPackages(), 0));
|
||||
snapshot.setStatus("PICKED");
|
||||
}
|
||||
if (CargoDiscardService.CANCELLED.equals(application.getStatus())) snapshot.setStatus("CANCELLED");
|
||||
@@ -134,5 +137,11 @@ public class CargoDiscardOutboundService {
|
||||
outbound.setPackageCount(com.baiyue.lastleg.discard.model.CargoDiscardQuantities.boxes(lines, "OUTBOUNDED".equals(status)));
|
||||
outbound.setUpdatedAt(LocalDateTime.now()); outbounds.updateById(outbound);
|
||||
}
|
||||
/** 保留逐 SKU 来源行,已投影过的同一实箱在后续行记零箱。 */
|
||||
private int physicalBoxes(com.baiyue.lastleg.discard.entity.CargoDiscardItem line, java.util.Set<String> seen) {
|
||||
if (!"INVENTORY".equals(line.getSourceType())) return line.getDiscardPackages();
|
||||
var marks = stockService.cartonMarks(line);
|
||||
return marks.isEmpty() ? line.getDiscardPackages() : (int)marks.stream().filter(mark -> seen.add(line.getInboundCode() + ":" + mark)).count();
|
||||
}
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
|
||||
+14
-4
@@ -165,7 +165,10 @@ public class CargoDiscardService {
|
||||
if (input.cartonIds() != null) {
|
||||
if (input.cartonIds().isEmpty() || input.cartonIds().size() != input.discardPackages()) throw badRequest("请选择具体废弃箱子,箱数须与选择一致");
|
||||
String source = input.inventoryPositionId() != null ? "INVENTORY" : "DROPSHIP";
|
||||
for (Long cartonId : input.cartonIds()) if (!requestedBoxes.add(source + ":" + cartonId)) throw badRequest("同一个箱子不能重复废弃");
|
||||
for (Long cartonId : input.cartonIds()) {
|
||||
boolean added = requestedBoxes.add(source + ":" + cartonId);
|
||||
if (!added && input.inventoryPositionId() == null) throw badRequest("同一个箱子不能重复废弃");
|
||||
}
|
||||
}
|
||||
int sources = (returnSource ? 1 : 0) + (input.orderId() != null ? 1 : 0) + (input.inventoryPositionId() != null ? 1 : 0);
|
||||
if (sources != 1) {
|
||||
@@ -202,7 +205,7 @@ public class CargoDiscardService {
|
||||
throw conflict("One cargo discard application cannot span operating warehouses");
|
||||
}
|
||||
// 退货明细为件数,整箱只在全部 SKU 校验完成后按箱 ID 计一次。
|
||||
if (!returnSource) total = Math.addExact(total, input.discardPackages());
|
||||
if (!returnSource && input.cartonIds() == null) total = Math.addExact(total, input.discardPackages());
|
||||
}
|
||||
|
||||
for (Long cartonId : returnCartonCache.keySet()) {
|
||||
@@ -214,6 +217,7 @@ public class CargoDiscardService {
|
||||
if (!availableSkus.equals(requestedSkus)) throw conflict("目前不支持部分废弃,必须选择退货箱内全部 SKU 库存");
|
||||
}
|
||||
total = Math.addExact(total, returnCartonCache.size());
|
||||
total = Math.addExact(total, requestedBoxes.size());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (!StringUtils.hasText(warehouseName)) warehouseName = warehouseNames().get(warehouseId);
|
||||
CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
@@ -236,9 +240,10 @@ public class CargoDiscardService {
|
||||
// 废弃申请独立承载废弃流程,不创建尾程订单。
|
||||
|
||||
Set<Long> reservedReturnCartons = new LinkedHashSet<>();
|
||||
stockService.reserveAll(customerId, operatorId, application, request.items(), now);
|
||||
for (CargoDiscardRequests.Item input : request.items()) {
|
||||
if (input.inventoryPositionId() != null) {
|
||||
stockService.reserve(customerId, operatorId, application, input, now);
|
||||
// 普通库存已按完整混装箱批量预留。
|
||||
} else if (input.returnCartonId() != null) {
|
||||
reserveReturn(customerId, application, returnCartonCache.get(input.returnCartonId()), input, now,
|
||||
reservedReturnCartons);
|
||||
@@ -513,10 +518,15 @@ public class CargoDiscardService {
|
||||
/** 废弃列表与详情按物理箱汇总,并按每箱单价计算应展示费用。 */
|
||||
private CargoDiscardResponses.Summary summary(CargoDiscardApplication value, Map<String, String> names) {
|
||||
int boxes = quantities.boxes(value);
|
||||
var lines = items.listApplication(value.getCustomerId(), value.getId());
|
||||
var skuCodes = lines.stream().map(CargoDiscardItem::getSkuCode).filter(s -> s != null && !s.isBlank()).distinct().sorted().toList();
|
||||
int requested = lines.isEmpty() ? value.getTotalPackages() : com.baiyue.lastleg.discard.model.CargoDiscardQuantities.boxes(lines, false);
|
||||
Integer actual = lines.stream().anyMatch(i -> i.getActualDiscardPackages() != null)
|
||||
? com.baiyue.lastleg.discard.model.CargoDiscardQuantities.boxes(lines, true) : null;
|
||||
return new CargoDiscardResponses.Summary(value.getId(), value.getCustomerId(), value.getDiscardNo(), value.getInboundCodes(),
|
||||
value.getOverseasWarehouseId(), StringUtils.hasText(value.getOverseasWarehouseName()) ? value.getOverseasWarehouseName() : names.get(value.getOverseasWarehouseId()), boxes,
|
||||
value.getFeePerPackage() == null ? value.getTotalFee() : value.getFeePerPackage().multiply(BigDecimal.valueOf(boxes)), value.getCurrency(), value.getStatus(), value.getCreatedBy(), value.getCreatedAt(),
|
||||
value.getReviewedAt(), outboundService.outboundNo(value), value.getCustomerConfirmedAt());
|
||||
value.getReviewedAt(), outboundService.outboundNo(value), value.getCustomerConfirmedAt(), skuCodes, skuCodes.size(), requested, actual);
|
||||
}
|
||||
|
||||
private void history(String customerId, Long applicationId, String from, String to, String operatorId,
|
||||
|
||||
+8
-2
@@ -32,8 +32,14 @@ public class CargoDiscardStatisticsService {
|
||||
for (var line : lines) {
|
||||
if (line.getSkuCode() != null && !line.getSkuCode().isBlank()) skus.add(line.getSkuCode());
|
||||
if (line.getReturnCartonId() == null) {
|
||||
piecesKnown = false;
|
||||
if (line.getActualDiscardPackages() != null) actualPiecesKnown = false;
|
||||
var selectedPieces = dimensions.pieces(application.getCustomerId(), line);
|
||||
if (selectedPieces == null) piecesKnown = false;
|
||||
else pieces += selectedPieces;
|
||||
if (line.getActualDiscardPackages() != null) {
|
||||
if (line.getActualDiscardPackages() == 0) { /* 实际未废弃商品为零,不推测箱规。 */ }
|
||||
else if (selectedPieces != null && line.getActualDiscardPackages() == line.getDiscardPackages()) actualPieces += selectedPieces;
|
||||
else actualPiecesKnown = false;
|
||||
}
|
||||
} else {
|
||||
pieces += line.getDiscardPackages();
|
||||
if (line.getActualDiscardPackages() != null) actualPieces += line.getActualDiscardPackages();
|
||||
|
||||
+143
-41
@@ -2,11 +2,9 @@ package com.baiyue.lastleg.discard.service;
|
||||
|
||||
import com.baiyue.lastleg.discard.dto.*;
|
||||
import com.baiyue.lastleg.discard.entity.*;
|
||||
import com.baiyue.lastleg.discard.mapper.*;
|
||||
import com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests;
|
||||
import com.baiyue.lastleg.inventory.command.service.InventoryCommandService;
|
||||
import com.baiyue.lastleg.inventory.entity.*;
|
||||
import com.baiyue.lastleg.inventory.mapper.*;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -16,129 +14,233 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.util.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/** Stock eligibility is based on physical available positions, independent of the source document type. */
|
||||
/** 根据实际可用仓位校验废弃库存,精确选箱与无对应关系实点分别结算。 */
|
||||
@Service
|
||||
public class CargoDiscardStockService {
|
||||
@Resource private CargoDiscardInventoryMapper stock;
|
||||
@Resource private InventoryPositionMapper positions;
|
||||
@Resource private InventoryBalanceMapper balances;
|
||||
@Resource private com.baiyue.lastleg.discard.repository.CargoDiscardStockRepository repository;
|
||||
@Resource private InventoryCommandService commands;
|
||||
@Resource private CargoDiscardItemMapper items;
|
||||
@Resource private CargoDiscardCartonMapper links;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
/** 展示可用库存及相关托盘,箱唛完整时才提供逐箱选择。 */
|
||||
public List<CargoDiscardResponses.Candidate> candidates(String customer, String keyword) {
|
||||
return stock.genericPositions(customer, keyword).stream().map(position -> {
|
||||
var balance = balances.selectById(position.getBalanceId());
|
||||
return repository.candidates(customer, keyword).stream().map(position -> {
|
||||
var balance = repository.balance(position.getBalanceId());
|
||||
if (balance == null || !customer.equals(balance.getCustomerId())) throw conflict("库存余额不存在");
|
||||
long available = Math.min(value(position.getAvailablePackages()), value(balance.getAvailablePackages()));
|
||||
var cartons = stock.genericCartons(customer, position.getId());
|
||||
boolean complete = cartons.size() == available && cartons.stream().allMatch(c -> c.getCartonMark() != null && !c.getCartonMark().isBlank())
|
||||
&& cartons.stream().map(c -> c.getCartonMark()).distinct().count() == cartons.size();
|
||||
var cartons = repository.cartons(customer, position.getId());
|
||||
boolean complete = completeCartons(cartons, available);
|
||||
var boxes = complete ? cartons.stream().map(c -> new CargoDiscardResponses.Box(c.getId(), c.getCartonMark(), position.getPalletNo(),
|
||||
List.of(new CargoDiscardResponses.CandidateSku(c.getSkuCode(), balance.getProductNameZh(), 1)))).toList() : List.<CargoDiscardResponses.Box>of();
|
||||
contents(c, balance.getProductNameZh()))).toList() : List.<CargoDiscardResponses.Box>of();
|
||||
return new CargoDiscardResponses.Candidate(null, "INVENTORY", null, null,
|
||||
balance.getInboundCode(), balance.getInboundOrderNo(), balance.getOverseasWarehouseId(),
|
||||
balance.getOverseasWarehouseName(), position.getCreatedAt(), available,
|
||||
List.of(new CargoDiscardResponses.CandidateSku(sku(balance), balance.getProductNameZh(), available)),
|
||||
complete ? summarizeSkus(cartons, balance) : List.of(new CargoDiscardResponses.CandidateSku(sku(balance), balance.getProductNameZh(), available)),
|
||||
position.getId(), position.getPalletNo(), boxes, complete,
|
||||
complete ? null : "当前库存缺少完整箱唛与托盘对应记录,可按全部库存废弃");
|
||||
complete ? null : "缺少完整箱唛与托盘对应记录,将预留所选库存,由仓库核对相关托盘并填写实际废弃箱数,未废弃部分释放");
|
||||
}).filter(c -> c.availablePackages() > 0).toList();
|
||||
}
|
||||
|
||||
/** 校验客户、仓库、SKU、预留上限与具体箱子的归属。 */
|
||||
public InventoryBalance validate(String customer, CargoDiscardRequests.Item request) {
|
||||
var position = position(customer, request.inventoryPositionId());
|
||||
var balance = balances.selectById(position.getBalanceId());
|
||||
var balance = repository.balance(position.getBalanceId());
|
||||
if (balance == null || !customer.equals(balance.getCustomerId())
|
||||
|| !Objects.equals(position.getOverseasWarehouseId(), balance.getOverseasWarehouseId()))
|
||||
throw conflict("库存不属于当前客户或仓库");
|
||||
if (!sku(balance).equals(request.skuCode().trim())) throw conflict("SKU 不属于所选库存");
|
||||
long available = Math.min(value(position.getAvailablePackages()), value(balance.getAvailablePackages()));
|
||||
if (available <= 0 || request.discardPackages() <= 0 || request.discardPackages() > available) throw conflict("所选废弃库存数量不足");
|
||||
if (request.cartonIds() != null) {
|
||||
if (request.cartonIds().size() != request.discardPackages() || new HashSet<>(request.cartonIds()).size() != request.cartonIds().size())
|
||||
throw conflict("废弃箱子数量与所选箱子不一致或重复");
|
||||
var cartons = stock.genericCartons(customer, position.getId());
|
||||
var cartons = repository.cartons(customer, position.getId());
|
||||
var ids = cartons.stream().map(c -> c.getId()).collect(java.util.stream.Collectors.toSet());
|
||||
if (cartons.size() != available || cartons.stream().anyMatch(c -> c.getCartonMark() == null || c.getCartonMark().isBlank()) || cartons.stream().map(c -> c.getCartonMark()).distinct().count() != cartons.size() || !ids.containsAll(request.cartonIds())) throw conflict("箱唛库存已变化或不属于所选托盘,请刷新后重选");
|
||||
} else if (request.discardPackages() != available) throw conflict("部分废弃必须选择具体箱子,不能只填写数量");
|
||||
if (stock.isDropshipPosition(customer, position.getId()) > 0) throw conflict("一件代发库存必须按真实箱唛选择,不能绕过箱级预留");
|
||||
if (cartons.stream().filter(c -> request.cartonIds().contains(c.getId())).anyMatch(c -> contents(c, null).stream().noneMatch(s -> request.skuCode().trim().equals(s.skuCode())))) throw conflict("SKU 不属于所选箱子");
|
||||
} else {
|
||||
if (!sku(balance).equals(request.skuCode().trim())) throw conflict("SKU 不属于所选库存");
|
||||
if (request.discardPackages() != available) throw conflict("部分废弃必须选择具体箱子,不能只填写数量");
|
||||
}
|
||||
if (repository.dropshipPosition(customer, position.getId())) throw conflict("一件代发库存必须按真实箱唛选择,不能绕过箱级预留");
|
||||
return balance;
|
||||
}
|
||||
|
||||
/** 预留所选实收箱并保存箱号快照,转运箱从已清点状态进入待废弃。 */
|
||||
public void reserve(String customer, String operator, CargoDiscardApplication application, CargoDiscardRequests.Item request, LocalDateTime at) {
|
||||
if (stock.lockGenericPosition(customer, request.inventoryPositionId()) == null) throw conflict("库存仓位不存在");
|
||||
if (repository.lockPosition(customer, request.inventoryPositionId()) == null) throw conflict("库存仓位不存在");
|
||||
var balance = validate(customer, request);
|
||||
reserveValidated(customer, operator, application, request, at, balance, repository.cartons(customer, request.inventoryPositionId()), new HashSet<>(), new HashSet<>());
|
||||
}
|
||||
|
||||
/** 整箱混装必须包含全部 SKU 仓位;先锁仓位并校验全部事实,再按实物箱仅锁定一次。 */
|
||||
public void reserveAll(String customer, String operator, CargoDiscardApplication application, List<CargoDiscardRequests.Item> requests, LocalDateTime at) {
|
||||
var stocks = requests.stream().filter(r -> r.inventoryPositionId() != null).toList();
|
||||
stocks.stream().map(CargoDiscardRequests.Item::inventoryPositionId).distinct().sorted().forEach(id -> {
|
||||
if (repository.lockPosition(customer, id) == null) throw conflict("库存仓位不存在");
|
||||
});
|
||||
var balances = new HashMap<Long, InventoryBalance>();
|
||||
var cartons = new HashMap<Long, List<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow>>();
|
||||
var selectedSkus = new HashMap<String, Set<String>>();
|
||||
for (var request : stocks) {
|
||||
balances.put(request.inventoryPositionId(), validate(customer, request));
|
||||
var facts = repository.cartons(customer, request.inventoryPositionId());
|
||||
cartons.put(request.inventoryPositionId(), facts);
|
||||
for (var carton : facts) if (request.cartonIds() == null || request.cartonIds().contains(carton.getId()))
|
||||
selectedSkus.computeIfAbsent(carton.getSourceType() + ":" + carton.getId(), k -> new HashSet<>()).add(request.skuCode().trim());
|
||||
}
|
||||
for (var request : stocks) for (var carton : cartons.get(request.inventoryPositionId())) {
|
||||
if (request.cartonIds() != null && !request.cartonIds().contains(carton.getId())) continue;
|
||||
var required = contents(carton, null).stream().map(CargoDiscardResponses.CandidateSku::skuCode).collect(java.util.stream.Collectors.toSet());
|
||||
if (required.size() > 1 && request.cartonIds() == null) throw conflict("混装箱必须选择具体箱子及箱内全部 SKU 库存");
|
||||
if (!selectedSkus.get(carton.getSourceType() + ":" + carton.getId()).containsAll(required)) throw conflict("混装箱必须选择箱内全部 SKU 库存");
|
||||
}
|
||||
var lockedBoxes = new HashSet<String>();
|
||||
var heldBoxes = new HashSet<String>();
|
||||
for (var request : stocks) reserveValidated(customer, operator, application, request, at,
|
||||
balances.get(request.inventoryPositionId()), cartons.get(request.inventoryPositionId()), lockedBoxes, heldBoxes);
|
||||
}
|
||||
|
||||
/** 使用本事务变更箱状态前的完整事实,为每个 SKU 分别预留并保存相同箱唛快照。 */
|
||||
private void reserveValidated(String customer, String operator, CargoDiscardApplication application, CargoDiscardRequests.Item request, LocalDateTime at,
|
||||
InventoryBalance balance, List<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow> availableCartons, Set<String> lockedBoxes, Set<String> heldBoxes) {
|
||||
var position = position(customer, request.inventoryPositionId());
|
||||
var line = new CargoDiscardItem(); line.setId(IdWorker.getId()); line.setApplicationId(application.getId());
|
||||
line.setCustomerId(customer); line.setSourceType("INVENTORY"); line.setInventoryPositionId(position.getId());
|
||||
line.setInboundCode(balance.getInboundCode()); line.setSkuCode(sku(balance)); line.setProductName(balance.getProductNameZh());
|
||||
line.setAvailablePackagesSnapshot(Math.toIntExact(Math.min(value(position.getAvailablePackages()), value(balance.getAvailablePackages())))); line.setDiscardPackages(request.discardPackages()); line.setCreatedAt(at);
|
||||
var availableCartons = stock.genericCartons(customer, position.getId());
|
||||
line.setInboundCode(balance.getInboundCode()); line.setSkuCode(request.skuCode().trim()); line.setProductName(sku(balance).equals(line.getSkuCode()) ? balance.getProductNameZh() : null);
|
||||
line.setAvailablePackagesSnapshot(availableCartons.isEmpty() ? Math.toIntExact(Math.min(value(position.getAvailablePackages()), value(balance.getAvailablePackages()))) : availableCartons.size()); line.setDiscardPackages(request.discardPackages()); line.setCreatedAt(at);
|
||||
var selected = request.cartonIds() == null
|
||||
? (availableCartons.size() == request.discardPackages() ? availableCartons : List.<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow>of())
|
||||
? (completeCartons(availableCartons, request.discardPackages()) ? availableCartons : List.<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow>of())
|
||||
: availableCartons.stream().filter(c -> request.cartonIds().contains(c.getId())).toList();
|
||||
if (!selected.isEmpty()) {
|
||||
for (var carton : selected) {
|
||||
if (!lockedBoxes.add(carton.getSourceType() + ":" + carton.getId())) continue;
|
||||
if (changeBox(customer, carton.getId(), carton.getSourceType(), "TRANSFER".equals(carton.getSourceType()) ? "COUNTED" : "AVAILABLE", "DISCARD_PENDING") != 1) throw conflict("箱子已被其他作业占用,请刷新后重选");
|
||||
var link = new CargoDiscardCarton(); link.setId(IdWorker.getId()); link.setCustomerId(customer); link.setApplicationId(application.getId());
|
||||
link.setItemId(line.getId()); link.setSourceType(carton.getSourceType()); link.setCartonId(carton.getId()); link.setCartonMark(carton.getCartonMark()); link.setCreatedAt(at); links.insert(link);
|
||||
link.setItemId(line.getId()); link.setSourceType(carton.getSourceType()); link.setCartonId(carton.getId()); link.setCartonMark(carton.getCartonMark()); link.setCreatedAt(at); repository.saveCarton(link);
|
||||
}
|
||||
snapshotMarks(line, selected.stream().map(c -> c.getCartonMark()).toList());
|
||||
line.setCartonScanRequired(request.discardPackages() < value(position.getAvailablePackages()) + value(position.getHoldPackages()) + value(position.getAllocatedPackages()) + value(position.getPickedPackages()));
|
||||
} else if (position.getPalletNo() == null || position.getPalletNo().isBlank()) {
|
||||
var marks = stock.looseCartonMarks(customer, position.getId());
|
||||
var marks = repository.looseMarks(customer, position.getId());
|
||||
if (marks.size() != request.discardPackages() || marks.stream().distinct().count() != marks.size()) throw conflict("散箱库存缺少完整箱唛,请先核对实物箱级入库记录");
|
||||
snapshotMarks(line, marks);
|
||||
}
|
||||
commands.hold(customer, operator, move(application, line, request.discardPackages(), "HOLD"));
|
||||
items.insert(line);
|
||||
int physical = selected.isEmpty() ? request.discardPackages() : (int)selected.stream().filter(c -> heldBoxes.add(position.getId() + ":" + c.getSourceType() + ":" + c.getId())).count();
|
||||
if (physical > 0) commands.hold(customer, operator, move(application, line, physical, "HOLD"));
|
||||
repository.saveItem(line);
|
||||
}
|
||||
|
||||
/** 同一仓位可能承载混装箱,完整 SKU 候选来自实箱而不是库存余额的首 SKU。 */
|
||||
private List<CargoDiscardResponses.CandidateSku> summarizeSkus(List<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow> cartons, InventoryBalance balance) {
|
||||
var boxes = new LinkedHashMap<String, Long>(); var pieces = new HashMap<String, Long>(); var unknown = new HashSet<String>();
|
||||
for (var carton : cartons) for (var content : contents(carton, null)) {
|
||||
boxes.merge(content.skuCode(), 1L, Math::addExact);
|
||||
if (content.availablePieces() == null) unknown.add(content.skuCode()); else pieces.merge(content.skuCode(), content.availablePieces(), Math::addExact);
|
||||
}
|
||||
return boxes.entrySet().stream().map(e -> new CargoDiscardResponses.CandidateSku(e.getKey(), sku(balance).equals(e.getKey()) ? balance.getProductNameZh() : null,
|
||||
e.getValue(), unknown.contains(e.getKey()) ? null : pieces.get(e.getKey()))).toList();
|
||||
}
|
||||
|
||||
/** 箱数、箱唛和唯一性均完整时,才允许将库存对应到具体箱子。 */
|
||||
private boolean completeCartons(List<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow> cartons, long count) {
|
||||
return cartons.size() == count && cartons.stream().allMatch(c -> c.getCartonMark() != null && !c.getCartonMark().isBlank())
|
||||
&& cartons.stream().map(c -> c.getCartonMark()).distinct().count() == cartons.size();
|
||||
}
|
||||
/** 转运实收 JSON 提供完整 SKU 与件数;无 JSON 的单 SKU 来源保留箱数单位。 */
|
||||
private List<CargoDiscardResponses.CandidateSku> contents(com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow carton, String productName) {
|
||||
if (carton.getSkuItemsJson() == null || carton.getSkuItemsJson().isBlank())
|
||||
return List.of(new CargoDiscardResponses.CandidateSku(carton.getSkuCode(), productName, 1));
|
||||
try {
|
||||
var values = objectMapper.readTree(carton.getSkuItemsJson());
|
||||
if (!values.isArray()) throw conflict("箱内 SKU 事实不完整");
|
||||
var quantities = new LinkedHashMap<String, Long>();
|
||||
for (var value : values) {
|
||||
String code = value.path("skuCode").asText().trim(); long pieces = value.path("quantity").asLong();
|
||||
if (code.isEmpty() || pieces <= 0) throw conflict("箱内 SKU 事实不完整");
|
||||
quantities.merge(code, pieces, Math::addExact);
|
||||
}
|
||||
if (quantities.isEmpty()) throw conflict("箱内 SKU 事实不完整");
|
||||
return quantities.entrySet().stream().map(e -> new CargoDiscardResponses.CandidateSku(e.getKey(), Objects.equals(e.getKey(), carton.getSkuCode()) ? productName : null, 1, e.getValue())).toList();
|
||||
} catch (com.fasterxml.jackson.core.JsonProcessingException e) { throw conflict("箱内 SKU 事实无法读取"); }
|
||||
}
|
||||
|
||||
/** 驳回或取消时恢复所选箱状态并释放本次冻结数量。 */
|
||||
public void release(String customer, CargoDiscardApplication application, CargoDiscardItem line) {
|
||||
changeSelectedCartons(customer, application, line, "AVAILABLE");
|
||||
commands.release(customer, application.getCreatedBy(), move(application, line, line.getDiscardPackages(), "RELEASE"));
|
||||
int physical = physicalPackages(customer, application, line);
|
||||
if (physical > 0) commands.release(customer, application.getCreatedBy(), move(application, line, physical, "RELEASE"));
|
||||
}
|
||||
|
||||
/** 无逐箱对应关系时按仓库实点扣冻结库存,未废弃余量在同一作业事务释放。 */
|
||||
@org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY)
|
||||
public void discard(String customer, String operator, CargoDiscardApplication application, CargoDiscardItem line, int actual) {
|
||||
if (actual != line.getDiscardPackages()) throw conflict("必须完成本次申请所选全部箱子,不能漏出");
|
||||
changeSelectedCartons(customer, application, line, "DISCARDED");
|
||||
commands.discardHeld(customer, operator, move(application, line, actual, "DISCARD"));
|
||||
if (actual < 0 || actual > line.getDiscardPackages()) throw conflict("实际废弃箱数不能超过申请预留箱数");
|
||||
if (!actualQuantityEditable(line) && actual != line.getDiscardPackages())
|
||||
throw conflict("必须完成本次申请所选全部箱子,不能漏出");
|
||||
// 已选择具体箱子的作业仍按箱唛结算,不能按数量猜测哪些箱已被废弃。
|
||||
if (!actualQuantityEditable(line)) changeSelectedCartons(customer, application, line, "DISCARDED");
|
||||
int physical = actualQuantityEditable(line) ? actual : physicalPackages(customer, application, line);
|
||||
if (physical > 0) commands.discardHeld(customer, operator, move(application, line, physical, "DISCARD"));
|
||||
int remaining = line.getDiscardPackages() - actual;
|
||||
if (remaining > 0) commands.release(customer, operator, move(application, line, remaining, "ACTUAL_RELEASE"));
|
||||
}
|
||||
|
||||
private int changeBox(String customer, Long cartonId, String source, String from, String to) {
|
||||
if ("TRANSFER".equals(source)) return stock.changeTransferCartonStatus(customer, cartonId, from, to);
|
||||
if (!"INBOUND".equals(source)) throw conflict("箱子库存来源不正确");
|
||||
return stock.changeGenericCartonStatus(customer, cartonId, from, to);
|
||||
/** 同仓位同箱只由首个 SKU 明细执行库存命令;其他 SKU 保存件数事实,不重复扣物理箱桶。 */
|
||||
private int physicalPackages(String customer, CargoDiscardApplication application, CargoDiscardItem line) {
|
||||
var marks = cartonMarks(line);
|
||||
if (marks.isEmpty()) return line.getDiscardPackages();
|
||||
var previous = new HashSet<String>();
|
||||
for (var other : repository.applicationItems(customer, application.getId())) {
|
||||
if (Objects.equals(other.getInventoryPositionId(), line.getInventoryPositionId()) && other.getId() != null && line.getId() != null && other.getId() < line.getId()) previous.addAll(cartonMarks(other));
|
||||
}
|
||||
return (int)marks.stream().filter(mark -> !previous.contains(mark)).count();
|
||||
}
|
||||
|
||||
/** 只有未保存具体箱唛的普通仓位库存允许仓库按实点箱数完成。 */
|
||||
public boolean actualQuantityEditable(CargoDiscardItem line) {
|
||||
return "INVENTORY".equals(line.getSourceType()) && line.getInventoryPositionId() != null
|
||||
&& cartonMarks(line).isEmpty() && !Boolean.TRUE.equals(line.getCartonScanRequired());
|
||||
}
|
||||
|
||||
/** 按箱来源执行带原状态条件的变更。 */
|
||||
private int changeBox(String customer, Long cartonId, String source, String from, String to) {
|
||||
if ("TRANSFER".equals(source)) return repository.changeTransferCarton(customer, cartonId, from, to);
|
||||
if (!"INBOUND".equals(source)) throw conflict("箱子库存来源不正确");
|
||||
return repository.changeInboundCarton(customer, cartonId, from, to);
|
||||
}
|
||||
/** 保存本次选箱事实,后续作业不得按数量猜测箱唛。 */
|
||||
private void snapshotMarks(CargoDiscardItem line, List<String> marks) {
|
||||
try { line.setCartonMarksJson(objectMapper.writeValueAsString(marks)); }
|
||||
catch (Exception e) { throw new IllegalStateException("Cannot snapshot carton marks", e); }
|
||||
}
|
||||
/** 按真实箱来源完成废弃或释放,转运箱释放后恢复已清点状态。 */
|
||||
/** 仅变更当前申请明细保存的箱子,转运箱释放后恢复已清点状态。 */
|
||||
private void changeSelectedCartons(String customer, CargoDiscardApplication application, CargoDiscardItem line, String status) {
|
||||
for (var link : links.listApplication(customer, application.getId())) {
|
||||
for (var link : repository.selectedCartons(customer, application.getId())) {
|
||||
if (Objects.equals(link.getItemId(), line.getId()) && changeBox(customer, link.getCartonId(), link.getSourceType(), "DISCARD_PENDING", "TRANSFER".equals(link.getSourceType()) && "AVAILABLE".equals(status) ? "COUNTED" : status) != 1)
|
||||
throw conflict("预留箱子的库存状态发生变化");
|
||||
}
|
||||
}
|
||||
/** 库存命令使用申请和明细构成的稳定来源与幂等键。 */
|
||||
private InventoryCommandRequests.Move move(CargoDiscardApplication application, CargoDiscardItem line, int count, String action) {
|
||||
String cycle = "CARGO_DISCARD:" + application.getId() + ":" + line.getId();
|
||||
return new InventoryCommandRequests.Move(line.getInventoryPositionId(), count, application.getDiscardNo(),
|
||||
null, cycle, cycle + ":" + action, "货物废弃 " + action);
|
||||
}
|
||||
/** 读取并核对客户指定库存仓位。 */
|
||||
public InventoryPosition position(String customer, Long id) {
|
||||
var position = positions.findTenantPosition(customer, id);
|
||||
var position = repository.position(customer, id);
|
||||
if (position == null) throw conflict("废弃库存仓位不存在或不属于当前客户");
|
||||
return position;
|
||||
}
|
||||
/** 只解析申请保存的箱唛,缺失对应关系保留为空。 */
|
||||
public List<String> cartonMarks(CargoDiscardItem line) {
|
||||
if (line.getCartonMarksJson() == null) return List.of();
|
||||
try { return objectMapper.readValue(line.getCartonMarksJson(), new com.fasterxml.jackson.core.type.TypeReference<List<String>>() {}); }
|
||||
catch (Exception e) { throw new IllegalStateException("Cannot read carton marks", e); }
|
||||
}
|
||||
/** 无 SKU 来源保留未指定标记,不猜测商品。 */
|
||||
private static String sku(InventoryBalance balance) { return balance.getSkuCode() == null || balance.getSkuCode().isBlank() ? "UNSPECIFIED" : balance.getSkuCode(); }
|
||||
/** 空库存桶按零处理。 */
|
||||
private static long value(Long value) { return value == null ? 0 : value; }
|
||||
/** 返回可恢复的业务冲突。 */
|
||||
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user