Merge remote-tracking branch 'origin/test' into test

This commit is contained in:
2026-10-16 16:21:51 +08:00
14 changed files with 473 additions and 48 deletions
@@ -27,6 +27,16 @@ import java.util.List;
@RequestMapping("/api/merchant/v1/last-leg/warehouse-transfers")
public class MerchantWarehouseTransferController {
@Resource private WarehouseTransferService service;
@Resource private com.baiyue.lastleg.transfer.service.WarehouseTransferForecastService forecastService;
/** 测量完成前追加预报箱唛,保留原编号和已测量内容。 */
@PostMapping("/{transferNo}/increase-forecast-cartons")
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
public ApiResponse<WarehouseTransferResponses.Detail> increaseForecastCartons(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
@Valid @RequestBody WarehouseTransferRequests.IncreaseForecastCartons request) {
return ApiResponse.success(forecastService.increase(customer(user), transferNo, operator(user), request));
}
@GetMapping("/inventory-source-cartons")
@PreAuthorize("hasAuthority('" + OverseasPermissions.MERCHANT_TRANSFER_CREATE + "')")
@@ -84,6 +84,13 @@ public class PdaWarehouseTransferController {
return ApiResponse.success(service.pdaCartonLabel(identity.overseasWarehouseId(), transferNo));
}
@PostMapping("/{transferNo}/cartons/reopen")
public ApiResponse<WarehouseTransferResponses.Detail> reopenMeasurement(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo) {
PdaWarehouseIdentity identity = PdaWarehouseIdentity.require(user);
return ApiResponse.success(service.reopenMeasurement(identity.overseasWarehouseId(), transferNo));
}
@PostMapping("/{transferNo}/cartons/count")
public ApiResponse<WarehouseTransferResponses.Detail> countCartons(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String transferNo,
@@ -105,10 +105,19 @@ public final class WarehouseTransferRequests {
public record Outbound(@NotBlank @Size(max = 255) String idempotencyKey,
@Size(max = 1000) String remark) {}
/** 仅增加既有SKU的预报箱数,携带原箱数用于并发核对及重试去重。 */
public record IncreaseForecastCartons(
@Positive @Max(5000) int expectedPackageCount,
@NotNull Long transferItemId,
@Min(0) @Max(5000) int expectedItemPackageCount,
@Positive @Max(5000) int packageCount
) {}
public record CountCartons(
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 5000) List<@Valid @NotNull CountCarton> boxes,
@Size(max = 1000) String discrepancyReason
@Size(max = 1000) String discrepancyReason,
@NotNull @Positive Integer forecastPackageCount
) {}
public record CountCarton(
@@ -127,8 +136,14 @@ public final class WarehouseTransferRequests {
public record Palletize(
@NotBlank @Size(max = 255) String idempotencyKey,
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds
) {}
@NotEmpty @Size(max = 20) List<@NotNull Long> photoFileIds,
@Size(max = 1000) String discrepancyReason
) {
/** 无差异的既有调用不需要填写原因。 */
public Palletize(String idempotencyKey, List<Long> photoFileIds) {
this(idempotencyKey, photoFileIds, null);
}
}
public record PalletDraft(
Long palletId,
@@ -10,6 +10,15 @@ import java.util.List;
@Mapper
public interface WarehouseTransferCartonMapper extends BaseMapper<WarehouseTransferCarton> {
/** 按客户及本单范围保存完成原因,允许清空重测前的差异说明。 */
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET discrepancy_reason=#{reason}, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId}")
int saveDiscrepancyReason(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId, @Param("reason") String reason);
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET counted_at=NULL, counted_by=NULL, count_idempotency_key=NULL, count_request_hash=NULL, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId}")
int reopenMeasurement(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId);
@org.apache.ibatis.annotations.Update("UPDATE ll_warehouse_transfer_carton SET transfer_pallet_id=NULL, updated_at=CURRENT_TIMESTAMP WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} AND transfer_pallet_id=#{palletId}")
int unassignPallet(@Param("customerId") String customerId, @Param("transferOrderId") Long transferOrderId, @Param("palletId") Long palletId);
@Select("SELECT * FROM ll_warehouse_transfer_carton WHERE customer_id=#{customerId} AND transfer_order_id=#{transferOrderId} ORDER BY carton_index")
List<WarehouseTransferCarton> listOrder(@Param("customerId") String customerId,
@Param("transferOrderId") Long transferOrderId);
@@ -0,0 +1,82 @@
package com.baiyue.lastleg.transfer.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baiyue.lastleg.transfer.entity.*;
import com.baiyue.lastleg.transfer.mapper.*;
import com.baiyue.lastleg.entity.LastlegOrder;
import com.baiyue.lastleg.entity.LastlegInboundOrder;
import com.baiyue.lastleg.entity.LastlegOrderDetail;
import com.baiyue.lastleg.entity.LastlegOrderStatusHistory;
import com.baiyue.lastleg.mapper.*;
import com.baiyue.lastleg.inboundplan.entity.InboundPlanLine;
import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
/** 转运预报补箱的数据访问,统一保留原箱号及客户隔离。 */
@Repository
public class TransferForecastRepository {
@Resource private WarehouseTransferOrderMapper transfers;
@Resource private WarehouseTransferItemMapper items;
@Resource private WarehouseTransferCartonMapper cartons;
@Resource private LastlegOrderMapper orders;
@Resource private LastlegInboundOrderMapper inboundOrders;
@Resource private LastlegOrderDetailMapper orderDetails;
@Resource private LastlegOrderStatusHistoryMapper histories;
@Resource private InboundPlanLineMapper planLines;
/** 与PDA清点及完工锁定同一转运单,避免补箱与完工并发。 */
public WarehouseTransferOrder lock(String customerId, String transferNo) {
return transfers.lockNo(customerId, transferNo);
}
/** 按创建顺序读取预报明细,保持关联尾程明细的序号稳定。 */
public List<WarehouseTransferItem> items(String customerId, Long transferId) {
return items.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
.eq(WarehouseTransferItem::getCustomerId, customerId)
.eq(WarehouseTransferItem::getTransferOrderId, transferId).orderByAsc(WarehouseTransferItem::getId));
}
/** 读取原箱号用于确定新增编号,不删除或重建原箱。 */
public List<WarehouseTransferCarton> cartons(String customerId, Long transferId) {
return cartons.listOrder(customerId, transferId);
}
/** 追加预报箱并同步各预报口径;清点数据保留,整单清点确认失效。 */
public void append(WarehouseTransferOrder transfer, WarehouseTransferItem item, int itemIndex,
List<WarehouseTransferCarton> added, LocalDateTime now, String operatorId) {
String customerId = transfer.getCustomerId();
var order = orders.selectOne(new LambdaQueryWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, transfer.getLastlegOrderId()));
if (order == null) throw new IllegalStateException("转运关联尾程订单不存在");
items.updateById(item);
transfers.updateById(transfer);
for (var carton : added) cartons.insert(carton);
cartons.update(null, new LambdaUpdateWrapper<WarehouseTransferCarton>()
.eq(WarehouseTransferCarton::getCustomerId, customerId).eq(WarehouseTransferCarton::getTransferOrderId, transfer.getId())
.set(WarehouseTransferCarton::getTotalCartons, transfer.getPackageCount()).set(WarehouseTransferCarton::getUpdatedAt, now));
cartons.reopenMeasurement(customerId, transfer.getId());
orders.update(null, new LambdaUpdateWrapper<LastlegOrder>()
.eq(LastlegOrder::getCustomerId, customerId).eq(LastlegOrder::getId, transfer.getLastlegOrderId())
.set(LastlegOrder::getPackageCount, transfer.getPackageCount()).set(LastlegOrder::getUpdatedAt, now));
inboundOrders.update(null, new LambdaUpdateWrapper<LastlegInboundOrder>()
.eq(LastlegInboundOrder::getCustomerId, customerId).eq(LastlegInboundOrder::getId, transfer.getInboundOrderId())
.set(LastlegInboundOrder::getPackageCount, transfer.getPackageCount()).set(LastlegInboundOrder::getUpdatedAt, now));
int updated = orderDetails.update(null, new LambdaUpdateWrapper<LastlegOrderDetail>()
.eq(LastlegOrderDetail::getCustomerId, customerId).eq(LastlegOrderDetail::getOrderId, transfer.getLastlegOrderId())
.eq(LastlegOrderDetail::getSourceInboundDetailId, transfer.getTransferNo() + ":" + itemIndex)
.set(LastlegOrderDetail::getPackageCount, item.getPackageCount()));
if (updated != 1) throw new IllegalStateException("转运关联尾程货物明细不完整");
planLines.update(null, new LambdaUpdateWrapper<InboundPlanLine>()
.eq(InboundPlanLine::getCustomerId, customerId).eq(InboundPlanLine::getOrderId, transfer.getLastlegOrderId())
.set(InboundPlanLine::getRequiredPackageCount, transfer.getPackageCount()));
var history = new LastlegOrderStatusHistory(); history.setCustomerId(customerId); history.setOrderId(transfer.getLastlegOrderId());
history.setFromStatus(order.getCurrentStatus()); history.setToStatus(order.getCurrentStatus());
history.setEventId(transfer.getTransferNo()); history.setCreatedAt(now);
history.setRemark("客户增加预报箱数,补充箱唛" + added.size() + "个;操作人:" + operatorId);
histories.insert(history);
}
}
@@ -0,0 +1,16 @@
package com.baiyue.lastleg.transfer.repository;
import com.baiyue.lastleg.transfer.mapper.WarehouseTransferCartonMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
/** 封装转运测量完成时的箱级差异说明持久化。 */
@Repository
public class TransferMeasurementRepository {
@Resource private WarehouseTransferCartonMapper cartons;
/** 整单统一保存原因,无差异时显式清空重测前的旧原因。 */
public void saveDiscrepancyReason(String customerId, Long transferOrderId, String reason) {
cartons.saveDiscrepancyReason(customerId, transferOrderId, reason);
}
}
@@ -0,0 +1,59 @@
package com.baiyue.lastleg.transfer.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
import com.baiyue.lastleg.transfer.entity.WarehouseTransferCarton;
import com.baiyue.lastleg.transfer.repository.TransferForecastRepository;
import com.baiyue.lastleg.service.LastlegOrderNoGenerator;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Set;
/** 测量完成前补充预报箱,只追加编号,不重建既有箱号。 */
@Service
public class WarehouseTransferForecastService {
@Resource private TransferForecastRepository repository;
@Resource private WarehouseTransferService transfers;
/** 锁单校验原预报快照,追加箱号并同步预报箱数,重复提交不重复生成。 */
@Transactional
public WarehouseTransferResponses.Detail increase(String customerId, String transferNo, String operatorId,
WarehouseTransferRequests.IncreaseForecastCartons request) {
var transfer = repository.lock(customerId, transferNo);
if (transfer == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "转运单不存在");
if (!transferNo.equals(transfer.getInboundCode()) || !Set.of("PENDING_REVIEW", "WAIT_PDA_RECEIVING").contains(transfer.getStatus()))
throw new ResponseStatusException(HttpStatus.CONFLICT, "仅测量完成前的外来转运单可增加预报箱数");
var items = repository.items(customerId, transfer.getId());
var item = items.stream().filter(row -> row.getId().equals(request.transferItemId())).findFirst()
.orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "SKU不属于当前转运单"));
int addedCount = request.packageCount() - request.expectedItemPackageCount();
long total = (long) request.expectedPackageCount() + addedCount;
if (addedCount <= 0 || total > 5000 || request.expectedPackageCount() <= 0)
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "只能增加预报箱数,整单最多5000箱");
// 相同目标值视为已完成的重试;不同原快照则要求刷新,避免多端覆盖。
if (item.getPackageCount() == request.packageCount() && transfer.getPackageCount() == total)
return transfers.detail(customerId, transferNo);
if (transfer.getPackageCount() != request.expectedPackageCount() || item.getPackageCount() != request.expectedItemPackageCount())
throw new ResponseStatusException(HttpStatus.CONFLICT, "预报箱数已变化,请刷新后重试");
var existing = repository.cartons(customerId, transfer.getId());
int index = existing.stream().mapToInt(WarehouseTransferCarton::getCartonIndex).max().orElse(0);
LocalDateTime now = LocalDateTime.now();
var added = new ArrayList<WarehouseTransferCarton>();
for (int i = 0; i < addedCount; i++) {
var carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId()); carton.setCustomerId(customerId);
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(item.getId());
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId()));
carton.setCartonIndex(++index); carton.setTotalCartons((int) total); carton.setStatus("EXPECTED");
carton.setCreatedAt(now); carton.setUpdatedAt(now); added.add(carton);
}
item.setPackageCount(request.packageCount()); transfer.setPackageCount((int) total); transfer.setUpdatedAt(now);
repository.append(transfer, item, items.indexOf(item), added, now, operatorId);
return transfers.detail(customerId, transferNo);
}
}
@@ -120,6 +120,7 @@ public class WarehouseTransferService {
@Resource private WarehouseTransferOrderMapper transfers;
@Resource private WarehouseTransferItemMapper transferItems;
@Resource private WarehouseTransferCartonMapper transferCartons;
@Resource private com.baiyue.lastleg.transfer.repository.TransferMeasurementRepository transferMeasurementRepository;
@Resource private WarehouseTransferSourceOrderMapper sourceOrders;
@Resource private WarehouseTransferPalletMapper transferPallets;
@Resource private WarehouseTransferPalletItemMapper transferPalletItems;
@@ -697,13 +698,13 @@ public class WarehouseTransferService {
return cartonLabel(transfer.getCustomerId(), transferNo);
}
/** 提交预报后打印整单固定箱唛,测量及未收状态不改变箱号集合。 */
@Transactional(readOnly = true)
public WarehouseTransferResponses.Label cartonLabel(String customerId, String transferNo) {
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
if (transfer == null) throw notFound("Warehouse transfer order not found");
if (DRAFT.equals(transfer.getStatus())) throw conflict("Submit the warehouse transfer order before printing carton labels");
List<WarehouseTransferResponses.Carton> cartons = transferCartons.listOrder(customerId, transfer.getId()).stream()
.filter(value -> !"NOT_RECEIVED".equals(value.getStatus()))
.map(value -> new WarehouseTransferResponses.Carton(value.getCartonMark(), value.getCustomerCartonMark(), value.getCartonIndex(), value.getTotalCartons(), value.getStatus(), value.getTransferItemId(), value.getTransferPalletId(), value.getLengthCm(), value.getWidthCm(), value.getHeightCm(), value.getGrossWeightKg(), value.getCountedQuantity(), value.getDiscrepancyReason(), value.getCountedBy(), value.getCountedAt(), cartonContents(value))).toList();
StringBuilder labels = new StringBuilder();
for (WarehouseTransferResponses.Carton carton : cartons) {
@@ -816,11 +817,24 @@ public class WarehouseTransferService {
WarehouseTransferOrder transfer = requirePdaMeasuringTransfer(warehouseId, transferNo);
WarehouseTransferPallet pallet = transferPallets.lockOne(transfer.getCustomerId(), transfer.getId(), palletId);
if (pallet == null) return detail(transfer.getCustomerId(), transferNo);
if (!"DRAFT".equals(pallet.getStatus())) throw conflict("Generated pallet cannot be removed");
if (!Set.of("DRAFT", "GENERATED", "BOUND").contains(pallet.getStatus())) throw conflict("当前托盘不能删除");
if (pallet.getWarehouseResourceLockId() != null) {
warehouseResources.release(transfer.getCustomerId(), pallet.getWarehouseResourceLockId());
}
transferCartons.unassignPallet(transfer.getCustomerId(), transfer.getId(), palletId);
transferCartons.reopenMeasurement(transfer.getCustomerId(), transfer.getId());
transferPallets.deleteById(palletId);
return detail(transfer.getCustomerId(), transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail reopenMeasurement(long warehouseId, String transferNo) {
WarehouseTransferOrder transfer = requirePdaMeasuringTransfer(warehouseId, transferNo);
// Preserve dimensions, SKU contents and assignments; invalidate the previous count request.
transferCartons.reopenMeasurement(transfer.getCustomerId(), transfer.getId());
return detail(transfer.getCustomerId(), transferNo);
}
@Transactional
public WarehouseTransferResponses.Detail generatePallet(long warehouseId, String transferNo, long palletId,
WarehouseTransferRequests.PalletAction request) {
@@ -874,6 +888,7 @@ public class WarehouseTransferService {
return detail(transfer.getCustomerId(), transferNo);
}
/** 保存逐箱实测及托盘归属,按整托汇总体积并保留三位小数。 */
@Transactional
public WarehouseTransferResponses.Detail countCartons(String operatorId, long warehouseId, String transferNo,
WarehouseTransferRequests.CountCartons request) {
@@ -881,6 +896,12 @@ public class WarehouseTransferService {
if (transfer == null) throw notFound("当前仓没有该转运单");
String customerId = transfer.getCustomerId();
List<WarehouseTransferCarton> cartons = transferCartons.listOrder(customerId, transfer.getId());
// 客户补箱后旧设备快照不可确认短收,必须刷新取得新增的预报箱唛。
if (!java.util.Objects.equals(request.forecastPackageCount(), transfer.getPackageCount()))
throw conflict("预报箱数已变化,请刷新后继续清点");
// PDA只清点客户预报的固定箱唛;不足时由客户补充预报,禁止现场另造编号。
if (request.boxes().stream().anyMatch(box -> !StringUtils.hasText(box.cartonMark())))
throw badRequest("请先由客户增加预报箱数,刷新取得箱唛后再清点");
String requestHash = hash(request);
boolean sameKey = cartons.stream().anyMatch(box -> request.idempotencyKey().equals(box.getCountIdempotencyKey()));
if (sameKey) {
@@ -891,8 +912,7 @@ public class WarehouseTransferService {
if (!WAIT_PDA_RECEIVING.equals(transfer.getStatus())) throw conflict("转运单已结束入库测量");
if (cartons.stream().anyMatch(box -> box.getCountedAt() != null)) throw conflict("本单已完成清点,请刷新查看结果");
if (cartons.isEmpty()) throw conflict("转运单缺少箱号预报");
if (request.boxes().size() != transfer.getPackageCount() && !StringUtils.hasText(request.discrepancyReason()))
throw badRequest("实收箱数与预报不一致时必须填写差异原因");
// 清点阶段只记录实收事实,差异原因在最终完成测量时强制填写。
Map<String, WarehouseTransferCarton> byMark = cartons.stream().collect(Collectors.toMap(WarehouseTransferCarton::getCartonMark, Function.identity()));
Map<Long, WarehouseTransferItem> byItem = transferItems.selectList(new LambdaQueryWrapper<WarehouseTransferItem>()
.eq(WarehouseTransferItem::getCustomerId, customerId).eq(WarehouseTransferItem::getTransferOrderId, transfer.getId()))
@@ -908,10 +928,9 @@ public class WarehouseTransferService {
|| !Set.of("GENERATED", "BOUND").contains(targetPallet.getStatus()))
throw badRequest("请先创建本单托盘号,再选择箱子所属托盘");
var carton = byMark.get(input.cartonMark());
if (StringUtils.hasText(input.cartonMark()) && (carton == null || !seen.add(input.cartonMark())))
if (carton == null || !seen.add(input.cartonMark()))
throw badRequest("箱号不属于本单或重复填写");
if (carton == null && input.transferItemId() == null) throw badRequest("新增实收箱必须选择本单申报明细");
var item = byItem.get(carton == null ? input.transferItemId() : carton.getTransferItemId());
var item = byItem.get(carton.getTransferItemId());
if (item == null) throw badRequest("箱子申报明细不属于本单");
if (input.items() == null || input.items().isEmpty() || input.items().size() > 200)
throw badRequest("每箱必须填写 1 至 200 行 SKU");
@@ -934,16 +953,8 @@ public class WarehouseTransferService {
carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash);
carton.setDiscrepancyReason(trim(request.discrepancyReason())); carton.setUpdatedAt(now); transferCartons.updateById(carton);
}
int nextIndex = cartons.stream().mapToInt(WarehouseTransferCarton::getCartonIndex).max().orElse(0);
for (var input : request.boxes()) {
var carton = byMark.get(input.cartonMark());
boolean added = carton == null;
if (added) {
carton = new WarehouseTransferCarton(); carton.setId(IdWorker.getId());
carton.setTransferOrderId(transfer.getId()); carton.setTransferItemId(input.transferItemId()); carton.setCustomerId(customerId);
carton.setCartonIndex(++nextIndex); carton.setTotalCartons(transfer.getPackageCount());
carton.setCartonMark(LastlegOrderNoGenerator.compactCartonMark(carton.getId())); carton.setCreatedAt(now);
}
carton.setTransferPalletId(input.transferPalletId());
carton.setLengthCm(input.length()); carton.setWidthCm(input.width()); carton.setHeightCm(input.height());
carton.setGrossWeightKg(input.weight());
@@ -957,14 +968,14 @@ public class WarehouseTransferService {
carton.setCountedBy(operatorId); carton.setCountedAt(now); carton.setStatus("COUNTED");
carton.setCountIdempotencyKey(request.idempotencyKey()); carton.setCountRequestHash(requestHash); carton.setUpdatedAt(now);
carton.setDiscrepancyReason(trim(request.discrepancyReason()));
if (added) transferCartons.insert(carton); else transferCartons.updateById(carton);
transferCartons.updateById(carton);
}
// Aggregate only the submitted, physically assigned boxes; never infer SKU placement from declaration order.
// 仅汇总实际分配到本托的实收箱,体积在整托汇总后四舍五入保存三位,避免逐箱舍入累积误差。
for (var pallet : countedPallets) {
var assigned = request.boxes().stream().filter(box -> pallet.getId().equals(box.transferPalletId())).toList();
pallet.setCartonCount(assigned.size());
pallet.setVolumeCbm(assigned.stream().map(box -> box.length().multiply(box.width()).multiply(box.height())
.movePointLeft(6)).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(6, java.math.RoundingMode.HALF_UP));
.movePointLeft(6)).reduce(BigDecimal.ZERO, BigDecimal::add).setScale(3, java.math.RoundingMode.HALF_UP));
pallet.setGrossWeightKg(assigned.stream().map(WarehouseTransferRequests.CountCarton::weight).reduce(BigDecimal.ZERO, BigDecimal::add));
pallet.setUpdatedAt(now); transferPallets.updateById(pallet);
}
@@ -981,6 +992,26 @@ public class WarehouseTransferService {
}
}
/** 按 SKU 汇总预报及实收件数;混箱逐项累计,兼容原单 SKU 箱记录。 */
boolean hasSkuReceiptDifference(List<WarehouseTransferCarton> cartons, List<WarehouseTransferItem> items) {
Map<String, Long> expected = new java.util.HashMap<>();
Map<String, Long> received = new java.util.HashMap<>();
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);
for (var carton : cartons) {
var contents = cartonContents(carton);
if (contents.isEmpty()) {
var item = byId.get(carton.getTransferItemId());
if (item == null) throw conflict("实收箱缺少有效SKU归属,请重新清点");
received.merge(item.getSkuCode(), carton.getCountedQuantity() == null ? 0L : carton.getCountedQuantity(), Long::sum);
} else {
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)));
}
private List<WarehouseTransferCarton> requireCountedCartons(WarehouseTransferOrder transfer) {
var cartons = transferCartons.listOrder(transfer.getCustomerId(), transfer.getId());
if (cartons.isEmpty() || cartons.stream().anyMatch(box -> box.getCountedAt() == null)
@@ -1004,6 +1035,7 @@ public class WarehouseTransferService {
}
}
/** 核对清点、照片、托盘及差异说明,在同一事务内完成测量并入库。 */
@Transactional
public WarehouseTransferResponses.Detail palletize(String operatorId, long overseasWarehouseId,
String transferNo,
@@ -1032,6 +1064,13 @@ public class WarehouseTransferService {
if (items.isEmpty() || items.stream().mapToInt(WarehouseTransferItem::getPackageCount).sum() != transfer.getPackageCount()) {
throw conflict("Warehouse transfer declared cargo is incomplete");
}
// 按每种 SKU 的件数核对实收,箱数变化不触发,不同 SKU 的差异不能抵消。
boolean skuDifference = hasSkuReceiptDifference(countedCartons, items);
String discrepancyReason = skuDifference ? trim(request.discrepancyReason()) : null;
if (skuDifference && !StringUtils.hasText(discrepancyReason))
throw badRequest("SKU实收件数与预报不一致时必须填写差异原因");
if (discrepancyReason != null && discrepancyReason.length() > 1000)
throw badRequest("差异原因不能超过1000字");
if (request.photoFileIds() == null || request.photoFileIds().isEmpty() || request.photoFileIds().size() > 20) {
throw conflict("Measurement and palletization require 1 to 20 on-site photos");
}
@@ -1052,6 +1091,8 @@ public class WarehouseTransferService {
List<DerivedPalletAllocation> derivedAllocations = deriveCountedPalletAllocations(countedCartons, persistedPallets);
LocalDateTime now = LocalDateTime.now();
List<WarehouseTransferPalletItem> completedPalletItems = new ArrayList<>();
// 与测量完成处于同一事务;短收箱与实收箱保持同一说明,供详情回显。
transferMeasurementRepository.saveDiscrepancyReason(customerId, transfer.getId(), discrepancyReason);
for (int index = 0; index < persistedPallets.size(); index++) {
WarehouseTransferPallet pallet = persistedPallets.get(index);
for (DerivedPalletAllocation allocation : derivedAllocations) {