Merge test into dev and resolve conflicts
This commit is contained in:
+24
@@ -6,6 +6,7 @@ import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchBolPrintService;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -13,6 +14,11 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -20,6 +26,8 @@ import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@RestController
|
||||
@ConditionalOnProperty(name = "lastleg.dispatch-query-api.enabled", havingValue = "true")
|
||||
@@ -27,6 +35,7 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@Tag(name = "Tail-leg vehicle dispatch query")
|
||||
public class DispatchQueryController {
|
||||
@Resource private DispatchService service;
|
||||
@Resource private DispatchBolPrintService printService;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
|
||||
@@ -48,6 +57,21 @@ public class DispatchQueryController {
|
||||
return ApiResponse.success(service.platformDetail(dispatchNo));
|
||||
}
|
||||
|
||||
@GetMapping(value = "/{dispatchNo}/prints/bol", produces = MediaType.APPLICATION_PDF_VALUE)
|
||||
@PreAuthorize("hasAuthority('" + OverseasPermissions.DISPATCH_LIST + "')")
|
||||
@Operation(summary = "Generate dispatch BOL as an A4 PDF")
|
||||
public ResponseEntity<byte[]> printBol(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String dispatchNo, HttpServletRequest request) {
|
||||
requirePlatform(user);
|
||||
DispatchBolPrintService.GeneratedPdf pdf = printService.generate(dispatchNo, request.getHeader("X-Request-Id"));
|
||||
ResponseEntity.BodyBuilder response = ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF)
|
||||
.cacheControl(CacheControl.noStore()).header(HttpHeaders.PRAGMA, "no-cache")
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment().filename(pdf.fileName(), StandardCharsets.UTF_8).build().toString())
|
||||
.header("X-Content-Type-Options", "nosniff");
|
||||
if (pdf.pageCount() != null) response.header("X-Print-Page-Count", pdf.pageCount());
|
||||
return response.contentLength(pdf.content().length).body(pdf.content());
|
||||
}
|
||||
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
|
||||
@@ -12,11 +12,16 @@ public record DispatchResponse(Long id, String dispatchNo, String vehiclePlate,
|
||||
int itemCount, int palletCount, int packageCount, int pickedPackageCount,
|
||||
int shippedPackageCount, int remainingPackageCount, String partialCompletionReason,
|
||||
LocalDateTime createdAt,
|
||||
List<Item> items) {
|
||||
List<Item> items, List<BolLine> bolLines) {
|
||||
public record Item(Long id, Long planPalletId, Long warehousePalletId, String palletNo,
|
||||
String overseasWarehouseId, String overseasWarehouseName,
|
||||
String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode,
|
||||
String destinationWarehouseName, String fbaWarehouseCode, int packageCount,
|
||||
BigDecimal volumeCbm, String sourceStage) {
|
||||
}
|
||||
|
||||
public record BolLine(String containerNo, String palletNo, String fbaWarehouseCode,
|
||||
String destinationWarehouseName, String warehouseNo, String fbaShipmentId,
|
||||
String poReferenceNo, int packageCount) {
|
||||
}
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.dispatch.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface DispatchBolMapper {
|
||||
@Select("SELECT " +
|
||||
"COALESCE(MAX(NULLIF(o.container_no,'')),MAX(NULLIF(ip.container_no,''))) AS containerNo," +
|
||||
"MAX(NULLIF(di.pallet_no,'')) AS palletNo," +
|
||||
"COALESCE(MAX(NULLIF(l.destination_warehouse_code,'')),MAX(NULLIF(di.fba_warehouse_code,''))," +
|
||||
"MAX(NULLIF(di.destination_warehouse_code,''))) AS fbaWarehouseCode," +
|
||||
"COALESCE(MAX(NULLIF(l.destination_warehouse_name,'')),MAX(NULLIF(di.destination_warehouse_name,''))) AS destinationWarehouseName," +
|
||||
"COALESCE(MAX(NULLIF(l.inbound_code,'')),MAX(NULLIF(di.inbound_code,''))) AS warehouseNo," +
|
||||
"GROUP_CONCAT(DISTINCT NULLIF(od.shipment_no,'') ORDER BY od.shipment_no SEPARATOR ', ') AS fbaShipmentId," +
|
||||
"GROUP_CONCAT(DISTINCT NULLIF(od.fba_tracking_no,'') ORDER BY od.fba_tracking_no SEPARATOR ', ') AS poReferenceNo," +
|
||||
"CASE WHEN MAX(l.id) IS NULL THEN COALESCE(MAX(di.package_count),0) " +
|
||||
"WHEN COALESCE(MAX(a.picked_package_count),0)>0 THEN MAX(a.picked_package_count) " +
|
||||
"ELSE COALESCE(MAX(a.planned_package_count),0) END AS packageCount " +
|
||||
"FROM ll_dispatch_item di " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet p ON p.id=di.plan_pallet_id " +
|
||||
"LEFT JOIN ll_inbound_plan ip ON ip.id=p.plan_id AND ip.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id AND l.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_order o ON o.id=l.order_id AND o.customer_id=di.customer_id " +
|
||||
"LEFT JOIN ll_order_detail od ON od.order_id=l.order_id AND od.customer_id=di.customer_id " +
|
||||
"WHERE di.customer_id=#{customerId} AND di.dispatch_order_id=#{dispatchOrderId} " +
|
||||
"GROUP BY di.id,l.id ORDER BY di.id,l.id")
|
||||
List<DispatchResponse.BolLine> findByDispatchOrder(@Param("customerId") String customerId,
|
||||
@Param("dispatchOrderId") Long dispatchOrderId);
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient;
|
||||
import com.baiyue.lastleg.client.PrintPdfClient;
|
||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||
import feign.FeignException;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DispatchBolPrintService {
|
||||
private static final String TEMPLATE = "DISPATCH_BILL_OF_LADING_A4_V2";
|
||||
private static final int FIRST_PAGE_ROWS = 18;
|
||||
private static final int CONTINUATION_PAGE_ROWS = 26;
|
||||
private static final PrintPdfClient.PageSpec A4 = new PrintPdfClient.PageSpec(new BigDecimal("210"), new BigDecimal("297"));
|
||||
|
||||
@Resource private DispatchService dispatches;
|
||||
@Resource private PrintPdfClient print;
|
||||
@Resource private DomesticWarehouseDirectoryClient warehouses;
|
||||
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
|
||||
|
||||
public GeneratedPdf generate(String dispatchNo, String requestId) {
|
||||
DispatchResponse detail = dispatches.platformDetail(dispatchNo);
|
||||
if (detail.bolLines() == null || detail.bolLines().isEmpty()) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "排车单没有可打印的BOL明细");
|
||||
}
|
||||
List<PrintPdfClient.Document> documents = documents(detail);
|
||||
String fileName = safeFilePart(detail.dispatchNo()) + "-BOL.pdf";
|
||||
try {
|
||||
ResponseEntity<byte[]> response = print.generateDispatchBol(internalToken, requestId,
|
||||
new PrintPdfClient.Request(TEMPLATE, A4, fileName, documents));
|
||||
byte[] content = response.getBody();
|
||||
MediaType contentType = response.getHeaders().getContentType();
|
||||
if (!response.getStatusCode().is2xxSuccessful() || content == null || content.length == 0
|
||||
|| contentType == null || !MediaType.APPLICATION_PDF.isCompatibleWith(contentType)) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务未返回有效PDF");
|
||||
}
|
||||
return new GeneratedPdf(content, fileName, response.getHeaders().getFirst("X-Print-Page-Count"));
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "打印服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
private List<PrintPdfClient.Document> documents(DispatchResponse detail) {
|
||||
String shipFromAddress = warehouseAddress(detail.overseasWarehouseId());
|
||||
Map<String, List<DispatchResponse.BolLine>> groups = new LinkedHashMap<>();
|
||||
for (DispatchResponse.BolLine line : detail.bolLines()) groups.computeIfAbsent(text(line.fbaWarehouseCode()), ignored -> new ArrayList<>()).add(line);
|
||||
List<PrintPdfClient.Document> documents = new ArrayList<>();
|
||||
for (Map.Entry<String, List<DispatchResponse.BolLine>> group : groups.entrySet()) {
|
||||
List<DispatchResponse.BolLine> lines = group.getValue();
|
||||
int pages = pageCount(lines.size());
|
||||
int total = lines.stream().mapToInt(DispatchResponse.BolLine::packageCount).sum();
|
||||
String destination = lines.stream().map(DispatchResponse.BolLine::destinationWarehouseName).filter(v -> v != null && !v.isBlank()).findFirst().orElse("—");
|
||||
int offset = 0;
|
||||
for (int page = 0; page < pages; page++) {
|
||||
int capacity = page == 0 ? FIRST_PAGE_ROWS : CONTINUATION_PAGE_ROWS;
|
||||
int end = Math.min(lines.size(), offset + capacity);
|
||||
List<PrintPdfClient.CargoRow> rows = lines.subList(offset, end).stream()
|
||||
.map(line -> new PrintPdfClient.CargoRow(text(line.containerNo()), text(line.palletNo()), text(line.fbaWarehouseCode()),
|
||||
text(line.warehouseNo()), text(line.fbaShipmentId()), text(line.poReferenceNo()), line.packageCount())).toList();
|
||||
documents.add(new PrintPdfClient.Document(text(detail.overseasWarehouseName()), shipFromAddress, text(detail.carrierName()),
|
||||
text(detail.fbaAppointmentNo()), dateTime(detail.estimatedArrivalAt()), text(detail.vehiclePlate()), "—",
|
||||
group.getKey(), destination, "—", text(detail.dispatchNo()), unique(lines, true), unique(lines, false),
|
||||
total, page + 1, pages, rows));
|
||||
offset = end;
|
||||
}
|
||||
}
|
||||
return List.copyOf(documents);
|
||||
}
|
||||
|
||||
private String warehouseAddress(String warehouseId) {
|
||||
long id;
|
||||
try {
|
||||
id = Long.parseLong(warehouseId == null ? "" : warehouseId.strip());
|
||||
} catch (NumberFormatException exception) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "排车单未关联有效的海外仓");
|
||||
}
|
||||
try {
|
||||
var response = warehouses.activeWarehouse(id, internalToken);
|
||||
if (response == null || !response.successful() || response.data() == null) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库主数据暂不可用,无法生成BOL");
|
||||
}
|
||||
List<DomesticWarehouseDirectoryClient.Address> addresses = response.data().addresses();
|
||||
if (addresses == null || addresses.isEmpty()) return "—";
|
||||
DomesticWarehouseDirectoryClient.Address address = addresses.stream().filter(Objects::nonNull)
|
||||
.filter(value -> "OPERATING".equals(value.addressType())).findFirst()
|
||||
.orElseGet(() -> addresses.stream().filter(Objects::nonNull).findFirst().orElse(null));
|
||||
if (address == null) return "—";
|
||||
List<String> parts = new ArrayList<>();
|
||||
add(parts, address.addressLine1());
|
||||
add(parts, address.addressLine2());
|
||||
String city = trimmed(address.city());
|
||||
String state = trimmed(address.stateProvince());
|
||||
String postalCode = trimmed(address.postalCode());
|
||||
String region = city == null ? state : state == null ? city : city + ", " + state;
|
||||
if (postalCode != null) region = region == null ? postalCode : region + " " + postalCode;
|
||||
add(parts, region);
|
||||
add(parts, address.countryCode());
|
||||
return parts.isEmpty() ? "—" : String.join(", ", parts);
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "仓库主数据暂不可用,无法生成BOL");
|
||||
}
|
||||
}
|
||||
|
||||
private void add(List<String> parts, String value) {
|
||||
if (value != null && !value.isBlank()) parts.add(value.strip());
|
||||
}
|
||||
|
||||
private String trimmed(String value) {
|
||||
return value == null || value.isBlank() ? null : value.strip();
|
||||
}
|
||||
|
||||
static int pageCount(int rowCount) {
|
||||
if (rowCount <= FIRST_PAGE_ROWS) return 1;
|
||||
return 1 + (rowCount - FIRST_PAGE_ROWS + CONTINUATION_PAGE_ROWS - 1) / CONTINUATION_PAGE_ROWS;
|
||||
}
|
||||
|
||||
private String unique(List<DispatchResponse.BolLine> lines, boolean shipment) {
|
||||
LinkedHashSet<String> values = new LinkedHashSet<>();
|
||||
for (DispatchResponse.BolLine line : lines) {
|
||||
String raw = shipment ? line.fbaShipmentId() : line.poReferenceNo();
|
||||
if (raw != null) for (String value : raw.split(",")) if (!value.isBlank()) values.add(value.strip());
|
||||
}
|
||||
return values.isEmpty() ? "—" : String.join(", ", values);
|
||||
}
|
||||
private String dateTime(java.time.LocalDateTime value) { return value == null ? "—" : value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); }
|
||||
private String text(String value) { return value == null || value.isBlank() ? "—" : value.strip(); }
|
||||
private String safeFilePart(String value) { return text(value).replaceAll("[\\\\/:*?\"<>|]", "-"); }
|
||||
|
||||
public record GeneratedPdf(byte[] content, String fileName, String pageCount) {}
|
||||
}
|
||||
+13
-3
@@ -12,6 +12,7 @@ import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchDraft;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchDraftContainer;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
||||
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
||||
@@ -52,6 +53,7 @@ public class DispatchService {
|
||||
|
||||
@Resource private DispatchOrderMapper orders;
|
||||
@Resource private DispatchItemMapper items;
|
||||
@Resource private DispatchBolMapper bolLines;
|
||||
@Resource private DispatchPalletLockMapper locks;
|
||||
@Resource private DispatchDraftMapper drafts;
|
||||
@Resource private DispatchDraftContainerMapper draftContainers;
|
||||
@@ -475,7 +477,8 @@ public class DispatchService {
|
||||
|
||||
public DispatchResponse detail(String customerId, String dispatchNo) {
|
||||
DispatchOrder dispatch = find(customerId, dispatchNo);
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()),
|
||||
bolLines.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
}
|
||||
|
||||
public DispatchResponse platformDetail(String dispatchNo) {
|
||||
@@ -483,7 +486,8 @@ public class DispatchService {
|
||||
if (dispatch == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||
}
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()),
|
||||
bolLines.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -593,6 +597,11 @@ public class DispatchService {
|
||||
}
|
||||
|
||||
private DispatchResponse response(DispatchOrder order, List<DispatchItem> itemList) {
|
||||
return response(order, itemList, List.of());
|
||||
}
|
||||
|
||||
private DispatchResponse response(DispatchOrder order, List<DispatchItem> itemList,
|
||||
List<DispatchResponse.BolLine> bolLineList) {
|
||||
String orderNos = itemList.stream().flatMap(item -> splitOrderRefs(item.getOrderRefs(), item.getOrderNo()).stream())
|
||||
.distinct().collect(Collectors.joining(","));
|
||||
return new DispatchResponse(order.getId(), order.getDispatchNo(), order.getVehiclePlate(), order.getDriverName(),
|
||||
@@ -609,7 +618,8 @@ public class DispatchService {
|
||||
item.getWarehousePalletId(), item.getPalletNo(), item.getOverseasWarehouseId(), item.getOverseasWarehouseName(),
|
||||
item.getOrderNo(), item.getOrderRefs(), item.getInboundCode(),
|
||||
item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(),
|
||||
Objects.requireNonNullElse(item.getPackageCount(), 0), item.getVolumeCbm(), item.getSourceStage())).toList());
|
||||
Objects.requireNonNullElse(item.getPackageCount(), 0), item.getVolumeCbm(), item.getSourceStage())).toList(),
|
||||
bolLineList == null ? List.of() : bolLineList);
|
||||
}
|
||||
|
||||
private static String route(List<DispatchPalletCandidate> selected) {
|
||||
|
||||
Reference in New Issue
Block a user