This commit is contained in:
2026-08-17 19:51:54 +08:00
parent cddd23e4c7
commit 45b51415cf
84 changed files with 1278 additions and 158 deletions
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.dto;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -12,7 +13,7 @@ public record InboundOrderSummary(
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
String feeStatus, String feeReviewStatus, String notificationStatus,
Integer version,
LocalDateTime expectedArrivalAt,
LocalDateTime expectedArrivalAt, LocalDateTime requestedPickupAt,
Long customerId, String customerName, Long companyId, String companyName,
Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName,
@@ -26,18 +27,20 @@ public record InboundOrderSummary(
String locationCodes,
LocalDateTime measureCompletedAt, LocalDateTime inboundedAt,
String expressCompanyCode, String expressCompanyName, String expressTrackingNo,
String loadingStatus, String containerNo, String cabinetNo,
String containerNo, String cabinetNo,
String vesselName, String voyageNo, LocalDateTime sailingAt, LocalDateTime portArrivalAt,
LocalDateTime createdAt, LocalDateTime updatedAt
) {
public static InboundOrderSummary from(
InboundOrderEntity order, InboundTransportSnapshotEntity transport, String locationCodes) {
InboundOrderEntity order, InboundTransportSnapshotEntity transport,
InboundPickupSnapshotEntity pickup, String locationCodes) {
return new InboundOrderSummary(
order.getId(), order.getTenantId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
order.getVersion(),
order.getExpectedArrivalAt(), order.getCustomerId(), order.getCustomerName(),
order.getExpectedArrivalAt(), pickup == null ? null : pickup.getPickupAt(),
order.getCustomerId(), order.getCustomerName(),
order.getCompanyId(), order.getCompanyName(), order.getWarehouseId(), order.getWarehouseName(),
order.getDeliveryWarehouseId(), order.getDeliveryWarehouseName(), order.getBusinessType(),
order.getChannelCode(), order.getChannelName(), order.getTaxMode(), order.getPricingTaxMode(),
@@ -50,7 +53,6 @@ public record InboundOrderSummary(
locationCodes,
order.getMeasureCompletedAt(), order.getInboundedAt(), order.getExpressCompanyCode(),
order.getExpressCompanyName(), order.getExpressTrackingNo(),
transport == null ? null : transport.getLoadingStatus(),
transport == null ? null : transport.getContainerNo(),
transport == null ? null : transport.getCabinetNo(),
transport == null ? null : transport.getVesselName(),
@@ -166,9 +166,12 @@ public final class LoadingListDtos {
Long itemId,
Long accountId,
String customerName,
Long orderId,
String orderNo,
Long inboundId,
Long inboundDetailId,
String inboundCode,
String containerNo,
String skuCode,
String destinationWarehouse,
String chineseName,
@@ -31,8 +31,25 @@ public record OrderApprovedInboundCommand(
String publicRemark,
String expressCompanyName,
String expressTrackingNo,
Integer totalPackageCount
Integer totalPackageCount,
String pickupMethod,
LocalDateTime requestedPickupAt
) {
public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName,
LocalDateTime expectedArrivalAt, String businessType, String channelCode, String channelName,
Long carrierId, String carrierName, String taxMode, String pricingTaxMode, String containerMode,
Boolean customsInspection, String destinationType, Long destinationWarehouseId, String destinationCode,
String destinationName, String publicRemark, String expressCompanyName, String expressTrackingNo,
Integer totalPackageCount
) {
this(tenantId, orderId, orderNo, inboundCode, customerId, customerName, warehouseId, warehouseName,
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, totalPackageCount, null, null);
}
/** 兼容尚未携带订单总箱数的历史事件。 */
public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
@@ -46,7 +63,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null);
expressTrackingNo, null, null, null);
}
/** 兼容未携带报价包税维度的历史调用方。 */
@@ -62,6 +79,6 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null);
expressTrackingNo, null, null, null);
}
}
@@ -62,5 +62,11 @@ public class ContainerBookingEntity {
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
@TableField(exist = false) private Long planId;
@TableField(exist = false) private String planNo;
@TableField(exist = false) private String planStatus;
@TableField(exist = false) private Boolean exceptionActive;
@TableField(exist = false) private String destinationClearanceDocumentStatus;
@TableField(exist = false) private String customsDeclarationDocumentStatus;
@TableField(exist = false) private String deliveryOrderDocumentStatus;
@TableField(exist = false) private String pickupOrderDocumentStatus;
}
@@ -0,0 +1,23 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/** 客户上门提货要求在入库域的快照。 */
@Getter
@Setter
@TableName("fl_inbound_pickup_snapshot")
public class InboundPickupSnapshotEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long inboundId;
private String pickupMethod;
private LocalDateTime pickupAt;
private LocalDateTime createdAt;
}
@@ -62,4 +62,5 @@ public class TruckAppointmentEntity {
@TableField(exist = false) private String bookingAgentName;
@TableField(exist = false) private String cabinetNo;
@TableField(exist = false) private String containerNo;
@TableField(exist = false) private Long attachmentCount;
}
@@ -13,7 +13,20 @@ import org.apache.ibatis.annotations.Update;
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
@Select("""
<script>
SELECT b.*, p.id AS plan_id,
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
THEN TRUE ELSE FALSE END AS exception_active,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DESTINATION_CLEARANCE' LIMIT 1),'MISSING') AS destination_clearance_document_status,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='CUSTOMS_DECLARATION' LIMIT 1),'MISSING') AS customs_declaration_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
@@ -61,7 +74,20 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
@Select("""
SELECT b.*, p.id AS plan_id,
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
THEN TRUE ELSE FALSE END AS exception_active,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DESTINATION_CLEARANCE' LIMIT 1),'MISSING') AS destination_clearance_document_status,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='CUSTOMS_DECLARATION' LIMIT 1),'MISSING') AS customs_declaration_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
@@ -0,0 +1,7 @@
package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface InboundPickupSnapshotMapper extends BaseMapper<InboundPickupSnapshotEntity> {
}
@@ -171,16 +171,19 @@ public interface LoadingListQueryMapper {
@Param("operatorName") String operatorName);
@Select("""
SELECT id AS item_id, account_id, customer_name, inbound_id, inbound_detail_id,
inbound_code, sku_code, destination_warehouse_name, product_name_zh,
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no,
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
i.sku_code, i.destination_warehouse_name, i.product_name_zh,
product_name_en, product_image_url, package_count, pallet_count,
gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg,
product_quantity, units_per_carton, fba_tracking_no, shipment_no,
shipping_mark, internal_remark, customer_remark, warehouse_remark,
load_sequence, frozen_at
FROM fl_loading_plan_item
WHERE loading_plan_id = #{planId} AND deleted = 0
ORDER BY load_sequence, id
FROM fl_loading_plan_item i
JOIN fl_loading_plan p ON p.id=i.loading_plan_id
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
WHERE i.loading_plan_id = #{planId} AND i.deleted = 0
ORDER BY i.load_sequence, i.id
""")
List<ItemRow> selectItems(@Param("planId") long planId);
@@ -337,9 +340,12 @@ public interface LoadingListQueryMapper {
private Long itemId;
private Long accountId;
private String customerName;
private Long orderId;
private String orderNo;
private Long inboundId;
private Long inboundDetailId;
private String inboundCode;
private String containerNo;
private String skuCode;
private String destinationWarehouseName;
private String productNameZh;
@@ -2,7 +2,7 @@ package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.TruckAppointmentDtos.Query; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import org.apache.ibatis.annotations.*;
@Mapper public interface TruckAppointmentMapper extends BaseMapper<TruckAppointmentEntity>{
@Select("SELECT * FROM fl_truck_appointment WHERE container_booking_id=#{containerBookingId} AND status NOT IN ('COMPLETED','CANCELLED','EXCEPTION') ORDER BY id FOR UPDATE") java.util.List<TruckAppointmentEntity> activeForUpdate(@Param("containerBookingId") long containerBookingId);
@Select("<script>SELECT * FROM fl_truck_appointment WHERE (#{q.keyword} IS NULL OR appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR vehicle_no LIKE CONCAT('%',#{q.keyword},'%') OR carrier_name LIKE CONCAT('%',#{q.keyword},'%')) AND (#{q.status} IS NULL OR status=#{q.status}) AND (#{q.customerId} IS NULL OR customer_id=#{q.customerId}) AND (#{q.containerBookingId} IS NULL OR container_booking_id=#{q.containerBookingId}) ORDER BY updated_at DESC,id DESC</script>") Page<TruckAppointmentEntity> page(Page<TruckAppointmentEntity> p,@Param("q") Query q);
@Select("<script>SELECT a.*,o.booking_no ocean_booking_no,COALESCE(o.warehouse_name,c.warehouse_name) warehouse_name,o.shipping_company_id,o.shipping_company_name,o.booking_agent_id,o.booking_agent_name,c.cabinet_no,c.container_no,(SELECT COUNT(*) FROM fl_truck_appointment_attachment x WHERE x.appointment_id=a.id) attachment_count FROM fl_truck_appointment a LEFT JOIN fl_ocean_booking o ON o.id=a.ocean_booking_id LEFT JOIN fl_container_booking c ON c.id=a.container_booking_id AND c.record_status='ACTIVE' WHERE (#{q.keyword} IS NULL OR a.appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR a.vehicle_no LIKE CONCAT('%',#{q.keyword},'%') OR a.carrier_name LIKE CONCAT('%',#{q.keyword},'%')) AND (#{q.status} IS NULL OR a.status=#{q.status}) AND (#{q.customerId} IS NULL OR a.customer_id=#{q.customerId}) AND (#{q.containerBookingId} IS NULL OR a.container_booking_id=#{q.containerBookingId}) ORDER BY a.updated_at DESC,a.id DESC</script>") Page<TruckAppointmentEntity> page(Page<TruckAppointmentEntity> p,@Param("q") Query q);
@Update("UPDATE fl_truck_appointment SET status=#{toStatus}, actual_arrived_warehouse_at=CASE WHEN #{toStatus}='ARRIVED_WAREHOUSE' THEN CURRENT_TIMESTAMP(3) ELSE actual_arrived_warehouse_at END, actual_loading_at=CASE WHEN #{toStatus}='LOADING' THEN CURRENT_TIMESTAMP(3) ELSE actual_loading_at END, actual_departed_at=CASE WHEN #{toStatus}='DEPARTED' THEN CURRENT_TIMESTAMP(3) ELSE actual_departed_at END, revision=revision+1, version=version+1, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int transition(@Param("id") long id,@Param("fromStatus") String from,@Param("toStatus") String to,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET status='COMPLETED',actual_loading_at=COALESCE(actual_loading_at,CURRENT_TIMESTAMP(3)),revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int completeLoadingFromPda(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET planned_loading_at=#{at}, revision=revision+1, version=version+1, status='RESCHEDULED', remark=#{reason}, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND version=#{version}") int reschedule(@Param("id") long id,@Param("at") java.time.LocalDateTime at,@Param("reason") String reason,@Param("version") int version);
@@ -54,4 +54,7 @@ public class OceanBookingEntity {
private String updatedByName;
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
@TableField(exist = false) private String shipperName;
@TableField(exist = false) private String consigneeName;
@TableField(exist = false) private Long containerCount;
}
@@ -15,16 +15,24 @@ import java.time.LocalDateTime;
public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Select("""
<script>
SELECT * FROM fl_ocean_booking
WHERE (#{query.customerId} IS NULL OR customer_id=#{query.customerId})
AND status IN ('CONFIRMED','CHANGE_PENDING','RELEASED','CANCELLED')
AND (#{query.status} IS NULL OR status=#{query.status})
AND (#{query.keyword} IS NULL OR booking_no LIKE CONCAT('%',#{query.keyword},'%')
OR customer_name LIKE CONCAT('%',#{query.keyword},'%')
OR bill_of_lading_no LIKE CONCAT('%',#{query.keyword},'%')
OR vessel_name LIKE CONCAT('%',#{query.keyword},'%')
OR voyage_no LIKE CONCAT('%',#{query.keyword},'%'))
ORDER BY updated_at DESC,id DESC
SELECT booking.*,
(SELECT party.party_name FROM fl_ocean_booking_party party
WHERE party.ocean_booking_id=booking.id AND party.party_type='SHIPPER' LIMIT 1) shipper_name,
(SELECT party.party_name FROM fl_ocean_booking_party party
WHERE party.ocean_booking_id=booking.id AND party.party_type='CONSIGNEE' LIMIT 1) consignee_name,
(SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation
JOIN fl_container_booking container ON container.id=allocation.container_booking_id
WHERE allocation.ocean_booking_id=booking.id AND container.record_status='ACTIVE') container_count
FROM fl_ocean_booking booking
WHERE (#{query.customerId} IS NULL OR booking.customer_id=#{query.customerId})
AND booking.status IN ('CONFIRMED','CHANGE_PENDING','RELEASED','CANCELLED')
AND (#{query.status} IS NULL OR booking.status=#{query.status})
AND (#{query.keyword} IS NULL OR booking.booking_no LIKE CONCAT('%',#{query.keyword},'%')
OR booking.customer_name LIKE CONCAT('%',#{query.keyword},'%')
OR booking.bill_of_lading_no LIKE CONCAT('%',#{query.keyword},'%')
OR booking.vessel_name LIKE CONCAT('%',#{query.keyword},'%')
OR booking.voyage_no LIKE CONCAT('%',#{query.keyword},'%'))
ORDER BY booking.updated_at DESC,booking.id DESC
</script>
""")
Page<OceanBookingEntity> selectPage(Page<OceanBookingEntity> page, @Param("query") Query query);
@@ -20,7 +20,8 @@ public final class OutboundDispatchDtos {
public record Aggregate(Long orderId, String orderNo, Long inboundId, String inboundCode, Long tenantId,
Long customerId, String customerName, String inboundStatus,
Long containerBookingId, String containerNo, Long loadingPlanId, String loadingPlanStatus,
Long containerBookingId, String cabinetNo, String containerNo,
Long loadingPlanId, String loadingPlanStatus,
Long pickTaskId, String pickTaskStatus, Long oceanBookingId, String oceanBookingStatus,
Long truckAppointmentId, String truckAppointmentStatus,
String dispatchStatus, Integer version, GateResult gate) {}
@@ -15,7 +15,7 @@ public interface OutboundDispatchReadMapper {
SELECT DISTINCT inbound.order_id orderId,inbound.order_no orderNo,inbound.id inboundId,
inbound.inbound_code inboundCode,inbound.tenant_id tenantId,inbound.customer_id customerId,
inbound.customer_name customerName,inbound.process_status inboundStatus,
booking.id containerBookingId,booking.container_no containerNo,
booking.id containerBookingId,booking.cabinet_no cabinetNo,booking.container_no containerNo,
plan.id loadingPlanId,plan.plan_status loadingPlanStatus
FROM fl_loading_plan_item item
JOIN fl_loading_plan plan ON plan.id=item.loading_plan_id
@@ -100,7 +100,7 @@ public interface OutboundDispatchReadMapper {
record Binding(Long orderId, String orderNo, Long inboundId, String inboundCode, Long tenantId,
Long customerId, String customerName, String inboundStatus, Long containerBookingId,
String containerNo, Long loadingPlanId, String loadingPlanStatus) {}
String cabinetNo, String containerNo, Long loadingPlanId, String loadingPlanStatus) {}
record IdStatus(Long id, String status) {}
record Evidence(Integer evidenceCount, Integer validSeal) {}
}
@@ -109,7 +109,8 @@ public class OutboundDispatchService {
GateResult gate = gate(binding, pick, ocean, truck, evidence);
return new Aggregate(binding.orderId(), binding.orderNo(), binding.inboundId(), binding.inboundCode(),
binding.tenantId(), binding.customerId(), binding.customerName(), binding.inboundStatus(),
binding.containerBookingId(), binding.containerNo(), binding.loadingPlanId(), binding.loadingPlanStatus(),
binding.containerBookingId(), binding.cabinetNo(), binding.containerNo(),
binding.loadingPlanId(), binding.loadingPlanStatus(),
id(pick), status(pick), id(ocean), status(ocean), id(truck), status(truck),
dispatch == null ? OutboundDispatchStatus.READY_FOR_DISPATCH.name() : dispatch.getStatus(),
dispatch == null ? 0 : dispatch.getVersion(), gate);
@@ -14,12 +14,14 @@ import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundPalletAllocationEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import com.baiyue.firstleg.mapper.InboundFileMapper;
import com.baiyue.firstleg.mapper.InboundInternalNoteMapper;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.mapper.InboundPalletAllocationMapper;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import com.baiyue.firstleg.mapper.InboundTransportSnapshotMapper;
import com.baiyue.firstleg.mapper.InboundPickupSnapshotMapper;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
@@ -37,6 +39,8 @@ public class InboundOrderRepository {
@Resource
private InboundTransportSnapshotMapper transports;
@Resource
private InboundPickupSnapshotMapper pickupSnapshots;
@Resource
private InboundPalletAllocationMapper pallets;
@Resource
private InboundFileMapper files;
@@ -82,9 +86,11 @@ public class InboundOrderRepository {
Map<Long, Long> tenantByInbound = result.getRecords().stream()
.collect(Collectors.toMap(InboundOrderEntity::getId, InboundOrderEntity::getTenantId));
Map<Long, InboundTransportSnapshotEntity> transportByInbound = transportMap(tenantId, tenantByInbound);
Map<Long, InboundPickupSnapshotEntity> pickupByInbound = pickupMap(tenantId, tenantByInbound);
Map<Long, String> locationCodesByInbound = locationCodesMap(tenantId, tenantByInbound);
List<InboundOrderSummary> records = result.getRecords().stream()
.map(order -> InboundOrderSummary.from(order, transportByInbound.get(order.getId()),
pickupByInbound.get(order.getId()),
locationCodesByInbound.get(order.getId())))
.toList();
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
@@ -191,6 +197,9 @@ public class InboundOrderRepository {
InboundTransportSnapshotEntity transport = transports.selectOne(
owned(InboundTransportSnapshotEntity::getTenantId,
InboundTransportSnapshotEntity::getInboundId, tenantId, inboundId));
InboundPickupSnapshotEntity pickup = pickupSnapshots.selectOne(
owned(InboundPickupSnapshotEntity::getTenantId,
InboundPickupSnapshotEntity::getInboundId, tenantId, inboundId));
List<InboundPalletAllocationEntity> palletAllocations = pallets.selectList(
owned(InboundPalletAllocationEntity::getTenantId,
InboundPalletAllocationEntity::getInboundId, tenantId, inboundId)
@@ -228,7 +237,7 @@ public class InboundOrderRepository {
.map(InboundPalletAllocationEntity::getLocationCode)
.filter(StringUtils::hasText)
.collect(Collectors.joining("、"));
return new InboundOrderDetail(InboundOrderSummary.from(order, transport, locationCodes),
return new InboundOrderDetail(InboundOrderSummary.from(order, transport, pickup, locationCodes),
order.getPublicRemark(),
palletSummaries, fileSummaries, noteSummaries, historySummaries);
}
@@ -247,6 +256,17 @@ public class InboundOrderRepository {
Function.identity(), (left, right) -> left));
}
public Map<Long, InboundPickupSnapshotEntity> pickupMap(
Long tenantId, Map<Long, Long> tenantByInbound) {
if (tenantByInbound.isEmpty()) return Map.of();
return pickupSnapshots.selectList(new LambdaQueryWrapper<InboundPickupSnapshotEntity>()
.eq(tenantId != null, InboundPickupSnapshotEntity::getTenantId, tenantId)
.in(InboundPickupSnapshotEntity::getInboundId, tenantByInbound.keySet()))
.stream().filter(item -> owns(tenantByInbound, item.getInboundId(), item.getTenantId()))
.collect(Collectors.toMap(InboundPickupSnapshotEntity::getInboundId,
Function.identity(), (left, right) -> left));
}
public Map<Long, String> locationCodesMap(Long tenantId, Map<Long, Long> tenantByInbound) {
if (tenantByInbound.isEmpty()) {
return Map.of();
@@ -171,7 +171,8 @@ public class LoadingListRepository {
private List<Item> items(List<ItemRow> rows) {
return rows.stream().map(row -> new Item(row.getItemId(), row.getAccountId(), row.getCustomerName(),
row.getInboundId(), row.getInboundDetailId(), row.getInboundCode(), row.getSkuCode(),
row.getOrderId(), row.getOrderNo(), row.getInboundId(), row.getInboundDetailId(),
row.getInboundCode(), row.getContainerNo(), row.getSkuCode(),
row.getDestinationWarehouseName(), row.getProductNameZh(), row.getProductNameEn(),
row.getProductImageUrl(), integer(row.getPackageCount()), integer(row.getPalletCount()),
decimal(row.getGrossWeightKg()), decimal(row.getNetWeightKg()), decimal(row.getVolumeCbm()),
@@ -9,8 +9,10 @@ import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.dto.InboundOrderReference;
import com.baiyue.firstleg.entity.FulfillmentTaskEntity;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import com.baiyue.firstleg.entity.InboundStatusHistoryEntity;
import com.baiyue.firstleg.mapper.InboundOrderMapper;
import com.baiyue.firstleg.mapper.InboundPickupSnapshotMapper;
import com.baiyue.firstleg.mapper.InboundStatusHistoryMapper;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
@@ -27,6 +29,8 @@ public class InboundOrderCreationService {
@Resource
private InboundStatusHistoryMapper histories;
@Resource
private InboundPickupSnapshotMapper pickupSnapshots;
@Resource
private FulfillmentTaskProvisioningService fulfillmentTasks;
@Transactional
@@ -37,6 +41,7 @@ public class InboundOrderCreationService {
FulfillmentTaskEntity task = fulfillmentTasks.provision(command, LocalDateTime.now());
linkTask(existing, task.getId());
upgradePreparing(existing, command);
savePickupSnapshot(existing.getId(), command, LocalDateTime.now());
return inboundId;
}
@@ -87,6 +92,7 @@ public class InboundOrderCreationService {
inbound.setVersion(0);
try {
orders.insert(inbound);
savePickupSnapshot(inbound.getId(), command, now);
} catch (DuplicateKeyException duplicate) {
InboundOrderEntity winner = findByOrder(command.tenantId(), command.orderId(), true);
if (winner == null) {
@@ -119,6 +125,7 @@ public class InboundOrderCreationService {
if (existing != null) {
long inboundId = requireMatchingInboundCode(existing, command.inboundCode());
refreshPreparing(existing, command);
savePickupSnapshot(existing.getId(), command, LocalDateTime.now());
return inboundId;
}
@@ -126,6 +133,7 @@ public class InboundOrderCreationService {
InboundOrderEntity inbound = newInbound(command, now, null, "PREPARING", "DRAFT");
try {
orders.insert(inbound);
savePickupSnapshot(inbound.getId(), command, now);
} catch (DuplicateKeyException duplicate) {
InboundOrderEntity winner = findByOrder(command.tenantId(), command.orderId(), true);
if (winner == null) {
@@ -248,6 +256,27 @@ public class InboundOrderCreationService {
histories.insert(history);
}
private void savePickupSnapshot(long inboundId, OrderApprovedInboundCommand command, LocalDateTime now) {
InboundPickupSnapshotEntity existing = pickupSnapshots.selectOne(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InboundPickupSnapshotEntity>()
.eq(InboundPickupSnapshotEntity::getTenantId, command.tenantId())
.eq(InboundPickupSnapshotEntity::getInboundId, inboundId));
if (existing == null) {
InboundPickupSnapshotEntity snapshot = new InboundPickupSnapshotEntity();
snapshot.setId(IdWorker.getId());
snapshot.setTenantId(command.tenantId());
snapshot.setInboundId(inboundId);
snapshot.setPickupMethod(command.pickupMethod());
snapshot.setPickupAt(command.requestedPickupAt());
snapshot.setCreatedAt(now);
pickupSnapshots.insert(snapshot);
} else if ("PREPARING".equals(findByOrder(command.tenantId(), command.orderId(), false).getProcessStatus())) {
existing.setPickupMethod(command.pickupMethod());
existing.setPickupAt(command.requestedPickupAt());
pickupSnapshots.updateById(existing);
}
}
private InboundOrderEntity findByOrder(long tenantId, long orderId, boolean currentRead) {
var query = new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<InboundOrderEntity>()
.eq(InboundOrderEntity::getTenantId, tenantId)
@@ -51,7 +51,7 @@ public class TruckAppointmentService {
private BookingLink validateBookingLink(SaveRequest r){if(r.oceanBookingId()==null||r.oceanBookingId()<=0)throw bad("oceanBookingId不能为空"); if(r.containerBookingId()==null||r.containerBookingId()<=0)throw bad("containerBookingId不能为空"); OceanBookingEntity b=oceanBookings.find(r.oceanBookingId()); if(b==null)throw new BizException(ResultCode.NOT_FOUND,"关联订舱不存在"); if(!"RELEASED".equals(b.getStatus()))throw conflict("仅已放舱订舱可创建拖车预约"); if(b.getCustomerId()==null||!StringUtils.hasText(b.getCustomerName()))throw conflict("关联订舱缺少客户快照"); if(r.customerId()!=null&&r.customerId()>0&&!Objects.equals(r.customerId(),b.getCustomerId()))throw bad("customerId与订舱客户不一致"); if(StringUtils.hasText(r.customerName())&&!r.customerName().trim().equals(b.getCustomerName()))throw bad("customerName与订舱客户快照不一致"); ContainerBookingEntity c=containers.find(r.containerBookingId()); if(c==null)throw new BizException(ResultCode.NOT_FOUND,"关联柜体不存在"); boolean allocated=oceanBookings.allocations(r.oceanBookingId()).stream().anyMatch(a->Objects.equals(a.getContainerBookingId(),r.containerBookingId())); if(!allocated)throw bad("containerBookingId未分配至该订舱"); if(r.warehouseId()!=null&&r.warehouseId()>0&&c.getWarehouseId()!=null&&!Objects.equals(r.warehouseId(),c.getWarehouseId()))throw bad("warehouseId与柜体不一致"); LoadingPlanEntity plan=loadingPlans.planByBooking(r.containerBookingId()); if(plan==null)throw conflict("关联柜次尚未建立装柜计划"); if(r.loadingPlanId()!=null&&!Objects.equals(r.loadingPlanId(),plan.getId()))throw bad("loadingPlanId与柜体当前装柜计划不一致"); return new BookingLink(b,plan);}
private TruckAppointmentStatus next(TruckAppointmentStatus s,String a){return switch(a){case"SUBMIT"->s==TruckAppointmentStatus.DRAFT?TruckAppointmentStatus.PENDING_DISPATCH:badStatus();case"DISPATCH"->s==TruckAppointmentStatus.PENDING_DISPATCH||s==TruckAppointmentStatus.RESCHEDULED?TruckAppointmentStatus.DISPATCHED:badStatus();case"PICKUP"->s==TruckAppointmentStatus.DISPATCHED?TruckAppointmentStatus.PICKUP_IN_PROGRESS:badStatus();case"ARRIVE"->s==TruckAppointmentStatus.PICKUP_IN_PROGRESS?TruckAppointmentStatus.ARRIVED_WAREHOUSE:badStatus();case"LOAD"->s==TruckAppointmentStatus.ARRIVED_WAREHOUSE?TruckAppointmentStatus.LOADING:badStatus();case"DEPART"->s==TruckAppointmentStatus.LOADING?TruckAppointmentStatus.DEPARTED:badStatus();case"COMPLETE"->s==TruckAppointmentStatus.DEPARTED?TruckAppointmentStatus.COMPLETED:badStatus();case"EXCEPTION"->s!=TruckAppointmentStatus.COMPLETED&&s!=TruckAppointmentStatus.CANCELLED?TruckAppointmentStatus.EXCEPTION:badStatus();case"CANCEL"->s!=TruckAppointmentStatus.COMPLETED?TruckAppointmentStatus.CANCELLED:badStatus();default->badStatus();};}
private TruckAppointmentStatus badStatus(){throw conflict("不允许的状态动作");} private TruckAppointmentEntity required(long id){TruckAppointmentEntity e=repo.find(id);if(e==null)throw new BizException(ResultCode.NOT_FOUND,"拖车预约不存在");return e;} private void checkVersion(TruckAppointmentEntity e,int v){if(e.getVersion()!=v)throw conflict("版本已变化,请刷新后重试");} private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);} private static BizException bad(String m){return new BizException(ResultCode.BAD_REQUEST,m);}
private TruckAppointmentEntity enrich(TruckAppointmentEntity value){OceanBookingEntity booking=oceanBookings.find(value.getOceanBookingId());if(booking!=null){value.setOceanBookingNo(booking.getBookingNo());value.setWarehouseName(booking.getWarehouseName());value.setShippingCompanyId(booking.getShippingCompanyId());value.setShippingCompanyName(booking.getShippingCompanyName());value.setBookingAgentId(booking.getBookingAgentId());value.setBookingAgentName(booking.getBookingAgentName());}if(value.getContainerBookingId()!=null){ContainerBookingEntity container=containers.find(value.getContainerBookingId());if(container!=null){value.setCabinetNo(container.getCabinetNo());value.setContainerNo(container.getContainerNo());if(!StringUtils.hasText(value.getWarehouseName()))value.setWarehouseName(container.getWarehouseName());}}return value;}
private TruckAppointmentEntity enrich(TruckAppointmentEntity value){OceanBookingEntity booking=oceanBookings.find(value.getOceanBookingId());if(booking!=null){value.setOceanBookingNo(booking.getBookingNo());value.setWarehouseName(booking.getWarehouseName());value.setShippingCompanyId(booking.getShippingCompanyId());value.setShippingCompanyName(booking.getShippingCompanyName());value.setBookingAgentId(booking.getBookingAgentId());value.setBookingAgentName(booking.getBookingAgentName());}if(value.getContainerBookingId()!=null){ContainerBookingEntity container=containers.find(value.getContainerBookingId());if(container!=null){value.setCabinetNo(container.getCabinetNo());value.setContainerNo(container.getContainerNo());if(!StringUtils.hasText(value.getWarehouseName()))value.setWarehouseName(container.getWarehouseName());}}value.setAttachmentCount((long)repo.attachments(value.getId()).size());return value;}
private void copy(TruckAppointmentEntity e,SaveRequest r,BookingLink link,String carrierName){OceanBookingEntity booking=link.booking();e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(r.containerBookingId());e.setLoadingPlanId(link.plan().getId());e.setWarehouseId(r.warehouseId());e.setCustomerId(booking.getCustomerId());e.setCustomerName(booking.getCustomerName());e.setCarrierId(r.carrierId());e.setCarrierName(carrierName);e.setFleetContact(r.fleetContact());e.setFleetPhone(r.fleetPhone());e.setDriverName(r.driverName());e.setDriverPhone(r.driverPhone());e.setDriverIdCard(r.driverIdCard());e.setVehicleNo(r.vehicleNo());e.setStoreName(r.storeName());e.setStoreContact(r.storeContact());e.setStorePhone(r.storePhone());e.setStoreAddress(r.storeAddress());e.setStoreCity(r.storeCity());e.setStoreZipcode(r.storeZipcode());e.setPackageCount(r.packageCount());e.setGrossWeightKg(r.grossWeightKg());e.setVolumeCbm(r.volumeCbm());e.setPalletCount(r.palletCount());e.setPoNo(r.poNo());e.setPlannedLoadingAt(r.plannedLoadingAt());e.setTransitMinutes(r.transitMinutes());e.setPayableAmount(r.payableAmount());e.setReceivableAmount(r.receivableAmount());e.setCurrency(r.currency()==null?"RMB":r.currency());e.setRemark(r.remark());}
private TruckAppointmentEntity claimOrReplay(long id,String action,String key,String hash){validateKey(key);String normalized=key.trim();if(repo.claimOperation(IdWorker.getId(),id,action,normalized,hash))return null;TruckAppointmentOperationEntity existing=repo.operation(normalized);if(existing==null||!Objects.equals(existing.getAppointmentId(),id)||!action.equals(existing.getAction())||!hash.equals(existing.getRequestHash()))throw conflict("Idempotency-Key已用于不同请求");if(!StringUtils.hasText(existing.getResponseSnapshot()))throw conflict("相同请求正在处理中,请稍后重试");try{return objectMapper.readValue(existing.getResponseSnapshot(),TruckAppointmentEntity.class);}catch(JsonProcessingException ex){throw new IllegalStateException("无法读取拖车预约幂等结果",ex);}}
private TruckAppointmentEntity completeOperation(long id,String action,String key,TruckAppointmentEntity result){try{String normalized=key.trim();if(repo.completeOperation(id,action,normalized,objectMapper.writeValueAsString(result))!=1)throw conflict("拖车预约幂等请求完成状态冲突");return result;}catch(JsonProcessingException ex){throw new IllegalStateException("无法保存拖车预约幂等结果",ex);}}
@@ -77,7 +77,8 @@ public class UnifiedInboundQueryService {
}
InboundTransportSnapshotEntity transport = includePhaseThree
? operations.findTransport(tenantId, inbound.getId()) : null;
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt, transport, null), timeline);
return new UnifiedInboundDetail(summary(inbound, detail, preload, pick, evidence, deliveryAt,
transport, null, null), timeline);
}
private List<UnifiedInboundSummary> summaries(List<InboundOrderEntity> records) {
@@ -93,17 +94,19 @@ public class UnifiedInboundQueryService {
Map<Long, Long> owners = records.stream().collect(Collectors.toMap(
InboundOrderEntity::getId, InboundOrderEntity::getTenantId, (left, right) -> left));
var locationCodes = inbounds.locationCodesMap(null, owners);
var pickups = inbounds.pickupMap(null, owners);
return records.stream().map(inbound -> summary(inbound, null, preloads.get(inbound.getId()),
picks.get(inbound.getId()), evidences.getOrDefault(inbound.getId(), List.of()),
deliveries.get(inbound.getId()) == null ? null : deliveries.get(inbound.getId()).getOccurredAt(),
transports.get(inbound.getId()), locationCodes.get(inbound.getId()))).toList();
transports.get(inbound.getId()), locationCodes.get(inbound.getId()), pickups.get(inbound.getId()))).toList();
}
private UnifiedInboundSummary summary(InboundOrderEntity inbound, InboundOrderDetail detail,
com.baiyue.firstleg.split.entity.PreloadPlanEntity preload,
com.baiyue.firstleg.loading.entity.InboundLoadingPickTaskEntity pick,
List<LoadingEvidenceEntity> evidence, LocalDateTime deliveryAt,
InboundTransportSnapshotEntity transport, String locationCodes) {
InboundTransportSnapshotEntity transport, String locationCodes,
com.baiyue.firstleg.entity.InboundPickupSnapshotEntity pickup) {
String mainStatus = mainStatus(inbound.getProcessStatus());
List<LoadingEvidenceEntity> activeEvidence = evidence.stream()
.filter(item -> item.getStatus() == null || "ACTIVE".equals(item.getStatus())).toList();
@@ -122,7 +125,7 @@ public class UnifiedInboundQueryService {
"DELIVERED".equals(mainStatus) ? "COMPLETED" : "PENDING", deliveryAt,
inbound.getExpressTrackingNo()) : null;
var inboundSummary = detail == null
? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, locationCodes) : detail.summary();
? com.baiyue.firstleg.dto.InboundOrderSummary.from(inbound, transport, pickup, locationCodes) : detail.summary();
return new UnifiedInboundSummary(inboundSummary, mainStatus,
nextAction(mainStatus, inbound.getProcessStatus(), preload == null ? null : preload.getStatus()),
preloadSummary, loadingSummary, deliverySummary);