提交
This commit is contained in:
+7
-5
@@ -11,20 +11,22 @@ import java.util.HashSet;
|
||||
public class OutboundQuantityRepository {
|
||||
@Resource private OutboundPalletItemMapper pallets;
|
||||
|
||||
/** 废弃退货来源依据已冻结箱唛计数,读取已有单据也不会把件数当箱数。 */
|
||||
/** 废弃来源依据已冻结箱唛去重计数,无箱唛的库存明细保留箱数快照。 */
|
||||
public Integer boxes(OutboundOrder outbound) {
|
||||
if (!"DISCARD_OUTBOUND".equals(outbound.getOutboundType())) return outbound.getPackageCount();
|
||||
var lines = pallets.findByOutbound(outbound.getCustomerId(), outbound.getId());
|
||||
if (lines.isEmpty()) return outbound.getPackageCount();
|
||||
var returnBoxes = new HashSet<String>();
|
||||
var markedBoxes = new HashSet<String>();
|
||||
int count = 0;
|
||||
for (var line : lines) {
|
||||
if (line.getReturnSkuQuantity() != null && line.getCartonMarks() != null && !line.getCartonMarks().isBlank()) {
|
||||
returnBoxes.add(line.getInboundCode() + ":" + line.getCartonMarks());
|
||||
if (line.getCartonMarks() != null && !line.getCartonMarks().isBlank()) {
|
||||
// 同一箱的多个 SKU 共用箱唛,只计一个实物箱;一行也可能冻结多个箱唛。
|
||||
java.util.Arrays.stream(line.getCartonMarks().split(",")).map(String::trim)
|
||||
.filter(mark -> !mark.isEmpty()).forEach(markedBoxes::add);
|
||||
} else {
|
||||
count = Math.addExact(count, java.util.Objects.requireNonNullElse(line.getPackageCount(), 0));
|
||||
}
|
||||
}
|
||||
return Math.addExact(count, returnBoxes.size());
|
||||
return Math.addExact(count, markedBoxes.size());
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -130,6 +130,7 @@ public class DeliveryTransferService {
|
||||
@Resource private DeliveryExecutionGuard deliveryExecution;
|
||||
@Resource private com.baiyue.lastleg.dispatch.port.DispatchExecutionPort execution;
|
||||
@Resource private com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository cancellations;
|
||||
@Resource private com.baiyue.lastleg.delivery.repository.OutboundQuantityRepository outboundQuantities;
|
||||
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask createOneClick(String customerId, String operatorId, DeliveryRequests.OneClickOutbound request) {
|
||||
@@ -1341,7 +1342,11 @@ public class DeliveryTransferService {
|
||||
} catch (JsonProcessingException exception) { throw new IllegalStateException("费用覆盖确认无法读取", exception); }
|
||||
}
|
||||
|
||||
private DeliveryResponses.OutboundRow outboundRow(OutboundOrder o) { return new DeliveryResponses.OutboundRow(o.getId(), o.getOutboundNo(), o.getDispatchNo(), o.getOverseasWarehouseId(), "DISCARD_OUTBOUND".equals(o.getOutboundType()) ? null : o.getLxCodes(), o.getStatus(), o.getPalletCount(), o.getPackageCount(), o.getGrossWeightKg(), o.getVolumeWeightKg(), o.getVolumeCbm(), o.getCreatedAt(), o.getOutboundType()); }
|
||||
/** 废弃出库按冻结实物箱汇总,避免历史件数快照被展示为箱数。 */
|
||||
private DeliveryResponses.OutboundRow outboundRow(OutboundOrder o) {
|
||||
Integer boxes = "DISCARD_OUTBOUND".equals(o.getOutboundType()) ? outboundQuantities.boxes(o) : o.getPackageCount();
|
||||
return new DeliveryResponses.OutboundRow(o.getId(), o.getOutboundNo(), o.getDispatchNo(), o.getOverseasWarehouseId(), "DISCARD_OUTBOUND".equals(o.getOutboundType()) ? null : o.getLxCodes(), o.getStatus(), o.getPalletCount(), boxes, o.getGrossWeightKg(), o.getVolumeWeightKg(), o.getVolumeCbm(), o.getCreatedAt(), o.getOutboundType());
|
||||
}
|
||||
private DeliveryResponses.OutboundPalletRow outboundPalletRow(OutboundPalletItem item) {
|
||||
return new DeliveryResponses.OutboundPalletRow(item.getId(), item.getPalletNo(), item.getCartonMarks(),
|
||||
item.getInboundCode(), item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(),
|
||||
|
||||
+8
@@ -46,6 +46,14 @@ public class CargoDiscardController {
|
||||
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
/** 读取全部筛选结果的数量统计,与分页记录分开返回。 */
|
||||
@GetMapping("/statistics")
|
||||
public ApiResponse<com.baiyue.lastleg.discard.service.CargoDiscardStatisticsService.Statistics> statistics(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam(required = false) String customerId,
|
||||
@Valid CargoDiscardRequests.Query query) {
|
||||
return ApiResponse.success(service.statistics(scope(user, customerId), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{discardNo}")
|
||||
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam(required = false) String customerId, @PathVariable String discardNo) {
|
||||
|
||||
+8
@@ -32,6 +32,14 @@ public class MerchantCargoDiscardController {
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DISCARD_LIST + "')")
|
||||
public ApiResponse<PageResult<CargoDiscardResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid CargoDiscardRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||
/** 客户统计只读取当前认证客户,不接受客户端客户归属。 */
|
||||
@GetMapping("/statistics")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DISCARD_LIST + "')")
|
||||
public ApiResponse<com.baiyue.lastleg.discard.service.CargoDiscardStatisticsService.Statistics> statistics(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid CargoDiscardRequests.Query query) {
|
||||
return ApiResponse.success(service.statistics(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{discardNo}")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_DISCARD_LIST + "')")
|
||||
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
|
||||
+8
-1
@@ -33,5 +33,12 @@ public final class CargoDiscardRequests {
|
||||
|
||||
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String discardNo, String inboundCode, String overseasWarehouseId,
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo) {}
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo,
|
||||
LocalDateTime completedFrom, LocalDateTime completedTo) {
|
||||
/** 未筛完成时间的调用保持创建时间查询语义。 */
|
||||
public Query(long page, long size, String discardNo, String inboundCode, String overseasWarehouseId,
|
||||
String status, LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||
this(page, size, discardNo, inboundCode, overseasWarehouseId, status, createdFrom, createdTo, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -37,7 +37,12 @@ public final class CargoDiscardResponses {
|
||||
String createdBy, LocalDateTime createdAt, LocalDateTime reviewedAt, String outboundNo, LocalDateTime customerConfirmedAt) {}
|
||||
|
||||
public record Item(Long id, Long orderId, String inboundCode, String skuCode,
|
||||
String productName, int availablePackagesSnapshot, int discardPackages, List<String> cartonMarks) {
|
||||
String productName, int availablePackagesSnapshot, int discardPackages, List<String> cartonMarks,
|
||||
String quantityUnit, Integer actualDiscardPackages, String pdaStatus) {
|
||||
/** 无来源事实的调用不猜测数量单位或实际完成数。 */
|
||||
public Item(Long id, Long orderId, String inboundCode, String skuCode, String productName, int availablePackagesSnapshot, int discardPackages, List<String> cartonMarks) {
|
||||
this(id, orderId, inboundCode, skuCode, productName, availablePackagesSnapshot, discardPackages, cartonMarks, null, null, null);
|
||||
}
|
||||
public Item(Long id, Long orderId, String inboundCode, String skuCode, String productName, int availablePackagesSnapshot, int discardPackages) {
|
||||
this(id, orderId, inboundCode, skuCode, productName, availablePackagesSnapshot, discardPackages, List.of());
|
||||
}
|
||||
|
||||
+6
-5
@@ -25,6 +25,7 @@ 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 关联已清点箱,避免漏箱或混入其他托盘的箱号。 */
|
||||
@Select("""
|
||||
SELECT c.id,c.carton_mark,c.sku_code,'INBOUND' source_type
|
||||
FROM ll_inbound_plan_carton c
|
||||
@@ -38,13 +39,13 @@ public interface CargoDiscardInventoryMapper {
|
||||
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
|
||||
AND t.transfer_no=p.batch_no AND t.operating_warehouse_id=p.overseas_warehouse_id
|
||||
JOIN ll_warehouse_transfer_carton c ON c.customer_id=t.customer_id AND c.transfer_order_id=t.id AND c.status='EXPECTED'
|
||||
JOIN ll_warehouse_transfer_pallet tp ON tp.customer_id=t.customer_id AND tp.transfer_order_id=t.id
|
||||
AND tp.warehouse_pallet_id=p.pallet_id AND tp.status='COMPLETED'
|
||||
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,'')
|
||||
WHERE p.customer_id=#{customerId} AND p.id=#{positionId}
|
||||
AND (SELECT COUNT(*) FROM ll_inventory_position current_position
|
||||
WHERE current_position.customer_id=p.customer_id AND current_position.balance_id=p.balance_id
|
||||
AND current_position.batch_no=p.batch_no AND current_position.overseas_warehouse_id=p.overseas_warehouse_id
|
||||
AND current_position.available_packages+current_position.hold_packages+current_position.allocated_packages+current_position.picked_packages>0)=1
|
||||
ORDER BY id
|
||||
""")
|
||||
List<com.baiyue.lastleg.discard.model.CargoDiscardStockCartonRow> genericCartons(@Param("customerId") String customerId, @Param("positionId") Long positionId);
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.discard.model;
|
||||
|
||||
import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton;
|
||||
|
||||
/** 废弃作业沿用签收时实物标签,兼容历史包裹内部 RP 编号。 */
|
||||
public final class CargoDiscardReturnMark {
|
||||
/** 工具类不允许实例化。 */
|
||||
private CargoDiscardReturnMark() { }
|
||||
|
||||
/** 包裹取签收单元编码;箱子及缺少来源编码的历史数据保留原箱唛。 */
|
||||
public static String of(ReturnInventoryCarton carton) {
|
||||
if (carton == null) return null;
|
||||
if ("PARCEL".equals(carton.getSourceUnitType()) && carton.getSourceUnitCode() != null
|
||||
&& !carton.getSourceUnitCode().isBlank()) return carton.getSourceUnitCode().trim();
|
||||
return carton.getCartonMark();
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -201,7 +201,8 @@ public class PdaCargoDiscardService {
|
||||
if ("INVENTORY".equals(item.getSourceType())) return stockService.cartonMarks(item);
|
||||
if (isReturnItem(item)) {
|
||||
var carton = returnCartons.selectById(item.getReturnCartonId());
|
||||
return carton.getCartonMark() == null ? List.of() : List.of(carton.getCartonMark());
|
||||
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()
|
||||
.filter(c -> Objects.equals(c.getItemId(), item.getId())).map(CargoDiscardCarton::getCartonMark).filter(Objects::nonNull).toList();
|
||||
|
||||
+7
-1
@@ -73,7 +73,7 @@ public class CargoDiscardOutboundService {
|
||||
|| !application.getOverseasWarehouseId().equals(carton.getOverseasWarehouseId()))
|
||||
throw conflict("废弃明细关联退货箱不存在或仓库不匹配");
|
||||
palletNo = carton.getPalletCode() == null || carton.getPalletCode().isBlank() ? null : carton.getPalletCode();
|
||||
cartonMarks = carton.getCartonMark();
|
||||
cartonMarks = com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton);
|
||||
} else {
|
||||
var profile = profiles.selectById(line.getProfileId());
|
||||
if (profile == null || !application.getCustomerId().equals(profile.getCustomerId()))
|
||||
@@ -108,6 +108,12 @@ public class CargoDiscardOutboundService {
|
||||
var line = lines.stream().filter(i -> Objects.equals(i.getId(), snapshot.getId())).findFirst()
|
||||
.orElseThrow(() -> conflict("废弃出库明细关联失效"));
|
||||
boolean returned = line.getReturnCartonId() != null;
|
||||
if (returned) {
|
||||
var carton = returnCartons.selectById(line.getReturnCartonId());
|
||||
if (carton == null || !application.getCustomerId().equals(carton.getCustomerId()))
|
||||
throw conflict("废弃明细关联退货箱不存在");
|
||||
snapshot.setCartonMarks(com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton));
|
||||
}
|
||||
int boxes = returned ? (returnBoxIds.add(line.getReturnCartonId()) ? 1 : 0) : line.getDiscardPackages();
|
||||
snapshot.setPackageCount(boxes);
|
||||
if ("COMPLETED".equals(line.getPdaStatus())) {
|
||||
|
||||
+17
-14
@@ -50,6 +50,7 @@ import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class CargoDiscardService {
|
||||
@Resource private CargoDiscardStatisticsService statisticsService;
|
||||
@Resource private com.baiyue.lastleg.discard.repository.CargoDiscardQuantityRepository quantities;
|
||||
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
|
||||
public static final String APPROVAL_PERMISSION = "platform:overseas-warehouse:discard:approve";
|
||||
@@ -97,14 +98,14 @@ public class CargoDiscardService {
|
||||
if (returnCartons == null || returnCartonItems == null) return result;
|
||||
for (ReturnInventoryCarton carton : returnCartons.findAvailable(customerId, null)) {
|
||||
if (StringUtils.hasText(keyword) && !containsIgnoreCase(carton.getSourceReturnNo(), keyword)
|
||||
&& !containsIgnoreCase(carton.getCartonMark(), keyword)) continue;
|
||||
&& !containsIgnoreCase(com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton), keyword)) continue;
|
||||
List<CargoDiscardResponses.CandidateSku> skus = returnCartonItems.listCarton(carton.getId()).stream()
|
||||
.filter(item -> value(item.getQuantity()) > 0)
|
||||
.map(item -> new CargoDiscardResponses.CandidateSku(item.getSkuCode(), item.getProductNameZh(), value(item.getQuantity())))
|
||||
.toList();
|
||||
long available = skus.stream().mapToLong(CargoDiscardResponses.CandidateSku::availablePackages).sum();
|
||||
if (available <= 0) continue;
|
||||
result.add(new CargoDiscardResponses.Candidate(null, "RETURN_CARTON", carton.getId(), carton.getCartonMark(),
|
||||
result.add(new CargoDiscardResponses.Candidate(null, "RETURN_CARTON", carton.getId(), com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton),
|
||||
carton.getSourceReturnNo(), carton.getSourceReturnNo(), carton.getOverseasWarehouseId(), null,
|
||||
null, available, skus));
|
||||
}
|
||||
@@ -253,7 +254,8 @@ public class CargoDiscardService {
|
||||
if ("INVENTORY".equals(line.getSourceType())) return stockService.cartonMarks(line);
|
||||
if (isReturnItem(line)) {
|
||||
var carton = returnCartons.selectById(line.getReturnCartonId());
|
||||
return carton == null || carton.getCartonMark() == null ? List.of() : List.of(carton.getCartonMark());
|
||||
String mark = com.baiyue.lastleg.discard.model.CargoDiscardReturnMark.of(carton);
|
||||
return mark == null ? List.of() : List.of(mark);
|
||||
}
|
||||
return discardCartons.listApplication(customer, application.getId()).stream().filter(link -> link.getItemId().equals(line.getId()))
|
||||
.map(CargoDiscardCarton::getCartonMark).distinct().sorted().toList();
|
||||
@@ -261,16 +263,7 @@ public class CargoDiscardService {
|
||||
|
||||
public PageResult<CargoDiscardResponses.Summary> page(String customerId, CargoDiscardRequests.Query query) {
|
||||
validateQuery(query);
|
||||
LambdaQueryWrapper<CargoDiscardApplication> wrapper = new LambdaQueryWrapper<CargoDiscardApplication>()
|
||||
.eq(StringUtils.hasText(customerId), CargoDiscardApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.discardNo()), CargoDiscardApplication::getDiscardNo, query.discardNo())
|
||||
.like(StringUtils.hasText(query.inboundCode()), CargoDiscardApplication::getInboundCodes, query.inboundCode())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), CargoDiscardApplication::getOverseasWarehouseId,
|
||||
query.overseasWarehouseId())
|
||||
.eq(StringUtils.hasText(query.status()), CargoDiscardApplication::getStatus, query.status())
|
||||
.ge(query.createdFrom() != null, CargoDiscardApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, CargoDiscardApplication::getCreatedAt, query.createdTo())
|
||||
.orderByDesc(CargoDiscardApplication::getCreatedAt).orderByDesc(CargoDiscardApplication::getId);
|
||||
var wrapper = com.baiyue.lastleg.discard.repository.CargoDiscardQueryRepository.filter(customerId, query);
|
||||
Page<CargoDiscardApplication> result = applications.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||
Map<String, String> names = result.getRecords().stream().anyMatch(a -> !StringUtils.hasText(a.getOverseasWarehouseName()))
|
||||
? warehouseNames() : Map.of();
|
||||
@@ -278,12 +271,19 @@ public class CargoDiscardService {
|
||||
return new PageResult<>(records, query.page(), query.size(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
/** 全筛选汇总与列表使用同一输入校验,不受当前分页影响。 */
|
||||
public CargoDiscardStatisticsService.Statistics statistics(String customerId, CargoDiscardRequests.Query query) {
|
||||
validateQuery(query);
|
||||
return statisticsService.statistics(customerId, query);
|
||||
}
|
||||
|
||||
public CargoDiscardResponses.Detail detail(String customerId, String discardNo) {
|
||||
CargoDiscardApplication application = applications.findNo(customerId, discardNo);
|
||||
if (application == null) throw notFound("Cargo discard application not found");
|
||||
List<CargoDiscardResponses.Item> itemViews = items.listApplication(customerId, application.getId()).stream()
|
||||
.map(value -> new CargoDiscardResponses.Item(value.getId(), value.getOrderId(), value.getInboundCode(),
|
||||
value.getSkuCode(), value.getProductName(), value.getAvailablePackagesSnapshot(), value.getDiscardPackages(), selectedMarks(customerId, application, value)))
|
||||
value.getSkuCode(), value.getProductName(), value.getAvailablePackagesSnapshot(), value.getDiscardPackages(), selectedMarks(customerId, application, value),
|
||||
value.getReturnCartonId() != null ? "件" : "箱", value.getActualDiscardPackages(), value.getPdaStatus()))
|
||||
.toList();
|
||||
List<CargoDiscardResponses.History> historyViews = histories.listApplication(customerId, application.getId()).stream()
|
||||
.map(value -> new CargoDiscardResponses.History(value.getFromStatus(), value.getToStatus(),
|
||||
@@ -528,6 +528,9 @@ public class CargoDiscardService {
|
||||
}
|
||||
|
||||
private void validateQuery(CargoDiscardRequests.Query query) {
|
||||
if (query.completedFrom() != null && query.completedTo() != null && query.completedFrom().isAfter(query.completedTo())) {
|
||||
throw badRequest("completedFrom must not be after completedTo");
|
||||
}
|
||||
if (StringUtils.hasText(query.status()) && !STATUSES.contains(query.status())) throw badRequest("Unsupported discard status");
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw badRequest("createdFrom must not be after createdTo");
|
||||
|
||||
+4
-2
@@ -66,6 +66,7 @@ public class CargoDiscardStockService {
|
||||
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("库存仓位不存在");
|
||||
var balance = validate(customer, request);
|
||||
@@ -80,7 +81,7 @@ public class CargoDiscardStockService {
|
||||
: availableCartons.stream().filter(c -> request.cartonIds().contains(c.getId())).toList();
|
||||
if (!selected.isEmpty()) {
|
||||
for (var carton : selected) {
|
||||
if (changeBox(customer, carton.getId(), carton.getSourceType(), "TRANSFER".equals(carton.getSourceType()) ? "EXPECTED" : "AVAILABLE", "DISCARD_PENDING") != 1) throw conflict("箱子已被其他作业占用,请刷新后重选");
|
||||
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);
|
||||
}
|
||||
@@ -115,9 +116,10 @@ public class CargoDiscardStockService {
|
||||
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())) {
|
||||
if (Objects.equals(link.getItemId(), line.getId()) && changeBox(customer, link.getCartonId(), link.getSourceType(), "DISCARD_PENDING", "TRANSFER".equals(link.getSourceType()) && "AVAILABLE".equals(status) ? "EXPECTED" : status) != 1)
|
||||
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("预留箱子的库存状态发生变化");
|
||||
}
|
||||
}
|
||||
|
||||
+5
@@ -5,6 +5,11 @@ import java.util.List;
|
||||
/** BOL按真实明细客户逐行读取整车,不依赖已移除的主单客户头。 */
|
||||
@Mapper
|
||||
public interface DispatchBolMapper {
|
||||
/** 旧客户详情入口沿用整车查询,但只返回该客户自己的BOL明细。 */
|
||||
default List<com.baiyue.lastleg.dispatch.dto.DispatchResponse.BolLine> findByDispatchOrder(String customerId, Long dispatchOrderId) {
|
||||
return findWholeDispatch(dispatchOrderId).stream().filter(line -> java.util.Objects.equals(customerId,line.customerId())).map(line -> new com.baiyue.lastleg.dispatch.dto.DispatchResponse.BolLine(line.containerNo(),line.palletNo(),line.fbaWarehouseCode(),line.destinationWarehouseName(),line.warehouseNo(),line.fbaShipmentId(),line.poReferenceNo(),line.packageCount())).toList();
|
||||
}
|
||||
|
||||
@Select("SELECT di.customer_id AS customerId,di.destination_type AS destinationType,di.delivery_address AS deliveryAddress,di.postal_code AS postalCode," +
|
||||
"COALESCE(MAX(NULLIF(o.container_no,'')),MAX(NULLIF(ip.container_no,''))) AS containerNo," +
|
||||
"MAX(NULLIF(di.pallet_no,'')) AS palletNo," +
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ public final class OverseasProcurementRequests {
|
||||
|
||||
public record Create(
|
||||
@Size(max = 255) String customerName,
|
||||
@NotBlank @Size(max = 128) String purchaseCategory,
|
||||
@Size(max = 128) String purchaseCategory,
|
||||
@NotBlank @Size(max = 255) String productName,
|
||||
@Size(max = 500) String productSpecification,
|
||||
@Size(max = 2000) String productUrl,
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ public class OverseasProcurementService {
|
||||
order.setCustomerId(customerId);
|
||||
order.setCustomerName(trim(request.customerName()));
|
||||
order.setOrderNo(orderNoGenerator.next(customerId, request.customerName()));
|
||||
order.setPurchaseCategory(request.purchaseCategory().trim());
|
||||
order.setPurchaseCategory(request.purchaseCategory() == null ? "" : request.purchaseCategory().trim());
|
||||
order.setProductName(request.productName().trim());
|
||||
order.setProductSpecification(trim(request.productSpecification()));
|
||||
order.setProductUrl(trim(request.productUrl()));
|
||||
|
||||
+9
@@ -25,6 +25,15 @@ import java.util.List;
|
||||
@RequestMapping("/api/merchant/v1/last-leg/relabel-applications")
|
||||
public class MerchantRelabelApplicationController {
|
||||
@Resource private RelabelApplicationService service;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelQuantityService quantities;
|
||||
|
||||
/** 客户汇总始终取认证客户范围,不接收客户端客户ID。 */
|
||||
@GetMapping("/quantity-summary")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ApiResponse<com.baiyue.lastleg.relabel.service.RelabelQuantityService.Totals> quantities(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid RelabelRequests.Query query) {
|
||||
return ApiResponse.success(quantities.totals(customer(user), query));
|
||||
}
|
||||
@GetMapping("/inventory-candidates")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_RELABEL_LIST + "')")
|
||||
public ApiResponse<List<RelabelResponses.InventoryCandidate>> candidates(
|
||||
|
||||
+10
@@ -25,6 +25,16 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@RequestMapping("/api/platform/v1/last-leg/relabel-applications")
|
||||
public class RelabelApplicationController {
|
||||
@Resource private RelabelApplicationService service;
|
||||
@Resource private com.baiyue.lastleg.relabel.service.RelabelQuantityService quantities;
|
||||
|
||||
/** 按平台当前全部筛选结果返回独立数量维度。 */
|
||||
@GetMapping("/quantity-summary")
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.RELABEL_ORDER_LIST + "')")
|
||||
public ApiResponse<com.baiyue.lastleg.relabel.service.RelabelQuantityService.Totals> quantities(
|
||||
@AuthenticationPrincipal AuthenticatedUser user, @Valid RelabelRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(quantities.totals(null, query));
|
||||
}
|
||||
@Resource private com.baiyue.lastleg.relabel.pda.service.PdaRelabelService pdaService;
|
||||
|
||||
@PostMapping("/{relabelNo}/rework")
|
||||
|
||||
+1
-24
@@ -570,30 +570,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public PageResult<RelabelResponses.Summary> page(String customerId, RelabelRequests.Query query) {
|
||||
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||
throw badRequest("createdFrom must not be after createdTo");
|
||||
}
|
||||
String requestedStatus = trim(query.status());
|
||||
LambdaQueryWrapper<RelabelApplication> wrapper = new LambdaQueryWrapper<RelabelApplication>()
|
||||
.eq(StringUtils.hasText(customerId), RelabelApplication::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.relabelNo()), RelabelApplication::getRelabelNo, query.relabelNo())
|
||||
.like(StringUtils.hasText(query.customerOrderNo()), RelabelApplication::getCustomerOrderNo, query.customerOrderNo())
|
||||
.eq(StringUtils.hasText(query.overseasWarehouseId()), RelabelApplication::getOverseasWarehouseId,
|
||||
trim(query.overseasWarehouseId()))
|
||||
.ge(query.createdFrom() != null, RelabelApplication::getCreatedAt, query.createdFrom())
|
||||
.le(query.createdTo() != null, RelabelApplication::getCreatedAt, query.createdTo())
|
||||
.apply(StringUtils.hasText(query.skuCode()),
|
||||
"EXISTS (SELECT 1 FROM ll_relabel_item ri WHERE ri.application_id=ll_relabel_application.id AND ri.customer_id=ll_relabel_application.customer_id AND ri.sku_code LIKE CONCAT('%',{0},'%'))",
|
||||
query.skuCode())
|
||||
.orderByDesc(RelabelApplication::getCreatedAt);
|
||||
if (PENDING_PROCESSING.equals(requestedStatus)) {
|
||||
wrapper.in(RelabelApplication::getStatus, PENDING_PROCESSING, CUSTOMER_CONFIRM_PENDING,
|
||||
"REJECTED", "CANCELLED");
|
||||
} else if ("COMPLETED".equals(requestedStatus)) {
|
||||
wrapper.in(RelabelApplication::getStatus, READY_FOR_OUTBOUND, "COMPLETED");
|
||||
} else if (StringUtils.hasText(requestedStatus)) {
|
||||
wrapper.eq(RelabelApplication::getStatus, requestedStatus);
|
||||
}
|
||||
LambdaQueryWrapper<RelabelApplication> wrapper = com.baiyue.lastleg.relabel.model.RelabelQueryFilter.build(customerId, query);
|
||||
Page<RelabelApplication> page = applications.selectPage(new Page<>(query.page(), query.size()), wrapper);
|
||||
List<RelabelResponses.Summary> records = page.getRecords().stream().map(value -> summary(value, itemList(value.getId()))).toList();
|
||||
return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
|
||||
+3
-2
@@ -529,6 +529,7 @@ public class ReturnInventoryService {
|
||||
if (cartons.updateById(carton) != 1) throw conflict("退货箱状态已变化,请重新核对");
|
||||
}
|
||||
|
||||
/** 包裹直接沿用签收实物编码,避免同一实物生成两套标签。 */
|
||||
@Transactional
|
||||
public ReturnInventoryCarton createReceiptCarton(ReturnOrder order, ReturnReceipt receipt,
|
||||
String requestedMark, List<ReceiptLine> lines) {
|
||||
@@ -537,8 +538,8 @@ public class ReturnInventoryService {
|
||||
required(lines.get(0).skuCode(), "SKU 不能为空");
|
||||
String cartonMark = text(requestedMark);
|
||||
long cartonId = IdWorker.getId();
|
||||
// One durable barcode identifies a physical parcel, irrespective of SKU count.
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = "RP-" + Long.toString(receipt.getId(), 36).toUpperCase(java.util.Locale.ROOT);
|
||||
// 包裹签收已生成 PK 标签,后续库存、废弃和扫码沿用同一编码。
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = normalizedUnitCode;
|
||||
else if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
+9
-1
@@ -458,7 +458,9 @@ public class ReturnReceiptService {
|
||||
RelabelApplication app = new RelabelApplication(); app.setId(appId); app.setCustomerId(customerId); app.setRelabelNo(relabelNo); app.setLastlegOrderId(tail.getId());
|
||||
app.setCustomerOrderNo(order.getReturnNo()); app.setOverseasWarehouseId(order.getOverseasWarehouseId());
|
||||
app.setLabelDeadline(now.plusDays(7));
|
||||
app.setContactName(text(order.getWarehouseContactName())); app.setContactPhone(text(order.getWarehouseContactPhone()));
|
||||
// 退货预报的仓库联系人选填;转换标时保留空值语义并满足申请表非空约束。
|
||||
app.setContactName(java.util.Objects.requireNonNullElse(text(order.getWarehouseContactName()), ""));
|
||||
app.setContactPhone(java.util.Objects.requireNonNullElse(text(order.getWarehouseContactPhone()), ""));
|
||||
app.setFulfillmentStatus("WAIT_APPROVAL"); app.setRemark(text(request.cartonRemark())); app.setStatus("PENDING_REVIEW"); app.setItemCount(request.items().size()); app.setTotalPackageCount(Math.toIntExact(totalQuantity)); app.setNotifiedPackageCount(Math.toIntExact(totalQuantity)); app.setChargeAmount(java.math.BigDecimal.ZERO); app.setChargeCurrency("CNY"); app.setWalletStatus("NOT_REQUIRED"); app.setIdempotencyKey(key); app.setRequestHash(requestHash); app.setCreatedBy(text(operatorId)); app.setCreatedAt(now); app.setUpdatedAt(now); relabelApplications.insert(app);
|
||||
List<ReturnInventoryCarton> availableCartons = cartons.findAvailable(customerId, order.getOverseasWarehouseId());
|
||||
Map<Long, ReturnInventoryCarton> selectedCartons = new LinkedHashMap<>();
|
||||
@@ -577,6 +579,11 @@ public class ReturnReceiptService {
|
||||
String unitType = norm(request.unitType());
|
||||
if (unitType == null) unitType = "PALLET";
|
||||
String unitCode = text(request.unitCode());
|
||||
// 箱和包裹编码会直接生成 Code128 标签,入账前拒绝无法打印的编码。
|
||||
if (unitCode != null && Set.of("BOX", "PARCEL").contains(unitType)) {
|
||||
try { new com.google.zxing.oned.Code128Writer().encode(unitCode); }
|
||||
catch (IllegalArgumentException invalid) { throw badRequest("实物编码无法生成条码,请使用 ASCII 编码"); }
|
||||
}
|
||||
ReturnOrder order = orders.lockNo(text(request.returnNo()));
|
||||
if (order == null || !String.valueOf(identity.overseasWarehouseId()).equals(order.getOverseasWarehouseId())) {
|
||||
throw notFound("当前绑定仓没有该退货单");
|
||||
@@ -869,3 +876,4 @@ public class ReturnReceiptService {
|
||||
private static ResponseStatusException notFound(String message) { return new ResponseStatusException(HttpStatus.NOT_FOUND, message); }
|
||||
private record Allocation(ReturnOrderItem item, long quantity) { }
|
||||
}
|
||||
|
||||
|
||||
+22
@@ -40,6 +40,28 @@ import org.apache.ibatis.annotations.Select;
|
||||
)
|
||||
|
||||
|
||||
UNION ALL
|
||||
SELECT c.carton_mark,c.sku_code,NULL,c.product_quantity,NULL,NULL,
|
||||
(SELECT JSON_ARRAYAGG(JSON_OBJECT('skuCode',s.sku_code,'quantity',s.product_quantity))
|
||||
FROM ll_dropship_carton_sku s WHERE s.customer_id=c.customer_id AND s.carton_id=c.id)
|
||||
FROM ll_dropship_carton c
|
||||
JOIN ll_order o ON o.id=c.order_id AND o.customer_id=c.customer_id
|
||||
JOIN ll_dropship_order_profile p ON p.id=c.profile_id AND p.customer_id=c.customer_id
|
||||
JOIN ll_inventory_position pos ON pos.id=p.inventory_position_id AND pos.customer_id=c.customer_id
|
||||
JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=c.customer_id
|
||||
WHERE c.customer_id=#{customerId} AND o.inbound_code=#{inboundCode}
|
||||
AND o.overseas_warehouse_id=#{warehouseId} AND c.status='AVAILABLE'
|
||||
AND p.status='ACTIVE' AND pos.available_packages>0 AND b.available_packages>0
|
||||
AND NOT EXISTS (SELECT 1 FROM ll_inbound_plan_carton existing
|
||||
WHERE existing.customer_id=c.customer_id AND existing.order_id=c.order_id
|
||||
AND existing.carton_mark=c.carton_mark)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM ll_relabel_item ri
|
||||
JOIN ll_relabel_application ra ON ra.id=ri.application_id AND ra.customer_id=ri.customer_id
|
||||
WHERE ri.customer_id=c.customer_id AND ri.source_inbound_code=o.inbound_code
|
||||
AND ra.overseas_warehouse_id=#{warehouseId}
|
||||
AND UPPER(TRIM(COALESCE(NULLIF(ri.source_carton_mark,''),ri.carton_mark)))=UPPER(TRIM(c.carton_mark))
|
||||
AND ra.status NOT IN ('CANCELLED','REJECTED'))
|
||||
ORDER BY cartonMark
|
||||
""")
|
||||
java.util.List<SourceCarton> sourceCartons(@Param("customerId") String customerId,
|
||||
|
||||
+19
@@ -3,11 +3,30 @@ package com.baiyue.lastleg.transfer.mapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import java.util.List;
|
||||
|
||||
/** 使用提交单据的原箱快照表达业务占用,不提前扣减实物库存。 */
|
||||
@Mapper
|
||||
public interface TransferSourceReservationMapper {
|
||||
/** 在出库事务中按原箱身份更新来源箱,保留箱内件数和清点时间。 */
|
||||
@Update("""
|
||||
UPDATE ll_warehouse_transfer_carton original
|
||||
JOIN ll_warehouse_transfer_order origin_order
|
||||
ON origin_order.id=original.transfer_order_id AND origin_order.customer_id=original.customer_id
|
||||
JOIN ll_warehouse_transfer_order outgoing
|
||||
ON outgoing.customer_id=origin_order.customer_id AND outgoing.inbound_code=origin_order.inbound_code
|
||||
AND outgoing.operating_warehouse_id=origin_order.operating_warehouse_id
|
||||
JOIN ll_warehouse_transfer_carton selected
|
||||
ON selected.customer_id=outgoing.customer_id AND selected.transfer_order_id=outgoing.id
|
||||
AND COALESCE(NULLIF(selected.customer_carton_mark,''),selected.carton_mark)=original.carton_mark
|
||||
SET original.status='SHIPPED',original.updated_at=CURRENT_TIMESTAMP
|
||||
WHERE outgoing.customer_id=#{customerId} AND outgoing.id=#{transferOrderId}
|
||||
AND outgoing.transfer_no<>outgoing.inbound_code AND origin_order.transfer_no=origin_order.inbound_code
|
||||
AND original.status='COUNTED'
|
||||
""")
|
||||
int shipSelectedCartons(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId);
|
||||
|
||||
/** 按同一入仓快照串行提交,防止不同草稿同时抢占原箱。 */
|
||||
@Select("SELECT id FROM ll_inbound_order WHERE customer_id=#{customerId} AND id=#{inboundOrderId} FOR UPDATE")
|
||||
Long lockInbound(@Param("customerId") String customerId, @Param("inboundOrderId") Long inboundOrderId);
|
||||
|
||||
+5
@@ -21,6 +21,11 @@ public class TransferInventorySourceRepository {
|
||||
return reservations.occupiedMarks(customerId, warehouseId, sourceCode);
|
||||
}
|
||||
|
||||
/** 按当前转运实际选择的箱唛记录来源箱出库,不按箱数截断历史箱集合。 */
|
||||
public int shipSelectedCartons(String customerId, Long transferOrderId) {
|
||||
return reservations.shipSelectedCartons(customerId, transferOrderId);
|
||||
}
|
||||
|
||||
/** 读取当前来源单未被有效换标申请使用的箱级快照。 */
|
||||
public List<InboundSnapshotMapper.SourceCarton> sourceCartons(String customerId, String warehouseId, String inboundCode) {
|
||||
return snapshots.sourceCartons(customerId, warehouseId, inboundCode);
|
||||
|
||||
+12
-3
@@ -1063,8 +1063,13 @@ public class WarehouseTransferService {
|
||||
boolean hasSkuReceiptDifference(List<WarehouseTransferCarton> cartons, List<WarehouseTransferItem> items) {
|
||||
Map<String, Long> expected = new java.util.HashMap<>();
|
||||
Map<String, Long> received = new java.util.HashMap<>();
|
||||
Set<String> unknown = new java.util.HashSet<>();
|
||||
Map<Long, WarehouseTransferItem> byId = items.stream().collect(Collectors.toMap(WarehouseTransferItem::getId, Function.identity()));
|
||||
for (var item : items) expected.merge(item.getSkuCode(), item.getDeclaredQuantity() == null ? 0L : item.getDeclaredQuantity(), Long::sum);
|
||||
// 同SKU任一申报缺失时整体不可比较,不能把未知件数视作零制造差异。
|
||||
for (var item : items) {
|
||||
if (item.getDeclaredQuantity() == null) unknown.add(item.getSkuCode());
|
||||
else expected.merge(item.getSkuCode(), item.getDeclaredQuantity(), Long::sum);
|
||||
}
|
||||
for (var carton : cartons) {
|
||||
var contents = cartonContents(carton);
|
||||
if (contents.isEmpty()) {
|
||||
@@ -1075,8 +1080,10 @@ public class WarehouseTransferService {
|
||||
for (var line : contents) received.merge(line.skuCode(), line.quantity(), Long::sum);
|
||||
}
|
||||
}
|
||||
return expected.entrySet().stream().anyMatch(entry -> !entry.getValue().equals(received.getOrDefault(entry.getKey(), 0L)))
|
||||
|| received.entrySet().stream().anyMatch(entry -> !entry.getValue().equals(expected.getOrDefault(entry.getKey(), 0L)));
|
||||
return expected.entrySet().stream().anyMatch(entry -> !unknown.contains(entry.getKey())
|
||||
&& !entry.getValue().equals(received.getOrDefault(entry.getKey(), 0L)))
|
||||
|| received.entrySet().stream().anyMatch(entry -> !unknown.contains(entry.getKey())
|
||||
&& !entry.getValue().equals(expected.getOrDefault(entry.getKey(), 0L)));
|
||||
}
|
||||
|
||||
private List<WarehouseTransferCarton> requireCountedCartons(WarehouseTransferOrder transfer) {
|
||||
@@ -1520,6 +1527,8 @@ public class WarehouseTransferService {
|
||||
allocation.setStatus("SHIPPED"); allocation.setActivePositionKey(null);
|
||||
allocation.setShippedAt(now); allocation.setUpdatedAt(now); inventoryAllocations.updateById(allocation);
|
||||
}
|
||||
// 同一事务记录所选原箱已出库,当前库存候选据此排除;签收快照仍完整保留。
|
||||
inventorySources.shipSelectedCartons(customerId, transfer.getId());
|
||||
transfer.setStatus(OUTBOUNDED); transfer.setOutboundIdempotencyKey(key);
|
||||
transfer.setOutboundRequestHash(requestHash); transfer.setOutboundAt(now); transfer.setUpdatedAt(now);
|
||||
transfers.updateById(transfer);
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.lastleg.security.InternalServiceTokenGuard;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.List;
|
||||
/** 服务令牌保护的来源事实接口。 */
|
||||
@RestController
|
||||
@RequestMapping("/internal/lastleg/v1/order-workflow-facts")
|
||||
public class InternalOrderWorkflowFactController {
|
||||
private final InternalServiceTokenGuard guard;
|
||||
private final OrderWorkflowFactService service;
|
||||
/** 注入服务身份守卫。 */
|
||||
public InternalOrderWorkflowFactController(InternalServiceTokenGuard guard,OrderWorkflowFactService service) { this.guard=guard;this.service=service; }
|
||||
/** 身份验证后批量查询,不暴露公网。 */
|
||||
@PostMapping
|
||||
public ApiResponse<List<OrderWorkflowFacts.Fact>> read(@RequestHeader(value="X-Internal-Service-Token",required=false) String token,@Valid @RequestBody OrderWorkflowFacts.Request request) {
|
||||
guard.verify(token);return ApiResponse.success(service.read(request));
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
/** 以不可变来源明细核对出库,取消不再遮挡待排车。 */
|
||||
@Mapper
|
||||
public interface OrderWorkflowFactMapper {
|
||||
/** 精确核对客户及来源;字符串参数沿用列排序规则,避免连接默认排序规则冲突。 */
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT o.customer_id tenant_id,o.source_inbound_id inbound_id,
|
||||
EXISTS(SELECT 1 FROM ll_outbound_pallet_item i JOIN ll_outbound_order b ON b.id=i.outbound_order_id AND b.customer_id=i.customer_id
|
||||
WHERE i.customer_id=o.customer_id AND i.source_order_id=o.id AND b.status NOT IN ('CANCELLED','CANCELED')) outbound_exists,
|
||||
EXISTS(SELECT 1 FROM ll_outbound_pallet_item i JOIN ll_outbound_order b ON b.id=i.outbound_order_id AND b.customer_id=i.customer_id
|
||||
WHERE i.customer_id=o.customer_id AND i.source_order_id=o.id AND b.status NOT IN ('CANCELLED','CANCELED','OUTBOUNDED')) pending_outbound,
|
||||
EXISTS(SELECT 1 FROM ll_outbound_pallet_item i JOIN ll_outbound_order b ON b.id=i.outbound_order_id AND b.customer_id=i.customer_id
|
||||
JOIN ll_delivery_order d ON d.outbound_order_id=b.id AND d.customer_id=b.customer_id
|
||||
WHERE i.customer_id=o.customer_id AND i.source_order_id=o.id AND b.status='OUTBOUNDED' AND d.status='DELIVERING' AND d.dispatched_at IS NOT NULL
|
||||
AND (d.destination_warehouse_code=i.destination_warehouse_code OR d.destination_warehouse_id=i.destination_warehouse_id
|
||||
OR (d.destination_type='PRIVATE' AND EXISTS(SELECT 1 FROM ll_dispatch_item di
|
||||
WHERE di.id=i.dispatch_item_id AND di.customer_id=i.customer_id AND di.dispatch_order_id=b.dispatch_order_id
|
||||
AND di.destination_type='PRIVATE' AND TRIM(di.delivery_address)=TRIM(d.delivery_address)
|
||||
AND COALESCE(TRIM(di.postal_code),'')=COALESCE(TRIM(d.postal_code),''))))) delivering,
|
||||
EXISTS(SELECT 1 FROM ll_inbound_plan p WHERE p.customer_id=o.customer_id AND p.source_order_id=o.id AND p.status IN ('COMPLETED','PENDING_RECONCILIATION')) unloaded
|
||||
FROM ll_order o WHERE o.source_region='FIRSTLEG' AND o.order_type NOT IN ('RELABEL_APPLICATION','WAREHOUSE_TRANSFER')
|
||||
AND <foreach collection='sources' item='s' open='(' separator=' OR ' close=')'>
|
||||
(o.customer_id=#{s.tenantKey} AND o.source_inbound_id=#{s.inboundKey})
|
||||
</foreach>
|
||||
</script>
|
||||
""")
|
||||
List<OrderWorkflowFacts.Fact> read(@Param("sources") List<OrderWorkflowFacts.Source> sources);
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
/** 尾程作业事实持久化入口。 */
|
||||
@Repository
|
||||
public class OrderWorkflowFactRepository {
|
||||
private final OrderWorkflowFactMapper mapper;
|
||||
/** 注入事实SQL提供者。 */
|
||||
public OrderWorkflowFactRepository(OrderWorkflowFactMapper mapper) { this.mapper=mapper; }
|
||||
/** 空来源不能扩大查询。 */
|
||||
public List<OrderWorkflowFacts.Fact> read(List<OrderWorkflowFacts.Source> sources) { return sources.isEmpty()?List.of():mapper.read(sources); }
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
/** 只读提供国内订单列表所需的真实作业事实。 */
|
||||
@Service
|
||||
public class OrderWorkflowFactService {
|
||||
private final OrderWorkflowFactRepository repository;
|
||||
/** 注入领域仓储。 */
|
||||
public OrderWorkflowFactService(OrderWorkflowFactRepository repository) { this.repository=repository; }
|
||||
/** 批量读取精确来源事实。 */
|
||||
public List<OrderWorkflowFacts.Fact> read(OrderWorkflowFacts.Request request) { return repository.read(request.sources()); }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.workflow;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.*;
|
||||
import java.util.List;
|
||||
/** 按客户和来源入仓单查询独立作业事实。 */
|
||||
public final class OrderWorkflowFacts {
|
||||
/** 仅作为内部契约命名空间,不允许实例化。 */
|
||||
private OrderWorkflowFacts() {}
|
||||
public record Source(@Positive long tenantId,@Positive long inboundId) {
|
||||
/** 字符型客户键以字符串绑定,避免隐式数字比较丢失长整型精度。 */
|
||||
public String tenantKey() { return Long.toString(tenantId); }
|
||||
/** 来源入库键保持原值,不通过数据库CAST引入连接排序规则。 */
|
||||
public String inboundKey() { return Long.toString(inboundId); }
|
||||
}
|
||||
public record Request(@NotEmpty @Size(max=500) List<@NotNull @Valid Source> sources) {}
|
||||
public record Fact(long tenantId,long inboundId,boolean outboundExists,boolean pendingOutbound,boolean delivering,boolean unloaded) {}
|
||||
}
|
||||
Reference in New Issue
Block a user