优化
This commit is contained in:
@@ -2,5 +2,5 @@ package com.baiyue.firstleg.domain;
|
||||
|
||||
public enum TruckAppointmentStatus {
|
||||
DRAFT, PENDING_DISPATCH, DISPATCHED, PICKUP_IN_PROGRESS, ARRIVED_WAREHOUSE,
|
||||
LOADING, DEPARTED, COMPLETED, RESCHEDULED, EXCEPTION, CANCELLED
|
||||
LOADING, COMPLETED, RESCHEDULED, EXCEPTION, CANCELLED
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ public final class TruckAppointmentDtos { private TruckAppointmentDtos(){}
|
||||
public record Query(@Size(max=64) String keyword, com.baiyue.firstleg.domain.TruckAppointmentStatus status, @Positive Long oceanBookingId, @Min(1) long page, @Min(1) @Max(200) long size){ public Query{if(page==0)page=1;if(size==0)size=20;} }
|
||||
public record CandidateQuery(@Size(max=64) String keyword,@Min(1) long page,@Min(1) @Max(200) long size){public CandidateQuery{if(page==0)page=1;if(size==0)size=20;}}
|
||||
public record Candidate(Long oceanBookingId,String oceanBookingNo,String billOfLadingNo,Long warehouseId,String warehouseName,String channelCode,String channelName,Boolean bookingDocumentReady){}
|
||||
public record SaveRequest(@Positive Long outboundOrderId,@NotNull @Positive Long oceanBookingId,@Positive Long warehouseId,@NotNull @Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=128) String billOfLadingNo,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@Pattern(regexp="RMB|USD") String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){}
|
||||
public record SaveRequest(@Positive Long outboundOrderId,@NotNull @Positive Long oceanBookingId,@Positive Long warehouseId,@NotNull @Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=128) String billOfLadingNo,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") @Digits(integer=16,fraction=2) BigDecimal payableAmount,@Pattern(regexp="RMB") String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){}
|
||||
public record ActionRequest(@NotNull @PositiveOrZero Integer expectedVersion,@Size(max=500) String reason){}
|
||||
public record AttachmentRequest(@NotNull @Positive Long fileId,@NotBlank @Pattern(regexp="DISPATCH_ORDER|DRIVER_DOCUMENT|VEHICLE_DOCUMENT|OTHER") String attachmentType,@Size(max=255) String fileName,@Size(max=128) String fileHash){ }
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface TruckAppointmentCompletionMapper {
|
||||
@Select("SELECT ocean_booking_id FROM fl_ocean_booking_container_allocation WHERE container_booking_id=#{containerId}")
|
||||
Long bookingForContainer(@Param("containerId") long containerId);
|
||||
|
||||
@Select("SELECT * FROM fl_truck_appointment WHERE ocean_booking_id=#{bookingId} "
|
||||
+ "AND status NOT IN ('COMPLETED','CANCELLED','EXCEPTION') ORDER BY id FOR UPDATE")
|
||||
List<TruckAppointmentEntity> activeForUpdate(@Param("bookingId") long bookingId);
|
||||
|
||||
// Current reads are essential: another container can have committed while waiting for the booking lock.
|
||||
@Select("""
|
||||
SELECT r.id requirementId,r.quantity requiredQuantity,c.id containerId,c.container_no containerNo,
|
||||
c.record_status recordStatus,p.id planId,p.plan_status planStatus,
|
||||
i.id itemId,i.account_id customerId,i.order_id orderId,i.order_no orderNo,
|
||||
i.inbound_id inboundId,i.inbound_code inboundCode,i.volume_cbm volumeCbm,
|
||||
d.status dispatchStatus,d.confirmed_at departedAt
|
||||
FROM fl_ocean_booking_container_requirement r
|
||||
LEFT JOIN fl_ocean_booking_container_allocation a ON a.requirement_id=r.id AND a.ocean_booking_id=r.ocean_booking_id
|
||||
LEFT JOIN fl_container_booking c ON c.id=a.container_booking_id
|
||||
LEFT JOIN fl_loading_plan p ON p.container_booking_id=c.id
|
||||
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id=p.id AND i.deleted=0
|
||||
LEFT JOIN fl_outbound_dispatch d ON d.container_booking_id=c.id
|
||||
AND d.inbound_id=i.inbound_id AND d.tenant_id=i.account_id
|
||||
WHERE r.ocean_booking_id=#{bookingId}
|
||||
ORDER BY r.id,c.id,p.id,i.id
|
||||
FOR UPDATE
|
||||
""")
|
||||
List<DepartureRow> departuresForUpdate(@Param("bookingId") long bookingId);
|
||||
|
||||
@Update("""
|
||||
UPDATE fl_truck_appointment SET status='COMPLETED',actual_departed_at=#{departedAt},
|
||||
version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3)
|
||||
WHERE id=#{id} AND version=#{version} AND status NOT IN ('COMPLETED','CANCELLED','EXCEPTION')
|
||||
""")
|
||||
int complete(@Param("id") long id, @Param("version") int version,
|
||||
@Param("departedAt") LocalDateTime departedAt);
|
||||
|
||||
record DepartureRow(Long requirementId, Integer requiredQuantity, Long containerId, String containerNo,
|
||||
String recordStatus, Long planId, String planStatus, Long itemId,
|
||||
Long customerId, Long orderId, String orderNo, Long inboundId, String inboundCode,
|
||||
BigDecimal volumeCbm, String dispatchStatus, LocalDateTime departedAt) { }
|
||||
}
|
||||
@@ -11,15 +11,15 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.myba
|
||||
AND d.valid=1 AND d.document_type IN ('BOOKING_CONFIRMATION','BILL_OF_LADING'))
|
||||
AND o.status='RELEASED' AND o.bill_of_lading_no IS NOT NULL AND o.bill_of_lading_no<>''
|
||||
AND NOT EXISTS (SELECT 1 FROM fl_truck_appointment t WHERE t.ocean_booking_id=o.id
|
||||
AND t.status NOT IN ('COMPLETED','CANCELLED','EXCEPTION'))
|
||||
AND t.status NOT IN ('CANCELLED','EXCEPTION'))
|
||||
AND (#{q.keyword} IS NULL OR o.booking_no LIKE CONCAT('%',#{q.keyword},'%')
|
||||
OR o.bill_of_lading_no LIKE CONCAT('%',#{q.keyword},'%'))
|
||||
ORDER BY o.updated_at DESC,o.id DESC
|
||||
</script>
|
||||
""") Page<Candidate> candidates(Page<Candidate> p,@Param("q") CandidateQuery q);
|
||||
@Select("SELECT * FROM fl_truck_appointment WHERE ocean_booking_id=#{oceanBookingId} AND status NOT IN ('COMPLETED','CANCELLED','EXCEPTION') ORDER BY created_at,id LIMIT 1 FOR UPDATE") TruckAppointmentEntity firstActiveForUpdate(@Param("oceanBookingId") long oceanBookingId);
|
||||
@Select("SELECT * FROM fl_truck_appointment WHERE ocean_booking_id=#{oceanBookingId} AND status NOT IN ('CANCELLED','EXCEPTION') ORDER BY created_at,id LIMIT 1 FOR UPDATE") TruckAppointmentEntity firstActiveForUpdate(@Param("oceanBookingId") long oceanBookingId);
|
||||
@Select("<script>SELECT a.*,o.booking_no ocean_booking_no,o.warehouse_name warehouse_name,o.shipping_company_id,o.shipping_company_name,o.booking_agent_id,o.booking_agent_name,(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 WHERE (#{q.keyword} IS NULL OR a.appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR o.booking_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.oceanBookingId} IS NULL OR a.ocean_booking_id=#{q.oceanBookingId}) 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=#{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, 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='DISPATCHED',bill_of_lading_no=#{billOfLadingNo},revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int dispatch(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("billOfLadingNo") String billOfLadingNo,@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);
|
||||
@Update("UPDATE fl_truck_appointment SET outbound_order_id=#{e.outboundOrderId},ocean_booking_id=#{e.oceanBookingId},warehouse_id=#{e.warehouseId},carrier_id=#{e.carrierId},carrier_name=#{e.carrierName},bill_of_lading_no=#{e.billOfLadingNo},fleet_contact=#{e.fleetContact},fleet_phone=#{e.fleetPhone},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},driver_id_card=#{e.driverIdCard},vehicle_no=#{e.vehicleNo},store_name=#{e.storeName},store_contact=#{e.storeContact},store_phone=#{e.storePhone},store_address=#{e.storeAddress},store_city=#{e.storeCity},store_zipcode=#{e.storeZipcode},package_count=#{e.packageCount},gross_weight_kg=#{e.grossWeightKg},volume_cbm=#{e.volumeCbm},pallet_count=#{e.palletCount},po_no=#{e.poNo},planned_loading_at=#{e.plannedLoadingAt},transit_minutes=#{e.transitMinutes},payable_amount=#{e.payableAmount},currency=#{e.currency},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version);
|
||||
|
||||
+8
-6
@@ -1,6 +1,7 @@
|
||||
package com.baiyue.firstleg.outbound.dispatch.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.firstleg.service.TruckAppointmentCompletionService;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.outbound.dispatch.domain.OutboundDispatchStatus;
|
||||
@@ -28,13 +29,16 @@ public class OutboundDispatchService {
|
||||
private final OutboundDispatchRepository repository;
|
||||
private final Map<String, OutboundDispatchGateContributor> contributors;
|
||||
private final CarrierScheduleQueryService carrierSchedules;
|
||||
private final TruckAppointmentCompletionService truckCompletion;
|
||||
|
||||
@Autowired
|
||||
public OutboundDispatchService(OutboundDispatchRepository repository,
|
||||
List<OutboundDispatchGateContributor> contributors,
|
||||
CarrierScheduleQueryService carrierSchedules) {
|
||||
CarrierScheduleQueryService carrierSchedules,
|
||||
TruckAppointmentCompletionService truckCompletion) {
|
||||
this.repository = repository;
|
||||
this.carrierSchedules = carrierSchedules;
|
||||
this.truckCompletion = truckCompletion;
|
||||
Map<String, OutboundDispatchGateContributor> indexed = new HashMap<>();
|
||||
for (OutboundDispatchGateContributor contributor : contributors) {
|
||||
if (indexed.put(contributor.code(), contributor) != null) {
|
||||
@@ -44,11 +48,6 @@ public class OutboundDispatchService {
|
||||
this.contributors = Map.copyOf(indexed);
|
||||
}
|
||||
|
||||
public OutboundDispatchService(OutboundDispatchRepository repository,
|
||||
List<OutboundDispatchGateContributor> contributors) {
|
||||
this(repository, contributors, null);
|
||||
}
|
||||
|
||||
public List<Aggregate> list(Query query) {
|
||||
return List.of(aggregate(bindings(query.containerBookingId())));
|
||||
}
|
||||
@@ -73,6 +72,7 @@ public class OutboundDispatchService {
|
||||
public Detail confirm(long containerBookingId, long operatorId, String operatorName,
|
||||
String key, ActionRequest request) {
|
||||
validateKey(key);
|
||||
Long bookingId = truckCompletion.lockForContainer(containerBookingId);
|
||||
String hash = hash("CONFIRM", containerBookingId, request);
|
||||
Detail replay = replay(containerBookingId, operatorId, "CONFIRM_DISPATCH", key, hash);
|
||||
if (replay != null) return replay;
|
||||
@@ -92,6 +92,7 @@ public class OutboundDispatchService {
|
||||
}
|
||||
}
|
||||
OutboundDispatchEntity representative = states.get(0);
|
||||
if (bookingId != null) truckCompletion.completeForBooking(bookingId);
|
||||
history(representative, "CONFIRM_DISPATCH", OutboundDispatchStatus.READY_FOR_DISPATCH.name(),
|
||||
OutboundDispatchStatus.WAITING_FOR_LOADING.name(), request.remark(), operatorId, operatorName);
|
||||
|
||||
@@ -113,6 +114,7 @@ public class OutboundDispatchService {
|
||||
public Detail reverse(long containerBookingId, long operatorId, String operatorName,
|
||||
String key, ActionRequest request) {
|
||||
validateKey(key);
|
||||
truckCompletion.lockForContainer(containerBookingId);
|
||||
String hash = hash("REVERSE", containerBookingId, request);
|
||||
Detail replay = replay(containerBookingId, operatorId, "REVERSE_DISPATCH", key, hash);
|
||||
if (replay != null) return replay;
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentHistoryEntity;
|
||||
import com.baiyue.firstleg.mapper.TruckAppointmentCompletionMapper;
|
||||
import com.baiyue.firstleg.mapper.TruckAppointmentCompletionMapper.DepartureRow;
|
||||
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
|
||||
import com.baiyue.firstleg.repository.TruckAppointmentRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TruckAppointmentCompletionService {
|
||||
private final TruckAppointmentCompletionMapper mapper;
|
||||
private final OceanBookingRepository bookings;
|
||||
private final TruckAppointmentRepository appointments;
|
||||
private final TruckAppointmentFeeService fees;
|
||||
|
||||
/** All writers lock the booking before appointment/outbound rows, serializing multiple container departures. */
|
||||
public void lockBooking(long bookingId) {
|
||||
if (bookings.findForUpdate(bookingId) == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "关联订舱不存在");
|
||||
}
|
||||
}
|
||||
|
||||
public Long lockForContainer(long containerId) {
|
||||
Long bookingId = mapper.bookingForContainer(containerId);
|
||||
if (bookingId != null) lockBooking(bookingId);
|
||||
return bookingId;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void completeForBooking(long bookingId) {
|
||||
lockBooking(bookingId);
|
||||
var active = mapper.activeForUpdate(bookingId);
|
||||
if (active.isEmpty()) return;
|
||||
List<DepartureRow> rows = mapper.departuresForUpdate(bookingId);
|
||||
if (!allDeparted(rows)) return;
|
||||
LocalDateTime departedAt = rows.stream().map(DepartureRow::departedAt)
|
||||
.max(LocalDateTime::compareTo).orElseThrow();
|
||||
for (var appointment : active) {
|
||||
// Fees, their immutable snapshots, Outbox and completion history share this transaction.
|
||||
fees.publish(appointment, rows, departedAt);
|
||||
if (mapper.complete(appointment.getId(), appointment.getVersion(), departedAt) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "拖车预约已变化,请刷新后重试");
|
||||
}
|
||||
var history = new TruckAppointmentHistoryEntity();
|
||||
history.setId(IdWorker.getId());
|
||||
history.setAppointmentId(appointment.getId());
|
||||
history.setRevision(appointment.getRevision() + 1);
|
||||
history.setAction("AUTO_COMPLETE");
|
||||
history.setFromStatus(appointment.getStatus().name());
|
||||
history.setToStatus("COMPLETED");
|
||||
history.setReason("订舱全部要求货柜已确认离开国内仓库,自动完成拖车并发布应付拖车费");
|
||||
history.setOperatorId(0L);
|
||||
history.setOperatorName("系统");
|
||||
history.setCreatedAt(LocalDateTime.now());
|
||||
appointments.history(history);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean allDeparted(List<DepartureRow> rows) {
|
||||
if (rows.isEmpty()) return false;
|
||||
if (rows.stream().anyMatch(row -> row.requirementId() == null || row.requiredQuantity() == null
|
||||
|| row.requiredQuantity() <= 0 || row.containerId() == null || !"ACTIVE".equals(row.recordStatus())
|
||||
|| row.planId() == null || !"COMPLETED".equals(row.planStatus()) || row.itemId() == null
|
||||
|| !"WAITING_FOR_LOADING".equals(row.dispatchStatus()) || row.departedAt() == null)) return false;
|
||||
return rows.stream().collect(Collectors.groupingBy(DepartureRow::requirementId)).values().stream()
|
||||
.allMatch(group -> group.stream().map(DepartureRow::containerId).distinct().count()
|
||||
== group.get(0).requiredQuantity());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.ContainerFeeDetailDtos.*;
|
||||
import com.baiyue.firstleg.entity.TruckAppointmentEntity;
|
||||
import com.baiyue.firstleg.mapper.TruckAppointmentCompletionMapper.DepartureRow;
|
||||
import com.baiyue.firstleg.repository.FeeItemRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class TruckAppointmentFeeService {
|
||||
private static final String TRUCKING_FEE_CODE = "FEE_TYPE_108";
|
||||
private final FeeItemRepository feeItems;
|
||||
private final ContainerFeeDetailService containerFees;
|
||||
private final WarehouseSupplierSnapshotService suppliers;
|
||||
|
||||
public void publish(TruckAppointmentEntity appointment, List<DepartureRow> rows, LocalDateTime departedAt) {
|
||||
BigDecimal total = appointment.getPayableAmount();
|
||||
if (total == null || total.signum() == 0) return;
|
||||
if (!"RMB".equals(appointment.getCurrency())) throw conflict("国内拖车应付只支持人民币");
|
||||
var item = feeItems.findItemByCode(TRUCKING_FEE_CODE);
|
||||
if (item == null || !"ACTIVE".equals(item.getStatus()) || !Boolean.TRUE.equals(item.getPickupPayable())) {
|
||||
throw conflict("国内拖车费项目不存在、已停用或未启用应付");
|
||||
}
|
||||
suppliers.trucking(appointment.getCarrierId(), null);
|
||||
List<Share> shares = allocate(total, rows);
|
||||
Map<Long, List<Share>> plans = new LinkedHashMap<>();
|
||||
shares.forEach(share -> plans.computeIfAbsent(share.owner().planId(), ignored -> new ArrayList<>()).add(share));
|
||||
for (var entry : plans.entrySet()) {
|
||||
BigDecimal planAmount = entry.getValue().stream().map(Share::amount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
var fee = new SaveRequest(item.getId(), "PAYABLE", departedAt.toLocalDate(), planAmount, "CNY",
|
||||
appointment.getCarrierId(), "ON_ACCOUNT", null, null, null, null,
|
||||
"国内拖车单 " + appointment.getAppointmentNo() + ";按实装体积分摊;拖车总额 " + total.toPlainString() + " 元");
|
||||
var allocations = entry.getValue().stream().map(share -> new Allocation(share.owner().customerId(),
|
||||
share.owner().orderId(), share.owner().orderNo(), share.owner().inboundId(), share.owner().inboundCode(), share.amount())).toList();
|
||||
var created = containerFees.createAllocationGroup(entry.getKey(), 0L, new AllocationGroupRequest(fee, allocations));
|
||||
containerFees.approveAllocationGroup(entry.getKey(), created.get(0).getAllocationGroupId(), 0L, "系统(拖车离仓完成)");
|
||||
}
|
||||
}
|
||||
|
||||
/** Largest remainders allocate exact cents, with deterministic ties and no zero-amount finance sources. */
|
||||
static List<Share> allocate(BigDecimal total, List<DepartureRow> rows) {
|
||||
if (total.signum() <= 0 || total.scale() > 2) throw conflict("拖车费必须为正数且最多两位小数");
|
||||
Map<Owner, BigDecimal> volumes = new LinkedHashMap<>();
|
||||
for (DepartureRow row : rows) {
|
||||
if (row.customerId() == null || row.customerId() <= 0 || row.orderId() == null || row.inboundId() == null
|
||||
|| row.orderNo() == null || row.orderNo().isBlank() || row.inboundCode() == null || row.inboundCode().isBlank()
|
||||
|| row.volumeCbm() == null || row.volumeCbm().signum() <= 0) {
|
||||
throw conflict("拖车费分摊缺少柜内订单归属或有效实装体积");
|
||||
}
|
||||
var owner = new Owner(row.planId(), row.customerId(), row.orderId(), row.orderNo(), row.inboundId(), row.inboundCode());
|
||||
volumes.merge(owner, row.volumeCbm(), BigDecimal::add);
|
||||
}
|
||||
if (volumes.isEmpty()) throw conflict("拖车费没有可分摊的柜内订单");
|
||||
var owners = volumes.keySet().stream().sorted(Comparator.comparing(Owner::planId)
|
||||
.thenComparing(Owner::customerId).thenComparing(Owner::inboundId)).toList();
|
||||
BigDecimal volume = volumes.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
BigDecimal cents = total.movePointRight(2).setScale(0, RoundingMode.UNNECESSARY);
|
||||
var amounts = new ArrayList<BigDecimal>();
|
||||
var remainders = new ArrayList<BigDecimal>();
|
||||
for (var owner : owners) {
|
||||
BigDecimal numerator = cents.multiply(volumes.get(owner));
|
||||
BigDecimal floor = numerator.divide(volume, 0, RoundingMode.DOWN);
|
||||
amounts.add(floor);
|
||||
remainders.add(numerator.subtract(floor.multiply(volume)));
|
||||
}
|
||||
int remaining = cents.subtract(amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add)).intValueExact();
|
||||
var ranked = java.util.stream.IntStream.range(0, owners.size()).boxed()
|
||||
.sorted(Comparator.<Integer, BigDecimal>comparing(remainders::get).reversed().thenComparingInt(Integer::intValue)).toList();
|
||||
for (int i = 0; i < remaining; i++) amounts.set(ranked.get(i), amounts.get(ranked.get(i)).add(BigDecimal.ONE));
|
||||
return java.util.stream.IntStream.range(0, owners.size()).filter(i -> amounts.get(i).signum() > 0)
|
||||
.mapToObj(i -> new Share(owners.get(i), amounts.get(i).movePointLeft(2))).toList();
|
||||
}
|
||||
|
||||
record Owner(Long planId, Long customerId, Long orderId, String orderNo, Long inboundId, String inboundCode) { }
|
||||
record Share(Owner owner, BigDecimal amount) { }
|
||||
private static BizException conflict(String message) { return new BizException(ResultCode.CONFLICT, message); }
|
||||
}
|
||||
@@ -32,8 +32,9 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
@Service
|
||||
public class TruckAppointmentService {
|
||||
private static final Set<String> ATTACHMENT_TYPES = Set.of("DISPATCH_ORDER", "DRIVER_DOCUMENT", "VEHICLE_DOCUMENT", "OTHER");
|
||||
private static final Set<String> CURRENCIES = Set.of("RMB", "USD");
|
||||
private static final Set<String> CURRENCIES = Set.of("RMB");
|
||||
@Resource TruckAppointmentRepository repo;
|
||||
@Resource TruckAppointmentCompletionService completion;
|
||||
@Resource OceanBookingRepository oceanBookings;
|
||||
@Resource OceanBookingDocumentService oceanBookingDocuments;
|
||||
@Resource TruckAppointmentFileClient files;
|
||||
@@ -46,10 +47,23 @@ public class TruckAppointmentService {
|
||||
@Transactional public TruckAppointmentAttachmentEntity upload(long id,String type,MultipartFile file,long uid){String attachmentType=attachmentType(type);required(id);try{var response=files.upload(internalToken,uid,0L,"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),file);if(response==null||!response.successful()||response.data()==null)throw bad("附件上传失败");var m=response.data();TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(m.fileId());a.setAttachmentType(attachmentType);a.setFileName(m.fileName());a.setFileHash(m.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");}}
|
||||
@Transactional public TruckAppointmentAttachmentEntity attach(long id,AttachmentRequest r,long uid){String attachmentType=attachmentType(r.attachmentType());required(id);TruckAppointmentFileClient.Metadata metadata;try{var response=files.metadata(internalToken,0L,"TRUCK_APPOINTMENT_ATTACHMENT",Long.toString(id),r.fileId());if(response==null||!response.successful()||response.data()==null)throw bad("附件不存在或归属不匹配");metadata=response.data();}catch(FeignException ex){throw new BizException(ResultCode.SERVICE_UNAVAILABLE,"文件服务暂不可用");} TruckAppointmentAttachmentEntity a=new TruckAppointmentAttachmentEntity();a.setId(IdWorker.getId());a.setAppointmentId(id);a.setFileId(metadata.fileId());a.setAttachmentType(attachmentType);a.setFileName(metadata.fileName());a.setFileHash(metadata.sha256());a.setVisibility("PLATFORM");a.setUploadedBy(uid);a.setUploadedAt(LocalDateTime.now());repo.attachment(a);return a;}
|
||||
@Transactional public void deleteAttachment(long id,long attachmentId){required(id);if(repo.deleteAttachment(id,attachmentId)!=1)throw new BizException(ResultCode.NOT_FOUND,"附件引用不存在");}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r);TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null)throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());TruckAppointmentEntity e=new TruckAppointmentEntity();e.setId(IdWorker.getId());e.setAppointmentNo(documentNumbers.next("TA"));e.setStatus(TruckAppointmentStatus.DRAFT);e.setRevision(0);e.setVersion(0);e.setCreatedAt(LocalDateTime.now());e.setUpdatedAt(e.getCreatedAt());copy(e,r,links,carrier.name());try{repo.insert(e);}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,null,e.getStatus().name(),"CREATE",null,uid,name);return enrich(e);}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){validateRequired(r);TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion());if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑");TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null&&!Objects.equals(active.getId(),e.getId()))throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());copy(e,r,links,carrier.name());try{if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试");}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name);return enrich(required(id));}
|
||||
@Transactional public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String clientHash,long uid,String name){String requestHash=hash(action,id,r);verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion());if("RESCHEDULE".equals(action))throw conflict("改约请使用专用接口");TruckAppointmentStatus from=e.getStatus(),to=next(from,action);String dispatchBill="DISPATCH".equals(action)?requireDispatchReady(e):null;int changed="DISPATCH".equals(action)?repo.dispatch(id,from.name(),dispatchBill,e.getVersion()):repo.transition(id,from.name(),to.name(),e.getVersion());if(changed!=1)throw conflict("状态或版本已变化,请刷新后重试");TruckAppointmentEntity after=enrich(required(id));record(after,from.name(),to.name(),action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,String idem,String clientHash,long uid,String name){String action="RESCHEDULE";String requestHash=hash(action,id,new ReschedulePayload(at,r));verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id);checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");TruckAppointmentEntity after=enrich(required(id));record(after,from,"RESCHEDULED",action,r.reason(),uid,name);return completeOperation(id,action,idem,after);}
|
||||
@Transactional public TruckAppointmentEntity create(long uid,String name,SaveRequest r){if(r.expectedVersion()==null||r.expectedVersion()!=0)throw bad("新增 expectedVersion 必须为0");validateRequired(r);completion.lockBooking(r.oceanBookingId());TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null)throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());TruckAppointmentEntity e=new TruckAppointmentEntity();e.setId(IdWorker.getId());e.setAppointmentNo(documentNumbers.next("TA"));e.setStatus(TruckAppointmentStatus.DRAFT);e.setRevision(0);e.setVersion(0);e.setCreatedAt(LocalDateTime.now());e.setUpdatedAt(e.getCreatedAt());copy(e,r,links,carrier.name());try{repo.insert(e);}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,null,e.getStatus().name(),"CREATE",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(e.getId()));}
|
||||
@Transactional public TruckAppointmentEntity edit(long id,long uid,String name,SaveRequest r){validateRequired(r);TruckAppointmentEntity e=required(id);completion.lockBooking(Math.min(e.getOceanBookingId(),r.oceanBookingId()));if(!Objects.equals(e.getOceanBookingId(),r.oceanBookingId()))completion.lockBooking(Math.max(e.getOceanBookingId(),r.oceanBookingId()));checkVersion(e,r.expectedVersion());if(!(e.getStatus()==TruckAppointmentStatus.DRAFT||e.getStatus()==TruckAppointmentStatus.PENDING_DISPATCH||e.getStatus()==TruckAppointmentStatus.RESCHEDULED))throw conflict("当前状态不可编辑");TruckAppointmentEntity active=repo.firstActiveForUpdate(r.oceanBookingId());if(active!=null&&!Objects.equals(active.getId(),e.getId()))throw existingAppointmentConflict(active);AssociationContext links=validateAssociations(r);var carrier=suppliers.trucking(r.carrierId(),r.carrierName());copy(e,r,links,carrier.name());try{if(repo.updateEdit(e,r.expectedVersion())!=1)throw conflict("版本已变化,请刷新后重试");}catch(DuplicateKeyException exception){if(exception.getMessage()!=null&&exception.getMessage().contains("uk_truck_active_ocean_booking"))throw existingAppointmentConflict(repo.firstActiveForUpdate(r.oceanBookingId()));throw exception;}record(e,e.getStatus().name(),e.getStatus().name(),"EDIT",null,uid,name);completion.completeForBooking(e.getOceanBookingId());return enrich(required(id));}
|
||||
@Transactional
|
||||
public TruckAppointmentEntity action(long id,String action,ActionRequest r,String idem,String clientHash,long uid,String name){
|
||||
String requestHash=hash(action,id,r);verifyClientHash(clientHash,requestHash);
|
||||
TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;
|
||||
TruckAppointmentEntity e=required(id);completion.lockBooking(e.getOceanBookingId());checkVersion(e,r.expectedVersion());
|
||||
if("RESCHEDULE".equals(action))throw conflict("改约请使用专用接口");
|
||||
TruckAppointmentStatus from=e.getStatus(),to=next(from,action);
|
||||
String dispatchBill="DISPATCH".equals(action)?requireDispatchReady(e):null;
|
||||
int changed="DISPATCH".equals(action)?repo.dispatch(id,from.name(),dispatchBill,e.getVersion()):repo.transition(id,from.name(),to.name(),e.getVersion());
|
||||
if(changed!=1)throw conflict("状态或版本已变化,请刷新后重试");
|
||||
record(required(id),from.name(),to.name(),action,r.reason(),uid,name);
|
||||
completion.completeForBooking(e.getOceanBookingId());
|
||||
return completeOperation(id,action,idem,enrich(required(id)));
|
||||
}
|
||||
@Transactional public TruckAppointmentEntity reschedule(long id,LocalDateTime at,ActionRequest r,String idem,String clientHash,long uid,String name){String action="RESCHEDULE";String requestHash=hash(action,id,new ReschedulePayload(at,r));verifyClientHash(clientHash,requestHash);TruckAppointmentEntity replay=claimOrReplay(id,action,idem,requestHash);if(replay!=null)return replay;TruckAppointmentEntity e=required(id);completion.lockBooking(e.getOceanBookingId());checkVersion(e,r.expectedVersion()); if(e.getStatus()==TruckAppointmentStatus.COMPLETED||e.getStatus()==TruckAppointmentStatus.CANCELLED)throw conflict("当前状态不可改约"); TruckAppointmentEntity active=repo.firstActiveForUpdate(e.getOceanBookingId());if(active!=null&&!Objects.equals(active.getId(),e.getId()))throw existingAppointmentConflict(active);String from=e.getStatus().name(); if(repo.reschedule(id,at,r.reason(),e.getVersion())!=1)throw conflict("状态或版本已变化,请刷新后重试");TruckAppointmentEntity after=enrich(required(id));record(after,from,"RESCHEDULED",action,r.reason(),uid,name);completion.completeForBooking(e.getOceanBookingId());return completeOperation(id,action,idem,enrich(required(id)));}
|
||||
private AssociationContext validateAssociations(SaveRequest r){
|
||||
OceanBookingEntity booking=oceanBookings.find(r.oceanBookingId());
|
||||
if(booking==null)throw new BizException(ResultCode.NOT_FOUND,"关联订舱不存在");
|
||||
@@ -59,9 +73,9 @@ public class TruckAppointmentService {
|
||||
oceanBookingDocuments.requireActiveValidDocument(booking);
|
||||
return new AssociationContext(booking,warehouseId,billOfLadingNo);
|
||||
}
|
||||
private static void validateRequired(SaveRequest r){if(r.oceanBookingId()==null||r.oceanBookingId()<=0)throw bad("oceanBookingId不能为空");if(r.carrierId()==null||r.carrierId()<=0)throw bad("carrierId不能为空");if(r.plannedLoadingAt()==null)throw bad("plannedLoadingAt不能为空");if(r.expectedVersion()==null||r.expectedVersion()<0)throw bad("expectedVersion不合法");}
|
||||
private static void validateRequired(SaveRequest r){if(r.oceanBookingId()==null||r.oceanBookingId()<=0)throw bad("oceanBookingId不能为空");if(r.carrierId()==null||r.carrierId()<=0)throw bad("carrierId不能为空");if(r.plannedLoadingAt()==null)throw bad("plannedLoadingAt不能为空");if(r.expectedVersion()==null||r.expectedVersion()<0)throw bad("expectedVersion不合法");if(r.payableAmount()!=null&&(r.payableAmount().signum()<0||r.payableAmount().scale()>2))throw bad("拖车费必须为非负数且最多两位小数");}
|
||||
private static Long mergeWarehouse(Long requested,Long linked,String source){if(requested!=null&&linked!=null&&!Objects.equals(requested,linked))throw bad("warehouseId与"+source+"不一致");return requested!=null?requested:linked;}
|
||||
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 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"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){if(value.getOceanBookingId()!=null){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());}}value.setAttachmentCount((long)repo.attachments(value.getId()).size());return value;}
|
||||
private void copy(TruckAppointmentEntity e,SaveRequest r,AssociationContext links,String carrierName){e.setOutboundOrderId(r.outboundOrderId());e.setOceanBookingId(r.oceanBookingId());e.setContainerBookingId(null);e.setLoadingPlanId(null);e.setWarehouseId(links.warehouseId());e.setCarrierId(r.carrierId());e.setCarrierName(carrierName);e.setBillOfLadingNo(links.billOfLadingNo());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.setCurrency(currency(r.currency()));e.setRemark(r.remark());}
|
||||
@@ -82,8 +96,8 @@ public class TruckAppointmentService {
|
||||
}
|
||||
private static String releasedBookingBill(OceanBookingEntity booking,String requested){String authoritative=trim(booking.getBillOfLadingNo());if(!StringUtils.hasText(authoritative))throw conflict("关联已放舱订舱缺少提单号");if(StringUtils.hasText(requested)&&!authoritative.equals(requested))throw bad("billOfLadingNo与关联订舱不一致");return authoritative;}
|
||||
private static String attachmentType(String value){String normalized=trim(value);if(!ATTACHMENT_TYPES.contains(normalized))throw bad("不支持的拖车附件类型");return normalized;}
|
||||
private static String currency(String value){if(!StringUtils.hasText(value))return "RMB";String normalized=value.trim();if(!CURRENCIES.contains(normalized))throw bad("currency仅支持RMB或USD");return normalized;}
|
||||
private static BizException existingAppointmentConflict(TruckAppointmentEntity existing){String appointmentNo=existing==null?null:trim(existing.getAppointmentNo());return conflict(StringUtils.hasText(appointmentNo)?"该订舱已存在拖车预约 "+appointmentNo+",请继续编辑":"该订舱已存在拖车预约,请刷新后继续编辑");}
|
||||
private static String currency(String value){if(!StringUtils.hasText(value))return "RMB";String normalized=value.trim();if(!CURRENCIES.contains(normalized))throw bad("国内拖车应付只支持人民币RMB");return normalized;}
|
||||
private static BizException existingAppointmentConflict(TruckAppointmentEntity existing){String appointmentNo=existing==null?null:trim(existing.getAppointmentNo());return conflict(StringUtils.hasText(appointmentNo)?"该订舱已存在拖车预约 "+appointmentNo+(existing.getStatus()==TruckAppointmentStatus.COMPLETED?",请查看已完成拖车单":",请继续编辑"):"该订舱已存在拖车预约,请刷新后继续编辑");}
|
||||
private static String trim(String value){return StringUtils.hasText(value)?value.trim():null;}
|
||||
private record AssociationContext(OceanBookingEntity booking,Long warehouseId,String billOfLadingNo){}
|
||||
private void record(TruckAppointmentEntity e,String from,String to,String action,String reason,long uid,String name){TruckAppointmentHistoryEntity h=new TruckAppointmentHistoryEntity();h.setId(IdWorker.getId());h.setAppointmentId(e.getId());h.setRevision(e.getRevision());h.setAction(action);h.setFromStatus(from);h.setToStatus(to);h.setReason(reason);h.setOperatorId(uid);h.setOperatorName(name);h.setCreatedAt(LocalDateTime.now());repo.history(h);}
|
||||
|
||||
Reference in New Issue
Block a user