This commit is contained in:
2026-09-10 11:18:31 +08:00
parent 8731efcbea
commit e9aa2ac047
22 changed files with 374 additions and 52 deletions
@@ -10,6 +10,7 @@ import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.InboundOrderDetail;
import com.baiyue.firstleg.dto.InboundOrderQuery;
import com.baiyue.firstleg.dto.InboundOrderSummary;
import com.baiyue.firstleg.dto.InboundCartonLabels;
import com.baiyue.firstleg.dto.InboundHoldDtos.CommandRequest;
import com.baiyue.firstleg.dto.InboundHoldDtos.Detail;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
@@ -22,6 +23,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Size;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
@@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RequestParam;
@@ -160,6 +163,18 @@ public class PlatformInboundOrderController {
return cartonLabelPrintService.generate(tenantId, inboundId, cartonIndex, requestId);
}
@Operation(summary = "批量生成入仓箱唛 PDF",
description = "按所选客户和入库单聚合生成一个 PDF;首次调用冻结各入库单整单箱号。")
@PostMapping("/carton-label-pdf")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:view')")
public ResponseEntity<byte[]> printCartonLabelsBatch(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestBody @Size(min = 1, max = 200) List<@Valid InboundCartonLabels.PlatformBatchItem> items,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
requirePlatform(user);
return cartonLabelPrintService.generateForPlatformInbounds(items, requestId);
}
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
FirstlegScopeGuard.platform(user, tenantId);
}
@@ -3,6 +3,8 @@ package com.baiyue.firstleg.dto;
import java.math.BigDecimal;
import java.util.List;
import jakarta.validation.constraints.Min;
/** 稳定箱号分配、补打和测量解析契约。 */
public final class InboundCartonLabels {
private InboundCartonLabels() {
@@ -12,6 +14,9 @@ public final class InboundCartonLabels {
List<Label> labels) {
}
public record PlatformBatchItem(@Min(1) long tenantId, @Min(1) long inboundId) {
}
public record Label(Long id, Long inboundDetailId, Integer cargoItemNo, String cargoName,
Integer detailPackageCount, String boxCode, Integer totalCount,
Integer cartonIndex, String boxSpecification, BigDecimal lengthCm,
@@ -286,21 +286,21 @@ public class FbaPricingRepository {
}
public int markInboundAdjusted(long tenantId, long inboundId, String processStatus, String feeStatus,
java.math.BigDecimal selectedWeightKg,
java.math.BigDecimal selectedVolumeCbm,
String chargeUnit, java.math.BigDecimal chargeableQuantity) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId)
.eq(InboundPricingEntity::getId, inboundId)
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.set(InboundPricingEntity::getFeeStatus, feeStatus);
if ("KG".equals(chargeUnit)) {
update.set(InboundPricingEntity::getChargeableWeightKg, chargeableQuantity);
} else {
update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeableQuantity);
}
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeUnit, chargeableQuantity);
return inbounds.update(null, update);
}
public int markInboundCalculated(long tenantId, long inboundId, String processStatus, String chargeMode,
java.math.BigDecimal selectedWeightKg,
java.math.BigDecimal selectedVolumeCbm,
java.math.BigDecimal chargeable) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId)
@@ -312,12 +312,13 @@ public class FbaPricingRepository {
.contains(processStatus)) {
update.set(InboundPricingEntity::getProcessStatus, "FEE_CONFIRMING");
}
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeMode, chargeable);
return inbounds.update(null, update);
}
public int markInboundContainerFeeRecalculated(long tenantId, long inboundId, String processStatus,
java.math.BigDecimal selectedWeightKg,
java.math.BigDecimal selectedVolumeCbm,
String chargeMode, java.math.BigDecimal chargeable) {
LambdaUpdateWrapper<InboundPricingEntity> update = new LambdaUpdateWrapper<InboundPricingEntity>()
.eq(InboundPricingEntity::getTenantId, tenantId)
@@ -325,11 +326,21 @@ public class FbaPricingRepository {
.eq(InboundPricingEntity::getProcessStatus, processStatus)
.in(InboundPricingEntity::getFeeStatus, List.of("CALCULATED", "CONFIRMED"))
.set(InboundPricingEntity::getFeeStatus, "CALCULATED");
if ("KG".equals(chargeMode)) update.set(InboundPricingEntity::getChargeableWeightKg, chargeable);
else update.set(InboundPricingEntity::getChargeableVolumeCbm, chargeable);
applyChargeableSnapshot(update, selectedWeightKg, selectedVolumeCbm, chargeMode, chargeable);
return inbounds.update(null, update);
}
private void applyChargeableSnapshot(LambdaUpdateWrapper<InboundPricingEntity> update,
java.math.BigDecimal selectedWeightKg,
java.math.BigDecimal selectedVolumeCbm,
String chargeUnit, java.math.BigDecimal chargeableQuantity) {
// 列表同时展示两项:当前计费单位保存最终计费量,另一项保存本次计价选定值。
update.set(InboundPricingEntity::getChargeableWeightKg,
"KG".equals(chargeUnit) ? chargeableQuantity : selectedWeightKg)
.set(InboundPricingEntity::getChargeableVolumeCbm,
"CBM".equals(chargeUnit) ? chargeableQuantity : selectedVolumeCbm);
}
public int confirmFee(long tenantId, long feeId, long operatorId, LocalDateTime now) {
return fees.update(null, new LambdaUpdateWrapper<InboundFeeEntity>()
.eq(InboundFeeEntity::getTenantId, tenantId).eq(InboundFeeEntity::getId, feeId)
@@ -166,7 +166,7 @@ public class FbaPricingService {
repository.insertItem(baseItem(tenantId, fee, rate, chargeable, subtotal, now));
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, fee, additional, now));
if (repository.markInboundCalculated(tenantId, inboundId, inbound.getProcessStatus(),
estimate.chargeUnit(), chargeable) != 1) {
estimate.chargeUnit(), weight, volume, chargeable) != 1) {
throw conflict("入库单状态已变化,费用试算未保存");
}
feeDetails.materializeCalculatedFreight(tenantId, inboundId, fee.getId(), operatorId, now);
@@ -342,7 +342,8 @@ public class FbaPricingService {
recalculated.getSubtotalAmount(), now));
if (additional.signum() > 0) repository.insertItem(additionalItem(tenantId, recalculated, additional, now));
if (repository.markInboundContainerFeeRecalculated(tenantId, member.inbound().getId(),
member.inbound().getProcessStatus(), estimate.chargeUnit(), member.chargeableQuantity()) != 1) {
member.inbound().getProcessStatus(), member.current().getActualWeightKg(),
member.current().getActualVolumeCbm(), estimate.chargeUnit(), member.chargeableQuantity()) != 1) {
throw conflict("柜内入库单状态已变化,不能重算阶梯费用");
}
feeDetails.materializeCalculatedFreight(tenantId, member.inbound().getId(), recalculated.getId(),
@@ -564,7 +565,7 @@ public class FbaPricingService {
repository.insertItem(extra);
}
if (repository.markInboundAdjusted(tenantId, inboundId, inbound.getProcessStatus(), adjusted.getFeeStatus(),
adjusted.getChargeUnit(), quantity) != 1) {
adjusted.getActualWeightKg(), adjusted.getActualVolumeCbm(), adjusted.getChargeUnit(), quantity) != 1) {
throw conflict("入库单状态已变化,费用调整未保存");
}
if (published) {
@@ -92,6 +92,27 @@ public class InboundCartonLabelPrintService {
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
}
public ResponseEntity<byte[]> generateForPlatformInbounds(
List<InboundCartonLabels.PlatformBatchItem> items, String requestId) {
if (items == null || items.isEmpty() || items.size() > 200
|| items.stream().anyMatch(item -> item == null || item.tenantId() <= 0 || item.inboundId() <= 0)) {
throw new BizException(ResultCode.BAD_REQUEST, "批量打印必须选择1至200个入库单");
}
List<InboundCartonLabels.PlatformBatchItem> distinctItems = items.stream().distinct().toList();
List<InboundCartonLabels.Label> labels = new ArrayList<>();
for (InboundCartonLabels.PlatformBatchItem item : distinctItems) {
labels.addAll(labelService.list(item.tenantId(), item.inboundId(), null).labels());
}
if (labels.isEmpty()) {
throw new BizException(ResultCode.VALIDATION_FAILED, "所选订单没有可生成的箱唛");
}
ResponseEntity<byte[]> response = generatePdf("carton-labels-" + labels.size() + ".pdf", labels, requestId);
HttpHeaders headers = new HttpHeaders();
headers.putAll(response.getHeaders());
headers.set("X-Carton-Label-Order-Count", String.valueOf(distinctItems.size()));
return ResponseEntity.status(response.getStatusCode()).headers(headers).body(response.getBody());
}
/** 一件代发托盘标签:托盘号、入仓码、SKU箱数和托盘二维码。 */
public ResponseEntity<byte[]> generateDropshipPallet(Pallet pallet, String requestId) {
if (pallet == null || !StringUtils.hasText(pallet.palletNo()) || pallet.items().isEmpty()) {
@@ -12,6 +12,7 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.repository.InboundCartonLabelRepository;
import feign.FeignException;
import jakarta.annotation.Resource;
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -113,7 +114,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
throw new BizException(ResultCode.INTERNAL_ERROR, "预入库单补建后未能读取");
}
return prepared;
} catch (FeignException exception) {
} catch (FeignException | NoFallbackAvailableException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法补建预入库单");
}
}
@@ -177,7 +178,7 @@ public class InboundCartonLabelService implements CartonLabelBindingResolver {
.thenComparing(OrderCargoClient.CargoLabelBinding::cargoId,
java.util.Comparator.nullsLast(Long::compareTo)))
.toList();
} catch (FeignException exception) {
} catch (FeignException | NoFallbackAvailableException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "订单服务暂不可用,无法生成唛头");
}
}