废弃 ,换标文件上传 ,排车托盘锁粒度
This commit is contained in:
+14
@@ -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) {
|
||||
|
||||
+3
@@ -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;
|
||||
}
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+32
@@ -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));
|
||||
}
|
||||
}
|
||||
+20
@@ -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) { }
|
||||
}
|
||||
+99
@@ -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); }
|
||||
}
|
||||
+39
@@ -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,
|
||||
|
||||
+103
-6
@@ -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();
|
||||
|
||||
+3
-2
@@ -54,7 +54,8 @@ public record DispatchPalletCandidate(
|
||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null, null);
|
||||
}
|
||||
public String sourcePalletKey() {
|
||||
// A plan pallet keeps the same lock identity before and after the physical pallet is received.
|
||||
return planPalletId != null ? "PLAN:" + planPalletId : "RECEIVED:" + warehousePalletId;
|
||||
// Once received, the physical pallet is the shared resource even when it contains
|
||||
// allocations from multiple inbound orders/plan-pallet rows.
|
||||
return warehousePalletId != null ? "RECEIVED:" + warehousePalletId : "PLAN:" + planPalletId;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -637,8 +637,9 @@ public class DispatchService {
|
||||
|
||||
private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) {
|
||||
DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey());
|
||||
if (active == null && candidate.warehousePalletId() != null) {
|
||||
active = locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId());
|
||||
if (active == null && candidate.warehousePalletId() != null && candidate.planPalletId() != null) {
|
||||
// Read legacy plan-level locks created before physical-pallet locking was introduced.
|
||||
active = locks.lockActive(customerId, "PLAN:" + candidate.planPalletId());
|
||||
}
|
||||
return active;
|
||||
}
|
||||
|
||||
+5
@@ -64,4 +64,9 @@ public interface InventoryBalanceMapper extends BaseMapper<InventoryBalance> {
|
||||
"<if test='overseasWarehouseId != null and overseasWarehouseId != \"\"'> AND overseas_warehouse_id=#{overseasWarehouseId}</if></script>")
|
||||
InventorySummary summarize(@Param("customerId") String customerId,
|
||||
@Param("overseasWarehouseId") String overseasWarehouseId);
|
||||
|
||||
@Update("UPDATE ll_inventory_balance SET available_packages=available_packages-#{packages},version=version+1,updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{balanceId} AND available_packages>=#{packages}")
|
||||
int deductAvailable(@Param("customerId") String customerId, @Param("balanceId") Long balanceId,
|
||||
@Param("packages") int packages);
|
||||
}
|
||||
|
||||
+7
@@ -6,6 +6,7 @@ import com.baiyue.lastleg.inventory.entity.InventoryPosition;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -31,4 +32,10 @@ public interface InventoryPositionMapper extends BaseMapper<InventoryPosition> {
|
||||
@Param("balanceId") Long balanceId,
|
||||
@Param("palletId") Long palletId,
|
||||
@Param("batchNo") String batchNo);
|
||||
|
||||
@Update("UPDATE ll_inventory_position SET available_packages=available_packages-#{packages}," +
|
||||
"status=CASE WHEN available_packages-#{packages}=0 THEN 'EMPTY' ELSE status END,version=version+1,updated_at=NOW(3) " +
|
||||
"WHERE customer_id=#{customerId} AND id=#{positionId} AND available_packages>=#{packages}")
|
||||
int deductAvailable(@Param("customerId") String customerId, @Param("positionId") Long positionId,
|
||||
@Param("packages") int packages);
|
||||
}
|
||||
|
||||
+8
@@ -51,6 +51,14 @@ public class MerchantRelabelApplicationController {
|
||||
@PathVariable String relabelNo, @RequestPart("file") MultipartFile file) {
|
||||
return ApiResponse.success(service.uploadShippingLabel(customer(user), relabelNo, operatorLong(user), file));
|
||||
}
|
||||
@PostMapping(value = "/{relabelNo}/files", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public ApiResponse<RelabelResponses.Detail> uploadFile(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @RequestParam String scope, @RequestParam(required = false) Long itemId,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
String kind = "SKU_TOTAL".equalsIgnoreCase(scope) ? "SKU_TOTAL" : "CARTON_TOTAL";
|
||||
if ("ITEM".equalsIgnoreCase(scope)) return ApiResponse.success(service.uploadItemFile(customer(user), relabelNo, itemId == null ? 0L : itemId, operatorLong(user), file));
|
||||
return ApiResponse.success(service.uploadTotalFile(customer(user), relabelNo, operatorLong(user), file, kind));
|
||||
}
|
||||
@GetMapping("/{relabelNo}/shipping-label/{fileId}/download")
|
||||
public ResponseEntity<byte[]> downloadShippingLabel(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String relabelNo, @PathVariable long fileId) {
|
||||
|
||||
@@ -12,6 +12,8 @@ public final class RelabelResponses {
|
||||
String contactName, String contactPhone, String status, int itemCount,
|
||||
int totalPackageCount, int notifiedPackageCount, String skuCodes,
|
||||
String totalFileId, String totalFileName, String totalFileUrl,
|
||||
String cartonTotalFileId, String cartonTotalFileName, String cartonTotalFileUrl,
|
||||
String skuTotalFileId, String skuTotalFileName, String skuTotalFileUrl,
|
||||
BigDecimal chargeAmount, String chargeCurrency, String walletStatus,
|
||||
String walletErrorMessage, String deliveryMethod, boolean printShippingLabel,
|
||||
String shippingLabelFileId, String shippingLabelFileName, String shippingLabelFileUrl,
|
||||
|
||||
+6
@@ -33,6 +33,12 @@ public class RelabelApplication {
|
||||
private String totalFileId;
|
||||
private String totalFileName;
|
||||
private String totalFileUrl;
|
||||
private String cartonTotalFileId;
|
||||
private String cartonTotalFileName;
|
||||
private String cartonTotalFileUrl;
|
||||
private String skuTotalFileId;
|
||||
private String skuTotalFileName;
|
||||
private String skuTotalFileUrl;
|
||||
private String status;
|
||||
private Integer itemCount;
|
||||
private Integer totalPackageCount;
|
||||
|
||||
+13
-3
@@ -98,11 +98,19 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file) {
|
||||
return uploadTotalFile(customerId, relabelNo, operatorId, file, "TOTAL");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public RelabelResponses.Detail uploadTotalFile(String customerId, String relabelNo, long operatorId, MultipartFile file, String kind) {
|
||||
RelabelApplication application = lockForDocument(customerId, relabelNo);
|
||||
requirePdf(file);
|
||||
var uploaded = documentFiles.upload(application.getCustomerId(), "OVERSEAS_RELABEL_TOTAL_DOCUMENT", String.valueOf(application.getId()), operatorId, file);
|
||||
application.setTotalFileId(String.valueOf(uploaded.fileId())); application.setTotalFileName(uploaded.fileName());
|
||||
application.setTotalFileUrl(documentUrl(relabelNo, uploaded.fileId(), null)); application.setUpdatedAt(LocalDateTime.now());
|
||||
String businessType = "SKU_TOTAL".equals(kind) ? "OVERSEAS_RELABEL_SKU_TOTAL_DOCUMENT" : "OVERSEAS_RELABEL_CARTON_TOTAL_DOCUMENT";
|
||||
var uploaded = documentFiles.upload(application.getCustomerId(), businessType, String.valueOf(application.getId()), operatorId, file);
|
||||
String fileId = String.valueOf(uploaded.fileId()); String url = documentUrl(relabelNo, uploaded.fileId(), null);
|
||||
if ("SKU_TOTAL".equals(kind)) { application.setSkuTotalFileId(fileId); application.setSkuTotalFileName(uploaded.fileName()); application.setSkuTotalFileUrl(url); }
|
||||
else { application.setCartonTotalFileId(fileId); application.setCartonTotalFileName(uploaded.fileName()); application.setCartonTotalFileUrl(url); }
|
||||
application.setTotalFileId(fileId); application.setTotalFileName(uploaded.fileName()); application.setTotalFileUrl(url); application.setUpdatedAt(LocalDateTime.now());
|
||||
applications.updateById(application);
|
||||
return detail(application.getCustomerId(), relabelNo);
|
||||
}
|
||||
@@ -708,6 +716,8 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
Objects.requireNonNullElse(value.getTotalPackageCount(), 0),
|
||||
Objects.requireNonNullElse(value.getNotifiedPackageCount(), 0), skuCodes,
|
||||
value.getTotalFileId(), value.getTotalFileName(), value.getTotalFileUrl(),
|
||||
value.getCartonTotalFileId(), value.getCartonTotalFileName(), value.getCartonTotalFileUrl(),
|
||||
value.getSkuTotalFileId(), value.getSkuTotalFileName(), value.getSkuTotalFileUrl(),
|
||||
value.getChargeAmount(), value.getChargeCurrency(), value.getWalletStatus(), value.getWalletErrorMessage(),
|
||||
value.getDeliveryMethod(), Boolean.TRUE.equals(value.getPrintShippingLabel()),
|
||||
value.getShippingLabelFileId(), value.getShippingLabelFileName(), value.getShippingLabelFileUrl(),
|
||||
|
||||
@@ -1919,7 +1919,9 @@ 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 '产品名称',
|
||||
available_packages_snapshot INT NOT NULL COMMENT '可用箱数快照', discard_packages INT NOT 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 'PENDING/COMPLETED',
|
||||
pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人', pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', PRIMARY KEY(id),
|
||||
UNIQUE KEY uk_ll_cargo_discard_item(application_id,profile_id,sku_code),
|
||||
KEY idx_ll_cargo_discard_item_list(customer_id,application_id),
|
||||
@@ -1942,6 +1944,20 @@ CREATE TABLE IF NOT EXISTS ll_cargo_discard_status_history (
|
||||
KEY idx_ll_cargo_discard_history(customer_id,application_id,created_at)
|
||||
) ENGINE=InnoDB COMMENT='货物废弃审批历史';
|
||||
|
||||
-- Backfill applications created before cargo discard became a first-class tail-leg order.
|
||||
INSERT IGNORE INTO ll_order (
|
||||
id,order_no,customer_id,order_type,inbound_code,inbound_order_no,source_region,source_inbound_id,
|
||||
source_order_id,source_order_no,overseas_warehouse_id,overseas_warehouse_name,package_count,
|
||||
current_status,version,source_event_id,source_loading_plan_id,source_payload_hash,created_at,updated_at
|
||||
)
|
||||
SELECT a.id,a.discard_no,a.customer_id,'CARGO_DISCARD',a.inbound_codes,a.discard_no,'LASTLEG_DISCARD',
|
||||
CONCAT('CARGO_DISCARD:',a.id),CAST(a.id AS CHAR),a.discard_no,a.overseas_warehouse_id,a.overseas_warehouse_name,
|
||||
a.total_packages,a.status,0,CONCAT('CARGO_DISCARD:',a.id),'CARGO_DISCARD',a.request_hash,a.created_at,a.updated_at
|
||||
FROM ll_cargo_discard_application a
|
||||
LEFT JOIN ll_order o ON o.customer_id=a.customer_id AND o.source_region='LASTLEG_DISCARD'
|
||||
AND o.source_inbound_id=CONCAT('CARGO_DISCARD:',a.id)
|
||||
WHERE o.id IS NULL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_overseas_procurement_order (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
customer_name VARCHAR(255) NULL COMMENT '客户名称快照', order_no VARCHAR(64) NOT NULL COMMENT '订单号',
|
||||
|
||||
+7
-7
@@ -14,6 +14,7 @@ import com.baiyue.lastleg.dropship.entity.DropshipCarton;
|
||||
import com.baiyue.lastleg.dropship.entity.DropshipSkuInventory;
|
||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderMapper;
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -48,6 +49,7 @@ class CargoDiscardServiceTest {
|
||||
@Mock CargoDiscardInventoryMapper inventory;
|
||||
@Mock CargoDiscardInventoryAdjustmentService adjustments;
|
||||
@Mock LastlegOrderMapper orders;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
|
||||
private CargoDiscardService service;
|
||||
private final AtomicReference<CargoDiscardApplication> storedApplication = new AtomicReference<>();
|
||||
@@ -66,6 +68,7 @@ class CargoDiscardServiceTest {
|
||||
ReflectionTestUtils.setField(service, "adjustments", adjustments);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
ReflectionTestUtils.setField(service, "feePerPackage", new BigDecimal("5.00"));
|
||||
lenient().when(applications.insert(any(CargoDiscardApplication.class))).thenAnswer(invocation -> { storedApplication.set(invocation.getArgument(0)); return 1; });
|
||||
@@ -100,7 +103,7 @@ class CargoDiscardServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void approvalDeductsSkuInventoryAndWritesOneLedgerPerCarton() {
|
||||
void approvalPushesToPdaWithoutDeductingInventory() {
|
||||
CargoDiscardApplication application = pendingApplication();
|
||||
storedApplication.set(application);
|
||||
CargoDiscardItem item = item();
|
||||
@@ -108,15 +111,12 @@ class CargoDiscardServiceTest {
|
||||
storedLinks.add(link(item.getId(), 1L));
|
||||
storedLinks.add(link(item.getId(), 2L));
|
||||
when(applications.lockNo("100", "FH-1")).thenReturn(application);
|
||||
when(adjustments.discard(eq("100"), eq(application), any(), any(), eq("9"), any()))
|
||||
.thenReturn(new CargoDiscardInventoryAdjustmentService.AdjustmentResult(3, 2, 1));
|
||||
|
||||
var detail = service.review("100", "FH-1", "9", new CargoDiscardRequests.Review("APPROVE", "ok"));
|
||||
|
||||
assertEquals(CargoDiscardService.APPROVED, detail.summary().status());
|
||||
verify(adjustments).discard(eq("100"), eq(application), any(), any(), eq("9"), any());
|
||||
assertEquals("ok; inventoryBefore=3, discarded=2, inventoryAfter=1",
|
||||
storedHistories.get(storedHistories.size() - 1).getRemark());
|
||||
verify(adjustments, never()).discard(any(), any(), any(), any(), any(), any());
|
||||
verify(inventory, never()).deductSkuInventory(any(), anyLong(), anyInt());
|
||||
assertEquals("ok", storedHistories.get(storedHistories.size() - 1).getRemark());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
@@ -412,6 +412,15 @@ class DispatchServiceTest {
|
||||
verify(locks, org.mockito.Mockito.times(1)).selectList(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void receivedPalletUsesPhysicalPalletAsDispatchLockIdentity() {
|
||||
DispatchPalletCandidate mergedInboundOrder = candidate(10L, 900L, "ONT8");
|
||||
DispatchPalletCandidate plannedOnly = candidate(11L, null, "ONT8");
|
||||
|
||||
assertEquals("RECEIVED:900", mergedInboundOrder.sourcePalletKey());
|
||||
assertEquals("PLAN:11", plannedOnly.sourcePalletKey());
|
||||
}
|
||||
|
||||
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
||||
return candidate(planPalletId, warehousePalletId, destination, 12);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
USE baiyue_lastleg;
|
||||
|
||||
ALTER TABLE ll_cargo_discard_item
|
||||
ADD COLUMN actual_discard_packages INT NULL COMMENT 'PDA确认实际废弃箱数' AFTER discard_packages,
|
||||
ADD COLUMN pda_status VARCHAR(32) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/COMPLETED' AFTER actual_discard_packages,
|
||||
ADD COLUMN pda_operator_id VARCHAR(128) NULL COMMENT 'PDA操作人' AFTER pda_status,
|
||||
ADD COLUMN pda_completed_at DATETIME(3) NULL COMMENT 'PDA完成时间' AFTER pda_operator_id;
|
||||
|
||||
INSERT IGNORE INTO ll_order (
|
||||
id,order_no,customer_id,order_type,inbound_code,inbound_order_no,source_region,source_inbound_id,
|
||||
source_order_id,source_order_no,overseas_warehouse_id,overseas_warehouse_name,package_count,
|
||||
current_status,version,source_event_id,source_loading_plan_id,source_payload_hash,created_at,updated_at
|
||||
)
|
||||
SELECT a.id,a.discard_no,a.customer_id,'CARGO_DISCARD',a.inbound_codes,a.discard_no,'LASTLEG_DISCARD',
|
||||
CONCAT('CARGO_DISCARD:',a.id),CAST(a.id AS CHAR),a.discard_no,a.overseas_warehouse_id,a.overseas_warehouse_name,
|
||||
a.total_packages,a.status,0,CONCAT('CARGO_DISCARD:',a.id),'CARGO_DISCARD',a.request_hash,a.created_at,a.updated_at
|
||||
FROM ll_cargo_discard_application a
|
||||
LEFT JOIN ll_order o ON o.customer_id=a.customer_id AND o.source_region='LASTLEG_DISCARD'
|
||||
AND o.source_inbound_id=CONCAT('CARGO_DISCARD:',a.id)
|
||||
WHERE o.id IS NULL;
|
||||
@@ -3893,6 +3893,18 @@ PREPARE relabel_inventory_stmt FROM @ddl; EXECUTE relabel_inventory_stmt; DEALLO
|
||||
-- 2026-09-09 / 换标PDA完工、客户确认及出库分流
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='delivery_method'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN delivery_method VARCHAR(32) NULL COMMENT ''派送方式:TRUCK卡车、PICKUP自提、EXPRESS快递;仅涉及箱唛换标时必填'' AFTER contact_phone'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT ''箱唛总文件ID'' AFTER total_file_url'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT ''箱唛总文件名'' AFTER carton_total_file_id'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_url VARCHAR(1000) NULL COMMENT ''箱唛总文件地址'' AFTER carton_total_file_name'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_id VARCHAR(128) NULL COMMENT ''SKU总文件ID'' AFTER carton_total_file_url'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_name VARCHAR(255) NULL COMMENT ''SKU总文件名'' AFTER sku_total_file_id'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_url VARCHAR(1000) NULL COMMENT ''SKU总文件地址'' AFTER sku_total_file_name'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='print_shipping_label'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN print_shipping_label TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''是否需要打印面单'' AFTER delivery_method'));
|
||||
PREPARE relabel_fulfillment_stmt FROM @ddl; EXECUTE relabel_fulfillment_stmt; DEALLOCATE PREPARE relabel_fulfillment_stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='shipping_label_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN shipping_label_file_id VARCHAR(128) NULL COMMENT ''客户上传面单文件ID'' AFTER print_shipping_label'));
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
USE baiyue_lastleg;
|
||||
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_id VARCHAR(128) NULL COMMENT ''箱唛总文件ID'' AFTER total_file_url'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_name VARCHAR(255) NULL COMMENT ''箱唛总文件名'' AFTER carton_total_file_id'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='carton_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN carton_total_file_url VARCHAR(1000) NULL COMMENT ''箱唛总文件地址'' AFTER carton_total_file_name'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_id'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_id VARCHAR(128) NULL COMMENT ''SKU总文件ID'' AFTER carton_total_file_url'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_name'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_name VARCHAR(255) NULL COMMENT ''SKU总文件名'' AFTER sku_total_file_id'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_relabel_application' AND column_name='sku_total_file_url'), 'SELECT 1', 'ALTER TABLE ll_relabel_application ADD COLUMN sku_total_file_url VARCHAR(1000) NULL COMMENT ''SKU总文件地址'' AFTER sku_total_file_name'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
Reference in New Issue
Block a user