废弃 ,换标文件上传 ,排车托盘锁粒度

This commit is contained in:
2026-09-14 16:49:00 +08:00
parent 60ebc2e376
commit 78bbd69a26
23 changed files with 443 additions and 21 deletions
@@ -60,6 +60,20 @@ public class CargoDiscardController {
return ApiResponse.success(service.review(scope(user, customerId), discardNo, operator(user), request));
}
@PostMapping("/{discardNo}/cancel")
public ApiResponse<CargoDiscardResponses.Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
requireApprovalPermission(user);
return ApiResponse.success(service.cancel(scope(user, customerId), discardNo, operator(user), true));
}
@PostMapping("/{discardNo}/complete")
public ApiResponse<CargoDiscardResponses.Detail> complete(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
requireApprovalPermission(user);
return ApiResponse.success(service.complete(scope(user, customerId), discardNo, operator(user)));
}
private String scope(AuthenticatedUser user, String requested) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) && user.tenantId() == 0L) {
@@ -30,6 +30,9 @@ public class MerchantCargoDiscardController {
@GetMapping("/{discardNo}")
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String discardNo) { return ApiResponse.success(service.detail(customer(user), discardNo)); }
@PostMapping("/{discardNo}/cancel")
public ApiResponse<CargoDiscardResponses.Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String discardNo) { return ApiResponse.success(service.cancel(customer(user), discardNo, operator(user), false)); }
private String customer(AuthenticatedUser user) {
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
@@ -21,5 +21,9 @@ public class CargoDiscardItem {
private String productName;
private Integer availablePackagesSnapshot;
private Integer discardPackages;
private Integer actualDiscardPackages;
private String pdaStatus;
private String pdaOperatorId;
private LocalDateTime pdaCompletedAt;
private LocalDateTime createdAt;
}
@@ -16,4 +16,7 @@ public interface CargoDiscardApplicationMapper extends BaseMapper<CargoDiscardAp
@Select("SELECT * FROM ll_cargo_discard_application WHERE customer_id=#{customerId} AND discard_no=#{discardNo} LIMIT 1 FOR UPDATE")
CargoDiscardApplication lockNo(@Param("customerId") String customerId, @Param("discardNo") String discardNo);
@Select("SELECT * FROM ll_cargo_discard_application WHERE discard_no=#{discardNo} AND overseas_warehouse_id=#{warehouseId} LIMIT 1 FOR UPDATE")
CargoDiscardApplication findNoByWarehouse(@Param("discardNo") String discardNo, @Param("warehouseId") String warehouseId);
}
@@ -0,0 +1,32 @@
package com.baiyue.lastleg.discard.pda.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.discard.pda.dto.PdaCargoDiscardDtos;
import com.baiyue.lastleg.discard.pda.service.PdaCargoDiscardService;
import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/pda/v1/last-leg/cargo-discards")
@PreAuthorize("hasAuthority('lastleg:pda:operate') and hasAuthority('ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR')")
public class PdaCargoDiscardController {
@Resource private PdaCargoDiscardService service;
@GetMapping public ApiResponse<List<PdaCargoDiscardDtos.PalletTask>> tasks(@AuthenticationPrincipal AuthenticatedUser user) {
return ApiResponse.success(service.tasks(PdaWarehouseIdentity.require(user)));
}
@PostMapping("/resolve") public ApiResponse<PdaCargoDiscardDtos.PalletTask> resolve(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody PdaCargoDiscardDtos.Scan request) {
return ApiResponse.success(service.resolve(PdaWarehouseIdentity.require(user), request.palletNo()));
}
@PostMapping("/{discardNo}/pallets/{palletNo}/confirm")
public ApiResponse<PdaCargoDiscardDtos.PalletTask> confirm(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable String discardNo, @PathVariable String palletNo, @Valid @RequestBody PdaCargoDiscardDtos.Confirm request) {
return ApiResponse.success(service.confirm(PdaWarehouseIdentity.require(user), discardNo, palletNo, request));
}
}
@@ -0,0 +1,20 @@
package com.baiyue.lastleg.discard.pda.dto;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
import java.util.List;
public final class PdaCargoDiscardDtos {
private PdaCargoDiscardDtos() { }
public record Scan(@NotBlank @Size(max = 128) String palletNo) { }
public record ActualLine(@NotNull Long itemId, @PositiveOrZero int actualPackages) { }
public record Confirm(@NotEmpty List<@Valid ActualLine> lines) { }
public record Sku(Long itemId, String skuCode, String productName, int plannedPackages,
Integer actualPackages, String status) { }
public record PalletTask(String discardNo, String customerId, String overseasWarehouseId,
String palletNo, Long profileId, String status, List<Sku> skus) { }
}
@@ -0,0 +1,99 @@
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 jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
public class PdaCargoDiscardService {
@Resource private CargoDiscardApplicationMapper applications;
@Resource private CargoDiscardItemMapper items;
@Resource private CargoDiscardCartonMapper cartons;
@Resource private DropshipOrderProfileMapper profiles;
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private CargoDiscardService discardService;
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))
.stream().flatMap(a -> palletTasks(a).stream()).toList();
}
public PdaCargoDiscardDtos.PalletTask resolve(PdaWarehouseIdentity identity, String palletNo) {
return tasks(identity).stream().filter(t -> t.palletNo().equals(palletNo.trim())).findFirst()
.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,
String.valueOf(identity.overseasWarehouseId()));
if (application == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "废弃任务不存在或不属于当前仓库");
if (!CargoDiscardService.APPROVED.equals(application.getStatus())) throw conflict("废弃任务当前不可进行 PDA 作业");
PdaCargoDiscardDtos.PalletTask task = palletTasks(application).stream().filter(t -> t.palletNo().equals(palletNo.trim()))
.findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "托盘不属于该废弃单"));
Map<Long,Integer> actuals = request.lines().stream().collect(Collectors.toMap(PdaCargoDiscardDtos.ActualLine::itemId,
PdaCargoDiscardDtos.ActualLine::actualPackages, (a,b) -> { throw conflict("PDA 明细重复"); }));
if (!actuals.keySet().equals(task.skus().stream().map(PdaCargoDiscardDtos.Sku::itemId).collect(Collectors.toSet()))) {
throw conflict("必须提交该托盘全部 SKU 的实际废弃数量");
}
LocalDateTime now = LocalDateTime.now();
for (PdaCargoDiscardDtos.Sku line : task.skus()) {
CargoDiscardItem item = items.selectById(line.itemId());
if ("COMPLETED".equals(item.getPdaStatus())) {
if (!Objects.equals(item.getActualDiscardPackages(), actuals.get(item.getId()))) throw conflict("已完成托盘的实际数量不能修改");
continue;
}
List<CargoDiscardCarton> links = cartons.listApplication(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);
item.setActualDiscardPackages(actuals.get(item.getId())); item.setPdaStatus("COMPLETED");
item.setPdaOperatorId(identity.operatorKey()); item.setPdaCompletedAt(now); items.updateById(item);
}
if (items.listApplication(application.getCustomerId(), application.getId()).stream()
.allMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
int actualTotal = items.listApplication(application.getCustomerId(), application.getId()).stream()
.mapToInt(i -> i.getActualDiscardPackages() == null ? 0 : i.getActualDiscardPackages()).sum();
application.setTotalPackages(actualTotal);
application.setTotalFee(application.getFeePerPackage().multiply(java.math.BigDecimal.valueOf(actualTotal)));
discardService.updateLinkedStatus(application.getCustomerId(), application, CargoDiscardService.APPROVED,
CargoDiscardService.PDA_COMPLETED, identity.operatorKey(), "全部托盘 PDA 废弃作业完成,待仓库确认");
}
return task;
}
private List<PdaCargoDiscardDtos.PalletTask> palletTasks(CargoDiscardApplication application) {
return items.listApplication(application.getCustomerId(), application.getId()).stream()
.collect(Collectors.groupingBy(CargoDiscardItem::getProfileId, LinkedHashMap::new, Collectors.toList()))
.entrySet().stream().map(entry -> {
DropshipOrderProfile profile = profiles.selectById(entry.getKey());
if (profile == null || !application.getCustomerId().equals(profile.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);
}).toList();
}
private static ResponseStatusException conflict(String message) { return new ResponseStatusException(HttpStatus.CONFLICT, message); }
}
@@ -9,6 +9,9 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipCartonLedger;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
import com.baiyue.lastleg.dropship.mapper.DropshipCartonLedgerMapper;
import com.baiyue.lastleg.dropship.mapper.DropshipOrderProfileMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper;
import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper;
import jakarta.annotation.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
@@ -25,6 +28,42 @@ import java.util.Map;
public class CargoDiscardInventoryAdjustmentService {
@Resource private CargoDiscardInventoryMapper inventory;
@Resource private DropshipCartonLedgerMapper cartonLedgers;
@Resource private DropshipOrderProfileMapper profiles;
@Resource private InventoryPositionMapper positions;
@Resource private InventoryBalanceMapper balances;
@Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discardPalletItem(String customerId, CargoDiscardApplication application,
CargoDiscardItem item, List<CargoDiscardCarton> links,
int actualPackages, String operatorId, LocalDateTime occurredAt) {
if (actualPackages < 0 || actualPackages > links.size()) throw conflict("实际废弃数量超出该托盘申请数量");
DropshipSkuInventory sku = inventory.lockSkuInventory(customerId, item.getSkuInventoryId());
if (sku == null) throw conflict("托盘 SKU 库存不存在: " + item.getSkuCode());
long before = value(sku.getPackageCount());
if (before < actualPackages || inventory.deductSkuInventory(customerId, sku.getId(), actualPackages) != 1) {
throw conflict("托盘 SKU 库存不足: " + item.getSkuCode());
}
var profile = profiles.selectById(item.getProfileId());
var position = profile == null || profile.getInventoryPositionId() == null ? null
: positions.findTenantPosition(customerId, profile.getInventoryPositionId());
if (profile == null || !customerId.equals(profile.getCustomerId()) || position == null
|| positions.deductAvailable(customerId, profile.getInventoryPositionId(), actualPackages) != 1
|| balances.deductAvailable(customerId, position.getBalanceId(), actualPackages) != 1) {
throw conflict("托盘库存明细数量不足或托盘位置不存在");
}
for (int index = 0; index < links.size(); index++) {
CargoDiscardCarton link = links.get(index);
String target = index < actualPackages ? "DISCARDED" : "AVAILABLE";
DropshipCarton carton = inventory.lockCarton(customerId, link.getCartonId());
if (carton == null || inventory.changeCartonStatus(customerId, carton.getId(), "DISCARD_PENDING", target) != 1) {
throw conflict("托盘预留箱状态已变化");
}
if (index < actualPackages) cartonLedgers.insert(discardFact(customerId, application, item, carton, operatorId, occurredAt));
}
long after = value(inventory.skuPackages(customerId, sku.getId()));
if (after != before - actualPackages) throw conflict("托盘 SKU 库存扣减不守恒");
return new AdjustmentResult(before, actualPackages, after);
}
@Transactional(propagation = Propagation.MANDATORY)
public AdjustmentResult discard(String customerId, CargoDiscardApplication application,
@@ -19,7 +19,9 @@ import com.baiyue.lastleg.discard.model.CargoDiscardCandidateRow;
import com.baiyue.lastleg.dropship.entity.DropshipCarton;
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
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.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
@@ -49,7 +51,10 @@ public class CargoDiscardService {
public static final String PENDING_REVIEW = "PENDING_REVIEW";
public static final String APPROVED = "APPROVED";
public static final String REJECTED = "REJECTED";
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED);
public static final String CANCELLED = "CANCELLED";
public static final String PDA_COMPLETED = "PDA_COMPLETED";
public static final String COMPLETED = "COMPLETED";
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, APPROVED, REJECTED, CANCELLED, PDA_COMPLETED, COMPLETED);
@Resource private CargoDiscardApplicationMapper applications;
@Resource private CargoDiscardItemMapper items;
@@ -58,6 +63,7 @@ public class CargoDiscardService {
@Resource private CargoDiscardInventoryMapper inventory;
@Resource private CargoDiscardInventoryAdjustmentService adjustments;
@Resource private LastlegOrderMapper orders;
@Resource private LastlegOrderStatusHistoryMapper orderHistories;
@Resource private ObjectMapper objectMapper;
@Value("${baiyue.lastleg.cargo-discard.fee-per-package-usd:5.00}")
private BigDecimal feePerPackage = new BigDecimal("5.00");
@@ -118,6 +124,37 @@ public class CargoDiscardService {
application.setCreatedAt(now); application.setUpdatedAt(now);
applications.insert(application);
// A discard request is also a customer-visible tail-leg order. Keep the
// application as the workflow record and link the order through its
// immutable source id so cancellation/status changes stay traceable.
LastlegOrder tailOrder = new LastlegOrder();
tailOrder.setId(IdWorker.getId());
tailOrder.setOrderNo(application.getDiscardNo());
tailOrder.setCustomerId(customerId);
tailOrder.setOrderType("CARGO_DISCARD");
tailOrder.setInboundCode(application.getInboundCodes());
tailOrder.setInboundOrderNo(application.getDiscardNo());
tailOrder.setSourceRegion("LASTLEG_DISCARD");
tailOrder.setSourceInboundId("CARGO_DISCARD:" + application.getId());
tailOrder.setSourceOrderId(String.valueOf(application.getId()));
tailOrder.setSourceOrderNo(application.getDiscardNo());
tailOrder.setOverseasWarehouseId(warehouseId);
tailOrder.setOverseasWarehouseName(warehouseName);
tailOrder.setPackageCount(total);
tailOrder.setCurrentStatus(PENDING_REVIEW);
tailOrder.setVersion(0);
tailOrder.setCreatedAt(now);
tailOrder.setUpdatedAt(now);
tailOrder.setSourceEventId("CARGO_DISCARD:" + application.getId());
tailOrder.setSourceLoadingPlanId("CARGO_DISCARD");
tailOrder.setSourcePayloadHash(requestHash);
orders.insert(tailOrder);
LastlegOrderStatusHistory initialOrderHistory = new LastlegOrderStatusHistory();
initialOrderHistory.setId(IdWorker.getId()); initialOrderHistory.setCustomerId(customerId); initialOrderHistory.setOrderId(tailOrder.getId());
initialOrderHistory.setFromStatus(null); initialOrderHistory.setToStatus(PENDING_REVIEW);
initialOrderHistory.setEventId("CARGO_DISCARD_CREATE:" + application.getId()); initialOrderHistory.setRemark(trim(request.remark())); initialOrderHistory.setCreatedAt(now);
orderHistories.insert(initialOrderHistory);
for (CargoDiscardRequests.Item input : request.items()) {
reserve(customerId, application, orderCache.get(input.orderId()), input, now);
}
@@ -175,11 +212,7 @@ public class CargoDiscardService {
Map<Long, List<CargoDiscardCarton>> linksByItem = links.stream()
.collect(java.util.stream.Collectors.groupingBy(CargoDiscardCarton::getItemId));
String historyRemark = request.remark();
if (APPROVED.equals(next)) {
CargoDiscardInventoryAdjustmentService.AdjustmentResult result = adjustments.discard(
customerId, application, applicationItems, linksByItem, operatorId, now());
historyRemark = result.auditRemark(request.remark());
} else {
if (REJECTED.equals(next)) {
release(customerId, links);
}
@@ -187,10 +220,74 @@ public class CargoDiscardService {
application.setStatus(next); application.setReviewedBy(operatorId);
application.setReviewRemark(trim(request.remark())); application.setReviewedAt(reviewedAt);
application.setUpdatedAt(reviewedAt); applications.updateById(application);
LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
if (linked != null) {
linked.setCurrentStatus(next); linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(reviewedAt);
orders.updateById(linked);
LastlegOrderStatusHistory orderHistory = new LastlegOrderStatusHistory(); orderHistory.setId(IdWorker.getId());
orderHistory.setCustomerId(customerId); orderHistory.setOrderId(linked.getId()); orderHistory.setFromStatus(PENDING_REVIEW);
orderHistory.setToStatus(next); orderHistory.setEventId("CARGO_DISCARD_REVIEW:" + application.getId()); orderHistory.setRemark(historyRemark); orderHistory.setCreatedAt(reviewedAt);
orderHistories.insert(orderHistory);
}
history(customerId, application.getId(), PENDING_REVIEW, next, operatorId, historyRemark, reviewedAt);
return detail(customerId, discardNo);
}
@Transactional
public CargoDiscardResponses.Detail complete(String customerId, String discardNo, String operatorId) {
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
if (application == null) throw notFound("Cargo discard application not found");
if (COMPLETED.equals(application.getStatus())) return detail(customerId, discardNo);
if (!PDA_COMPLETED.equals(application.getStatus())) throw conflict("全部托盘 PDA 作业完成后才能确认废弃完成");
updateLinkedStatus(customerId, application, PDA_COMPLETED, COMPLETED, operatorId, "仓库确认废弃完成");
return detail(customerId, discardNo);
}
public void updateLinkedStatus(String customerId, CargoDiscardApplication application, String from, String to,
String operatorId, String remark) {
LocalDateTime at = now(); application.setStatus(to); application.setUpdatedAt(at); applications.updateById(application);
LastlegOrder linked = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
if (linked != null) {
linked.setCurrentStatus(to); linked.setPackageCount(application.getTotalPackages());
linked.setVersion((linked.getVersion() == null ? 0 : linked.getVersion()) + 1); linked.setUpdatedAt(at); orders.updateById(linked);
LastlegOrderStatusHistory h = new LastlegOrderStatusHistory(); h.setId(IdWorker.getId()); h.setCustomerId(customerId); h.setOrderId(linked.getId());
h.setFromStatus(from); h.setToStatus(to); h.setEventId("CARGO_DISCARD_STATUS:" + application.getId() + ":" + to); h.setRemark(remark); h.setCreatedAt(at); orderHistories.insert(h);
}
history(customerId, application.getId(), from, to, operatorId, remark, at);
}
@Transactional
public CargoDiscardResponses.Detail cancel(String customerId, String discardNo, String operatorId, boolean platform) {
CargoDiscardApplication application = applications.lockNo(customerId, discardNo);
if (application == null) throw notFound("Cargo discard application not found");
if (CANCELLED.equals(application.getStatus())) return detail(customerId, discardNo);
if (REJECTED.equals(application.getStatus())) throw conflict("已驳回的废弃申请不能取消");
if (PDA_COMPLETED.equals(application.getStatus()) || COMPLETED.equals(application.getStatus())) throw conflict("PDA 已完成库存扣减,废弃申请不能取消");
if (items.listApplication(customerId, application.getId()).stream().anyMatch(i -> "COMPLETED".equals(i.getPdaStatus()))) {
throw conflict("已有托盘完成废弃作业,不能取消申请");
}
if (!platform && !PENDING_REVIEW.equals(application.getStatus())) {
throw conflict("客户只能取消待审核的废弃申请");
}
List<CargoDiscardCarton> links = discardCartons.listApplication(customerId, application.getId());
release(customerId, links);
LocalDateTime at = now();
String from = application.getStatus();
application.setStatus(CANCELLED); application.setReviewedBy(platform ? operatorId : application.getReviewedBy());
application.setReviewRemark(platform ? "平台取消" : application.getReviewRemark());
application.setUpdatedAt(at); applications.updateById(application);
LastlegOrder order = orders.findSource(customerId, "LASTLEG_DISCARD", "CARGO_DISCARD:" + application.getId());
if (order != null) {
order.setCurrentStatus(CANCELLED); order.setVersion((order.getVersion() == null ? 0 : order.getVersion()) + 1); order.setUpdatedAt(at);
orders.updateById(order);
LastlegOrderStatusHistory h = new LastlegOrderStatusHistory(); h.setId(IdWorker.getId()); h.setCustomerId(customerId); h.setOrderId(order.getId());
h.setFromStatus(from); h.setToStatus(CANCELLED); h.setEventId("CARGO_DISCARD_CANCEL:" + application.getId()); h.setRemark("废弃申请已取消"); h.setCreatedAt(at);
orderHistories.insert(h);
}
history(customerId, application.getId(), from, CANCELLED, operatorId, "废弃申请已取消", at);
return detail(customerId, discardNo);
}
private void reserve(String customerId, CargoDiscardApplication application, LastlegOrder order,
CargoDiscardRequests.Item input, LocalDateTime now) {
String skuCode = input.skuCode().trim();