This commit is contained in:
2026-09-11 16:17:16 +08:00
parent 2d928d667b
commit 51ce609ab1
57 changed files with 942 additions and 139 deletions
@@ -60,4 +60,5 @@ public interface PrintPdfClient {
record DropshipPalletSkuRow(String inboundCode, String skuCode, String cargoName,
int cartonCount, int skuQuantity) {}
}
@@ -1,12 +1,16 @@
package com.baiyue.firstleg.domain;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/** 首版固定渠道目录;上传名称只能映射到这些稳定编码。 */
public final class PriceBookChannelCatalog {
private PriceBookChannelCatalog() {}
private static final Set<String> SEA_RAIL_CHANNEL_CODES = Set.of(
"EXX_RAIL_NY", "NYEXX", "OA_CHICAGO_RAIL", "CPHT");
public static final List<ChannelDefinition> CHANNELS = List.of(
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡", "ZBYHK", "ZBYHWK"),
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡", "BYHK", "BYHWK"),
@@ -37,6 +41,11 @@ public final class PriceBookChannelCatalog {
inboundCodePrefix, overseasInboundCodePrefix);
}
public static boolean isSeaRail(String channelCode) {
return channelCode != null
&& SEA_RAIL_CHANNEL_CODES.contains(channelCode.trim().toUpperCase(Locale.ROOT));
}
public record ChannelDefinition(
String channelCode,
String displayName,
@@ -23,6 +23,7 @@ public interface LoadingListQueryMapper {
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
p.created_by, p.created_by_name, p.created_at, p.version,
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
b.channel_code, b.outbound_confirmed_at, b.actual_empty_reported_at,
b.vessel_name, b.voyage_no, b.container_type,
(SELECT COUNT(*) FROM fl_preload_loading_plan_link preload_link
WHERE preload_link.loading_plan_id = p.id) AS preload_link_count,
@@ -51,6 +52,7 @@ public interface LoadingListQueryMapper {
p.id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
p.created_by, p.created_by_name, p.created_at, p.version,
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
b.channel_code, b.outbound_confirmed_at, b.actual_empty_reported_at,
b.vessel_name, b.voyage_no, b.container_type
""";
@@ -394,6 +396,9 @@ public interface LoadingListQueryMapper {
private String cabinetNo;
private Long warehouseId;
private String warehouseName;
private String channelCode;
private LocalDateTime outboundConfirmedAt;
private LocalDateTime actualEmptyReportedAt;
private String vesselName;
private String voyageNo;
private String containerType;
@@ -15,7 +15,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
@@ -47,7 +46,7 @@ public class OceanBookingPrintService {
String fileName = booking.getBookingNo() + "-" + suffix + ".pdf";
PrintPdfClient.FreightA4PdfRequest request = new PrintPdfClient.FreightA4PdfRequest(
template, A4, fileName, List.of(new PrintPdfClient.FreightDocument(
fields(template, detail), List.of(cargo(detail.containerRequirements())))));
fields(template, detail), List.of(emptyCargo()))));
try {
ResponseEntity<byte[]> response = print.generateFreightA4Documents(internalToken, requestId, request);
MediaType contentType = response.getHeaders().getContentType();
@@ -78,12 +77,12 @@ public class OceanBookingPrintService {
fields.put("placeOfDelivery", text(booking.getPlaceOfDelivery()));
fields.put("serviceTerm", text(booking.getServiceTerm()));
fields.put("freightTerm", text(booking.getFreightTerm()));
fields.put("totalPackagesInWords", Integer.toString(totalContainers(detail.containerRequirements())));
fields.put("totalPackagesInWords", "");
if (SHIPPING_INSTRUCTION.equals(template)) {
fields.put("toCompany", text(booking.getBookingAgentName()));
fields.put("jobNumber", text(booking.getBookingNo()));
fields.put("etdDate", date(booking.getEstimatedSailingAt()));
fields.put("cargoQuantity", Integer.toString(totalContainers(detail.containerRequirements())));
fields.put("cargoQuantity", "");
fields.put("carrier", text(booking.getShippingCompanyName()));
fields.put("bookingAgent", text(booking.getBookingAgentName()));
fields.put("remark", text(booking.getRemark()));
@@ -99,18 +98,8 @@ public class OceanBookingPrintService {
return fields;
}
private static PrintPdfClient.CargoRow cargo(List<OceanBookingContainerRequirementEntity> requirements) {
BigDecimal gross = requirements.stream().map(value -> value.getMaxWeightKg()
.multiply(BigDecimal.valueOf(value.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal volume = requirements.stream().map(value -> value.getCapacityCbm()
.multiply(BigDecimal.valueOf(value.getQuantity()))).reduce(BigDecimal.ZERO, BigDecimal::add);
return new PrintPdfClient.CargoRow("N/M", Integer.toString(totalContainers(requirements)),
"CONTAINERS", requirementSummary(requirements), decimal(gross, 3) + " KGS",
decimal(volume, 4) + " CBM");
}
private static int totalContainers(List<OceanBookingContainerRequirementEntity> values) {
return values.stream().mapToInt(OceanBookingContainerRequirementEntity::getQuantity).sum();
private static PrintPdfClient.CargoRow emptyCargo() {
return new PrintPdfClient.CargoRow("—", "—", "—", "—", "—", "—");
}
private static String requirementSummary(List<OceanBookingContainerRequirementEntity> values) {
@@ -128,9 +117,6 @@ public class OceanBookingPrintService {
private static String date(java.time.LocalDateTime value) {
return value == null ? "" : value.format(DateTimeFormatter.ISO_LOCAL_DATE);
}
private static String decimal(BigDecimal value, int scale) {
return value.setScale(scale, RoundingMode.HALF_UP).toPlainString();
}
private static String first(String first, String fallback) { return !text(first).isEmpty() ? first.trim() : text(fallback); }
private static String join(String first, String second) { return List.of(text(first), text(second)).stream().filter(value -> !value.isEmpty()).collect(Collectors.joining(" / ")); }
private static String text(String value) { return value == null ? "" : value.trim(); }
@@ -261,7 +261,7 @@ public class InboundMeasurementService {
ConfirmedWeightTotals confirmedTotals = confirmedWeightTotals(weights);
if (confirmedTotals.palletCount() == 0) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"请至少上传并确认一张称重图片后再提交测量审核");
"即使只有一箱,也必须上传并确认至少一张称重图片后再提交测量审核");
}
requireConfirmedPackageTotal(inbound, items);
@@ -26,6 +26,7 @@ import com.baiyue.firstleg.repository.LoadingListRepository;
import com.baiyue.firstleg.repository.LoadingPickRepository;
import com.baiyue.firstleg.repository.ContainerRepository;
import com.baiyue.firstleg.domain.Iso6346ContainerNumber;
import com.baiyue.firstleg.domain.PriceBookChannelCatalog;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -138,7 +139,9 @@ public class LoadingListService {
}
public BusinessDocument containerLogisticsDocument(long bookingId,String type){ requireLogisticsType(type); requireBookingPlan(bookingId); return document(repository.document(bookingId,type)); }
public BusinessDocument uploadContainerLogisticsDocument(long bookingId,String type,long operatorId,String operatorName,String key,int expectedVersion,String reason,MultipartFile file){
requireLogisticsType(type); String businessType=fileType(type); return uploadDocument(requireBookingPlan(bookingId),type,businessType,bookingId,Long.toString(bookingId),operatorId,operatorName,key,expectedVersion,reason,file);
requireLogisticsType(type); String businessType=fileType(type); AggregateRow plan=requireBookingPlan(bookingId);
requireDeliveryOrderUploadAllowed(plan);
return uploadDocument(plan,type,businessType,bookingId,Long.toString(bookingId),operatorId,operatorName,key,expectedVersion,reason,file);
}
public DocumentDownload downloadContainerDocument(long bookingId,String type,long fileId){
requireLogisticsType(type); String businessType=fileType(type); requireBookingPlan(bookingId); ContainerDocumentEntity d=requireDocumentEntity(bookingId,type); if(!Long.valueOf(fileId).equals(d.getFileId())) throw notFound("货柜资料不存在");
@@ -190,6 +193,17 @@ public class LoadingListService {
public record DocumentDownload(String fileName,MediaType contentType,byte[] content) {}
private static void requireLogisticsType(String type){ if(!DELIVERY_ORDER.equals(type)) throw badRequest("documentType只能是DELIVERY_ORDER"); }
private static String fileType(String type){ if (DELIVERY_ORDER.equals(type)) return DO_FILE_TYPE; throw badRequest("documentType只能是DELIVERY_ORDER"); }
private static void requireDeliveryOrderUploadAllowed(AggregateRow plan) {
if (!"COMPLETED".equals(plan.getPlanStatus()) || plan.getOutboundConfirmedAt() == null) {
throw conflict("货柜进入海外履约后才可上传DO资料");
}
if (plan.getActualEmptyReportedAt() != null) {
throw conflict("货柜已还柜,不可再上传DO资料");
}
if (PriceBookChannelCatalog.isSeaRail(plan.getChannelCode())) {
throw conflict("海铁渠道不适用DO资料");
}
}
@Transactional
@BusinessAudit(event = "CONTAINER_CLEARANCE_DOCUMENT_ATTACH", resourceType = "CONTAINER",
@@ -92,14 +92,18 @@ public class MeasurementReviewService {
"人工确认箱数合计与入库单总箱数不一致,不能审核通过");
}
var weights = measurements.listWeights(tenantId, inboundId);
if (weights.isEmpty() || weights.stream().anyMatch(weight ->
if (weights.isEmpty()) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"该票测量已提交但没有称重图片;请先退回测量,上传并确认至少一张称重图片后重新提交(即使只有一箱也必须称重)");
}
if (weights.stream().anyMatch(weight ->
!"CONFIRMED".equals(weight.getRecognitionStatus())
|| weight.getGrossWeightKg() == null
|| weight.getGrossWeightKg().signum() <= 0
|| weight.getNetWeightKg() == null
|| weight.getNetWeightKg().signum() <= 0)) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"所有称重结果必须已确认且包含大于零的毛重和货物净重,才能审核通过");
"该票存在未确认或重量无效的称重图片;请先在审核弹框逐张修正并确认后再审核");
}
BigDecimal grossWeight = weights.stream()
.map(com.baiyue.firstleg.entity.InboundWeightResultEntity::getGrossWeightKg)