feat(trucking): refine truck demand contacts vehicle and service times
This commit is contained in:
@@ -2725,13 +2725,12 @@ CREATE TABLE ll_us_truck_order (
|
||||
id bigint NOT NULL COMMENT '主键ID',
|
||||
customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID',
|
||||
order_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '订单号',
|
||||
transport_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '运输类型',
|
||||
vehicle_type VARCHAR(32) NULL COMMENT '车辆类型:PALLET_15为15拖,PALLET_30为30拖;拼车未提供时为空',
|
||||
origin varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '起运地',
|
||||
origin_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '起运地址',
|
||||
destination varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目的地',
|
||||
destination_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的地址',
|
||||
container_type varchar(32) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜',
|
||||
pallet_count int NOT NULL DEFAULT '1' COMMENT '客户申报托盘数',
|
||||
pallet_count INT NOT NULL DEFAULT 0 COMMENT '确认托盘数;独立下单首次实收确定,拼车来源取真实预报',
|
||||
package_count int DEFAULT NULL COMMENT '可选箱数,零或空值均有效',
|
||||
received_pallet_count int NOT NULL DEFAULT '0' COMMENT 'PDA实收托盘数',
|
||||
allocated_pallet_count int NOT NULL DEFAULT '0' COMMENT '已分配到合车行程的托盘数',
|
||||
@@ -2739,9 +2738,12 @@ CREATE TABLE ll_us_truck_order (
|
||||
total_volume_cbm decimal(18,6) NOT NULL DEFAULT '0.000000' COMMENT '总体积(立方米)',
|
||||
budget_amount decimal(18,2) DEFAULT NULL COMMENT '预算金额',
|
||||
currency varchar(3) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'USD' COMMENT '币种',
|
||||
contact_name varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人姓名',
|
||||
contact_info varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '联系人联系方式',
|
||||
requested_date date NOT NULL COMMENT '要求服务日期',
|
||||
origin_contact_name VARCHAR(128) NULL COMMENT '起运地联系人',
|
||||
origin_contact_phone VARCHAR(128) NULL COMMENT '起运地联系电话',
|
||||
destination_contact_name VARCHAR(128) NULL COMMENT '目的地联系人',
|
||||
destination_contact_phone VARCHAR(128) NULL COMMENT '目的地联系电话',
|
||||
pickup_at DATETIME NULL COMMENT '取货时间,按填写的日期时分保存,不转换时区',
|
||||
delivery_at DATETIME NULL COMMENT '送货时间,按填写的日期时分保存;拼车来源未提供时为空',
|
||||
status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '美国卡车订单状态',
|
||||
remark varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
|
||||
idempotency_key varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '业务幂等键',
|
||||
@@ -2754,7 +2756,7 @@ CREATE TABLE ll_us_truck_order (
|
||||
UNIQUE KEY uk_ll_us_truck_order_no (customer_id,order_no),
|
||||
UNIQUE KEY uk_ll_us_truck_order_key (customer_id,idempotency_key),
|
||||
KEY idx_ll_us_truck_order_list (customer_id,status,created_at),
|
||||
KEY idx_ll_us_truck_order_date (customer_id,requested_date,created_at)
|
||||
KEY idx_ll_us_truck_order_date (customer_id,pickup_at,created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='美国卡车运输订单';
|
||||
|
||||
CREATE TABLE ll_us_truck_order_operation (
|
||||
|
||||
+3
-3
@@ -73,11 +73,11 @@ public class FleetPoolOrderBridge {
|
||||
loose.insert(order); // No placeholder ll_order: incoming owns the one physical tail order.
|
||||
} else {
|
||||
var order=new UsTruckOrder();order.setId(row.getBusinessOrderId());order.setCustomerId(in.customerId());order.setOrderNo(row.getBusinessOrderNo());
|
||||
order.setTransportType(transport);order.setOrigin(in.warehouseName());order.setDestination(in.destinationLabel());
|
||||
order.setContainerType(null); // Pool inquiry does not collect a container specification.
|
||||
order.setOrigin(in.warehouseName());order.setDestination(in.destinationLabel());
|
||||
order.setPalletCount(in.expectedPalletCount());order.setPackageCount(in.packageCount());order.setReceivedPalletCount(0);order.setAllocatedPalletCount(0);
|
||||
order.setTotalWeightKg(in.weightKg());order.setTotalVolumeCbm(in.volumeCbm());order.setCurrency(in.currency());
|
||||
order.setContactName(in.contactName());order.setContactInfo(in.contactPhone());order.setRequestedDate(in.plannedDepartureAt().toLocalDate());
|
||||
// 来源未提供的车型、目的联系人与送货时间保持未知。
|
||||
order.setOriginContactName(in.contactName());order.setOriginContactPhone(in.contactPhone());order.setPickupAt(in.plannedDepartureAt());
|
||||
order.setStatus(row.getFulfillmentStage());order.setRemark(in.remark());order.setIdempotencyKey("FLEET_POOL:"+in.bookingNo());
|
||||
order.setRequestHash(hash);order.setCreatedBy(operator);order.setVersion(0L);order.setCreatedAt(now);order.setUpdatedAt(now);
|
||||
trucks.insert(order); // No simulated ll_us_truck_order_pallet rows.
|
||||
|
||||
+30
-32
@@ -11,75 +11,73 @@ import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class UsTruckOrderRequests {
|
||||
/** 仅提供请求模型。 */
|
||||
private UsTruckOrderRequests() {}
|
||||
|
||||
/** 客户或平台提交独立本土卡车需求,时间按填写值保存。 */
|
||||
public record Create(
|
||||
@Size(max = 32) String transportType,
|
||||
@NotBlank @Pattern(regexp = "PALLET_15|PALLET_30") String vehicleType,
|
||||
@NotBlank @Size(max = 255) String origin,
|
||||
@Size(max = 500) String originAddress,
|
||||
@NotBlank @Size(max = 255) String destination,
|
||||
@Size(max = 500) String destinationAddress,
|
||||
@Size(max = 32) String containerType,
|
||||
@NotNull @Min(1) Integer palletCount,
|
||||
@Min(0) Integer packageCount,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal totalWeightKg,
|
||||
@NotNull @DecimalMin("0.00") BigDecimal totalVolumeCbm,
|
||||
@DecimalMin("0.00") BigDecimal budgetAmount,
|
||||
@Size(min = 3, max = 3) String currency,
|
||||
@NotBlank @Size(max = 128) String contactName,
|
||||
@NotBlank @Size(max = 128) String contactInfo,
|
||||
@NotNull LocalDate requestedDate,
|
||||
@NotBlank @Size(max = 128) String originContactName,
|
||||
@NotBlank @Size(max = 128) String originContactPhone,
|
||||
@NotBlank @Size(max = 128) String destinationContactName,
|
||||
@NotBlank @Size(max = 128) String destinationContactPhone,
|
||||
@NotNull LocalDateTime pickupAt,
|
||||
@NotNull LocalDateTime deliveryAt,
|
||||
@Size(max = 1000) String remark,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {
|
||||
/** Backward-compatible constructor for clients still sending legacy transport fields. */
|
||||
public Create(String transportType, String origin, String originAddress, String destination,
|
||||
String destinationAddress, String containerType, Integer packageCount,
|
||||
BigDecimal totalWeightKg, BigDecimal totalVolumeCbm, BigDecimal budgetAmount,
|
||||
String currency, String contactName, String contactInfo, LocalDate requestedDate,
|
||||
String remark, String idempotencyKey) {
|
||||
this(transportType, origin, originAddress, destination, destinationAddress, containerType,
|
||||
packageCount == null || packageCount < 1 ? 1 : packageCount, packageCount,
|
||||
totalWeightKg, totalVolumeCbm, budgetAmount, currency, contactName, contactInfo,
|
||||
requestedDate, remark, idempotencyKey);
|
||||
}
|
||||
}
|
||||
@NotBlank @Size(max = 255) String idempotencyKey) {}
|
||||
|
||||
public record Query(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String orderNo, String routeKeyword, String status, String transportType,
|
||||
String customerId, LocalDate requestedFrom, LocalDate requestedTo) {}
|
||||
String orderNo, String routeKeyword, String status, String vehicleType,
|
||||
String customerId, @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime pickupFrom,
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime pickupTo) {}
|
||||
|
||||
public record MerchantQuery(
|
||||
@Min(1) long page, @Min(1) @Max(200) long size,
|
||||
String orderNo, String routeKeyword, String status, String transportType,
|
||||
LocalDate requestedFrom, LocalDate requestedTo) {
|
||||
String orderNo, String routeKeyword, String status, String vehicleType,
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime pickupFrom,
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime pickupTo) {
|
||||
/** 客户范围由认证身份决定,查询模型不接收客户标识。 */
|
||||
public Query scoped() {
|
||||
return new Query(page, size, orderNo, routeKeyword, status, transportType,
|
||||
null, requestedFrom, requestedTo);
|
||||
return new Query(page, size, orderNo, routeKeyword, status, vehicleType,
|
||||
null, pickupFrom, pickupTo);
|
||||
}
|
||||
}
|
||||
|
||||
/** 待审核订单可更新需求;两端联系信息和时间须完整填写。 */
|
||||
public record Update(
|
||||
@NotNull @Min(0) Long version,
|
||||
@Size(max = 32) String transportType,
|
||||
@NotBlank @Pattern(regexp = "PALLET_15|PALLET_30") String vehicleType,
|
||||
@Size(max = 255) String origin,
|
||||
@Size(max = 500) String originAddress,
|
||||
@Size(max = 255) String destination,
|
||||
@Size(max = 500) String destinationAddress,
|
||||
@Size(max = 32) String containerType,
|
||||
@Min(1) Integer palletCount,
|
||||
@Min(0) Integer packageCount,
|
||||
@DecimalMin("0.00") BigDecimal totalWeightKg,
|
||||
@DecimalMin("0.00") BigDecimal totalVolumeCbm,
|
||||
@DecimalMin("0.00") BigDecimal budgetAmount,
|
||||
@Size(min = 3, max = 3) String currency,
|
||||
@Size(max = 128) String contactName,
|
||||
@Size(max = 128) String contactInfo,
|
||||
LocalDate requestedDate,
|
||||
@NotBlank @Size(max = 128) String originContactName,
|
||||
@NotBlank @Size(max = 128) String originContactPhone,
|
||||
@NotBlank @Size(max = 128) String destinationContactName,
|
||||
@NotBlank @Size(max = 128) String destinationContactPhone,
|
||||
@NotNull LocalDateTime pickupAt,
|
||||
@NotNull LocalDateTime deliveryAt,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record ChangeStatus(
|
||||
|
||||
+6
-5
@@ -1,20 +1,21 @@
|
||||
package com.baiyue.lastleg.trucking.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class UsTruckOrderResponses {
|
||||
/** 仅提供响应模型。 */
|
||||
private UsTruckOrderResponses() {}
|
||||
|
||||
/** Pool orders retain null containerType; forecast physical pallets do not imply a container type. */
|
||||
/** 独立订单展示客户填写的车型和两端信息;拼车来源未知字段保持为空。 */
|
||||
public record Summary(
|
||||
Long id, String customerId, String orderNo, String transportType,
|
||||
String origin, String destination, String containerType, int palletCount, int packageCount,
|
||||
Long id, String customerId, String orderNo, String vehicleType,
|
||||
String origin, String destination, int palletCount, int packageCount,
|
||||
int receivedPalletCount, int allocatedPalletCount,
|
||||
BigDecimal totalWeightKg, BigDecimal totalVolumeCbm, BigDecimal budgetAmount,
|
||||
String currency, String contactName, String contactInfo, LocalDate requestedDate,
|
||||
String currency, String originContactName, String originContactPhone,
|
||||
String destinationContactName, String destinationContactPhone, LocalDateTime pickupAt, LocalDateTime deliveryAt,
|
||||
String status, long version, LocalDateTime createdAt, LocalDateTime updatedAt, com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.Arrangement fleetArrangement) {}
|
||||
|
||||
public record History(
|
||||
|
||||
@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@@ -15,12 +14,11 @@ public class UsTruckOrder {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String orderNo;
|
||||
private String transportType;
|
||||
private String vehicleType;
|
||||
private String origin;
|
||||
private String originAddress;
|
||||
private String destination;
|
||||
private String destinationAddress;
|
||||
private String containerType;
|
||||
private Integer palletCount;
|
||||
private Integer packageCount;
|
||||
private Integer receivedPalletCount;
|
||||
@@ -29,9 +27,12 @@ public class UsTruckOrder {
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private BigDecimal budgetAmount;
|
||||
private String currency;
|
||||
private String contactName;
|
||||
private String contactInfo;
|
||||
private LocalDate requestedDate;
|
||||
private String originContactName;
|
||||
private String originContactPhone;
|
||||
private String destinationContactName;
|
||||
private String destinationContactPhone;
|
||||
private LocalDateTime pickupAt;
|
||||
private LocalDateTime deliveryAt;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.lastleg.trucking.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.entity.*;
|
||||
import com.baiyue.lastleg.trucking.mapper.*;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
import java.util.List;
|
||||
|
||||
/** 卡车订单持久化边界;事务和状态决策由服务负责。 */
|
||||
@Repository
|
||||
public class UsTruckOrderRepository {
|
||||
private final UsTruckOrderMapper orders;
|
||||
private final UsTruckOrderPalletMapper pallets;
|
||||
private final UsTruckOrderStatusHistoryMapper histories;
|
||||
private final UsTruckOrderOperationMapper operations;
|
||||
|
||||
/** 绑定本服务的订单、托盘和不可变历史 Mapper。 */
|
||||
public UsTruckOrderRepository(UsTruckOrderMapper orders, UsTruckOrderPalletMapper pallets,
|
||||
UsTruckOrderStatusHistoryMapper histories, UsTruckOrderOperationMapper operations) {
|
||||
this.orders = orders; this.pallets = pallets; this.histories = histories; this.operations = operations;
|
||||
}
|
||||
|
||||
/** 按客户、路线、状态、车型和取货时间组合分页条件。 */
|
||||
public Page<UsTruckOrder> page(String customerId, UsTruckOrderRequests.Query query) {
|
||||
LambdaQueryWrapper<UsTruckOrder> w = new LambdaQueryWrapper<UsTruckOrder>()
|
||||
.eq(StringUtils.hasText(customerId), UsTruckOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), UsTruckOrder::getOrderNo, query.orderNo())
|
||||
.and(StringUtils.hasText(query.routeKeyword()), x -> x.like(UsTruckOrder::getOrigin, query.routeKeyword())
|
||||
.or().like(UsTruckOrder::getDestination, query.routeKeyword()))
|
||||
.eq(StringUtils.hasText(query.status()), UsTruckOrder::getStatus,
|
||||
StringUtils.hasText(query.status()) ? query.status().trim().toUpperCase() : null)
|
||||
.eq(StringUtils.hasText(query.vehicleType()), UsTruckOrder::getVehicleType,
|
||||
StringUtils.hasText(query.vehicleType()) ? query.vehicleType() : null)
|
||||
.eq(StringUtils.hasText(query.customerId()), UsTruckOrder::getCustomerId, query.customerId())
|
||||
.ge(query.pickupFrom() != null, UsTruckOrder::getPickupAt, query.pickupFrom())
|
||||
.le(query.pickupTo() != null, UsTruckOrder::getPickupAt, query.pickupTo())
|
||||
.orderByDesc(UsTruckOrder::getCreatedAt).orderByDesc(UsTruckOrder::getId);
|
||||
return orders.selectPage(Page.of(query.page(), query.size()), w);
|
||||
}
|
||||
/** 按客户和订单号读取。 */
|
||||
public UsTruckOrder findNo(String customerId, String orderNo) { return orders.findNo(customerId, orderNo); }
|
||||
/** 在当前事务中按客户和订单号加行锁。 */
|
||||
public UsTruckOrder lockNo(String customerId, String orderNo) { return orders.lockNo(customerId, orderNo); }
|
||||
/** 按客户和创建幂等键读取。 */
|
||||
public UsTruckOrder findKey(String customerId, String key) { return orders.findKey(customerId, key); }
|
||||
/** 保存新订单。 */
|
||||
public void insertOrder(UsTruckOrder order) { orders.insert(order); }
|
||||
/** 保存已锁定订单的当前状态。 */
|
||||
public void updateOrder(UsTruckOrder order) { orders.updateById(order); }
|
||||
/** 读取本客户订单的托盘。 */
|
||||
public List<UsTruckOrderPallet> pallets(String customerId, Long orderId) { return pallets.listOrder(customerId, orderId); }
|
||||
/** 保存实际托盘记录。 */
|
||||
public void insertPallet(UsTruckOrderPallet row) { pallets.insert(row); }
|
||||
/** 更新实际托盘处理状态。 */
|
||||
public void updatePallet(UsTruckOrderPallet row) { pallets.updateById(row); }
|
||||
/** 读取本客户订单的不可变历史。 */
|
||||
public List<UsTruckOrderStatusHistory> histories(String customerId, Long orderId) { return histories.listOrder(customerId, orderId); }
|
||||
/** 保存订单历史。 */
|
||||
public void insertHistory(UsTruckOrderStatusHistory row) { histories.insert(row); }
|
||||
/** 按客户、订单和幂等键读取操作结果。 */
|
||||
public UsTruckOrderOperation findOperation(String customerId, Long orderId, String key) { return operations.findKey(customerId, orderId, key); }
|
||||
/** 保存不可变操作摘要。 */
|
||||
public void insertOperation(UsTruckOrderOperation row) { operations.insert(row); }
|
||||
}
|
||||
+106
-74
@@ -1,20 +1,14 @@
|
||||
package com.baiyue.lastleg.trucking.service;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.container.ContainerSpecificationCatalog;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderOperation;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderPallet;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderStatusHistory;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderOperationMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderPalletMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderStatusHistoryMapper;
|
||||
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@@ -26,12 +20,9 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HexFormat;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -44,8 +35,6 @@ public class UsTruckOrderService {
|
||||
@Resource private com.baiyue.lastleg.service.LastlegOrderNoGenerator orderNoGenerator;
|
||||
public static final String MANAGE_PERMISSION = OverseasPermissions.TRUCK_STATUS;
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
/** Kept for source compatibility; new orders use PENDING_REVIEW. */
|
||||
@Deprecated public static final String PENDING_CONFIRMATION = PENDING_REVIEW;
|
||||
public static final String WAITING_INBOUND = "WAITING_INBOUND";
|
||||
public static final String RECEIVING = "RECEIVING";
|
||||
public static final String WAITING_CONSOLIDATION = "WAITING_CONSOLIDATION";
|
||||
@@ -63,43 +52,41 @@ public class UsTruckOrderService {
|
||||
PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, REJECTED, CANCELLED);
|
||||
private static final Set<String> CANCELLABLE = Set.of(PENDING_REVIEW, WAITING_INBOUND, RECEIVING,
|
||||
PALLET_OVERAGE_REVIEW, WAITING_CONSOLIDATION, PARTIALLY_ALLOCATED, ALLOCATED, PENDING_DISPATCH);
|
||||
private static final Set<String> TRANSPORT_TYPES = Set.of("PORT_PICKUP", "WAREHOUSE_TO_WAREHOUSE", "WAREHOUSE_TO_DOOR");
|
||||
private static final Set<String> VEHICLE_TYPES = Set.of("PALLET_15", "PALLET_30");
|
||||
|
||||
@Resource private UsTruckOrderMapper orders;
|
||||
@Resource private UsTruckOrderOperationMapper operations;
|
||||
@Resource private UsTruckOrderPalletMapper pallets;
|
||||
@Resource private UsTruckOrderStatusHistoryMapper histories;
|
||||
@Resource private com.baiyue.lastleg.trucking.repository.UsTruckOrderRepository repository;
|
||||
@Resource private ObjectMapper objectMapper;
|
||||
|
||||
/** 创建卡车需求,校验两端资料并保留创建幂等结果。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail create(String customerId, String operatorId, UsTruckOrderRequests.Create request) {
|
||||
String key = request.idempotencyKey().trim();
|
||||
String hash = hash(request);
|
||||
UsTruckOrder replay = orders.findKey(customerId, key);
|
||||
UsTruckOrder replay = repository.findKey(customerId, key);
|
||||
if (replay != null) return replay(customerId, replay, hash);
|
||||
if (StringUtils.hasText(request.transportType())) validateTransport(request.transportType());
|
||||
validateRequirements(request.vehicleType(), request.originContactName(), request.originContactPhone(),
|
||||
request.destinationContactName(), request.destinationContactPhone(), request.pickupAt(), request.deliveryAt());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
UsTruckOrder order = new UsTruckOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setOrderNo(orderNoGenerator.next("TRUCK", request.origin(), order.getId()));
|
||||
order.setTransportType(StringUtils.hasText(request.transportType()) ? norm(request.transportType()) : null);
|
||||
order.setVehicleType(request.vehicleType());
|
||||
order.setOrigin(trim(request.origin())); order.setOriginAddress(trim(request.originAddress()));
|
||||
order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress()));
|
||||
order.setContainerType(StringUtils.hasText(request.containerType())
|
||||
? ContainerSpecificationCatalog.require(request.containerType()).containerType()
|
||||
: ContainerSpecificationCatalog.DEFAULT_CONTAINER_TYPE);
|
||||
order.setPalletCount(request.palletCount()); order.setPackageCount(value(request.packageCount()));
|
||||
// 车型只表达用车需求;实际托数由收货记录,不能按车型虚构货量。
|
||||
order.setPalletCount(0); order.setPackageCount(value(request.packageCount()));
|
||||
order.setReceivedPalletCount(0); order.setAllocatedPalletCount(0);
|
||||
order.setTotalWeightKg(request.totalWeightKg()); order.setTotalVolumeCbm(request.totalVolumeCbm());
|
||||
order.setBudgetAmount(request.budgetAmount()); order.setCurrency(defaultCurrency(request.currency()));
|
||||
order.setContactName(request.contactName().trim()); order.setContactInfo(request.contactInfo().trim());
|
||||
order.setRequestedDate(request.requestedDate()); order.setStatus(PENDING_REVIEW);
|
||||
order.setOriginContactName(request.originContactName().trim()); order.setOriginContactPhone(request.originContactPhone().trim());
|
||||
order.setDestinationContactName(request.destinationContactName().trim()); order.setDestinationContactPhone(request.destinationContactPhone().trim());
|
||||
order.setPickupAt(request.pickupAt()); order.setDeliveryAt(request.deliveryAt()); order.setStatus(PENDING_REVIEW);
|
||||
order.setRemark(trim(request.remark())); order.setIdempotencyKey(key); order.setRequestHash(hash);
|
||||
order.setCreatedBy(operatorId); order.setVersion(0L); order.setCreatedAt(now); order.setUpdatedAt(now);
|
||||
try { orders.insert(order); }
|
||||
try { repository.insertOrder(order); }
|
||||
catch (DuplicateKeyException e) {
|
||||
UsTruckOrder concurrent = orders.findKey(customerId, key);
|
||||
UsTruckOrder concurrent = repository.findKey(customerId, key);
|
||||
if (concurrent != null) return replay(customerId, concurrent, hash);
|
||||
throw e;
|
||||
}
|
||||
@@ -107,57 +94,48 @@ public class UsTruckOrderService {
|
||||
return detail(customerId, order.getOrderNo());
|
||||
}
|
||||
|
||||
/** 按客户范围分页读取订单并批量关联承运安排。 */
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.REPEATABLE_READ)
|
||||
public PageResult<UsTruckOrderResponses.Summary> page(String customerId, UsTruckOrderRequests.Query query) {
|
||||
validateQuery(query);
|
||||
LambdaQueryWrapper<UsTruckOrder> w = new LambdaQueryWrapper<UsTruckOrder>()
|
||||
.eq(StringUtils.hasText(customerId), UsTruckOrder::getCustomerId, customerId)
|
||||
.like(StringUtils.hasText(query.orderNo()), UsTruckOrder::getOrderNo, query.orderNo())
|
||||
.and(StringUtils.hasText(query.routeKeyword()), x -> x.like(UsTruckOrder::getOrigin, query.routeKeyword())
|
||||
.or().like(UsTruckOrder::getDestination, query.routeKeyword()))
|
||||
.eq(StringUtils.hasText(query.status()), UsTruckOrder::getStatus,
|
||||
StringUtils.hasText(query.status()) ? norm(query.status()) : null)
|
||||
.eq(StringUtils.hasText(query.transportType()), UsTruckOrder::getTransportType,
|
||||
StringUtils.hasText(query.transportType()) ? norm(query.transportType()) : null)
|
||||
.eq(StringUtils.hasText(query.customerId()), UsTruckOrder::getCustomerId, query.customerId())
|
||||
.ge(query.requestedFrom() != null, UsTruckOrder::getRequestedDate, query.requestedFrom())
|
||||
.le(query.requestedTo() != null, UsTruckOrder::getRequestedDate, query.requestedTo())
|
||||
.orderByDesc(UsTruckOrder::getCreatedAt).orderByDesc(UsTruckOrder::getId);
|
||||
Page<UsTruckOrder> result = orders.selectPage(Page.of(query.page(), query.size()), w);
|
||||
Page<UsTruckOrder> result = repository.page(customerId, query);
|
||||
var arrangements = fleetPool.arrangements(result.getRecords().stream().map(this::poolKey).toList());
|
||||
return new PageResult<>(result.getRecords().stream().map(o -> summary(o, arrangements.get(poolKey(o)))).toList(), result.getCurrent(),
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
/** 读取本客户的订单、实际托盘及操作历史。 */
|
||||
@Transactional(readOnly = true, isolation = org.springframework.transaction.annotation.Isolation.REPEATABLE_READ)
|
||||
public UsTruckOrderResponses.Detail detail(String customerId, String orderNo) {
|
||||
UsTruckOrder order = orders.findNo(customerId, orderNo);
|
||||
UsTruckOrder order = repository.findNo(customerId, orderNo);
|
||||
if (order == null) throw notFound();
|
||||
return detail(order, histories.listOrder(customerId, order.getId()));
|
||||
return detail(order, repository.histories(customerId, order.getId()));
|
||||
}
|
||||
|
||||
/** 按版本更新待审核订单的车型、联系人和取送时间。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail update(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Update request) {
|
||||
UsTruckOrder order = lock(customerId, orderNo);
|
||||
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
|
||||
if (!PENDING_REVIEW.equals(order.getStatus())) throw conflict("Only pending review orders can be edited");
|
||||
if (request.transportType() != null) { validateTransport(request.transportType()); order.setTransportType(norm(request.transportType())); }
|
||||
validateRequirements(request.vehicleType(), request.originContactName(), request.originContactPhone(),
|
||||
request.destinationContactName(), request.destinationContactPhone(), request.pickupAt(), request.deliveryAt());
|
||||
order.setVehicleType(request.vehicleType());
|
||||
setText(request.origin(), order::setOrigin); setPresent(request.originAddress(), order::setOriginAddress);
|
||||
setText(request.destination(), order::setDestination); setPresent(request.destinationAddress(), order::setDestinationAddress);
|
||||
setText(request.containerType(), v -> order.setContainerType(
|
||||
ContainerSpecificationCatalog.require(v).containerType()));
|
||||
if (request.palletCount() != null) order.setPalletCount(request.palletCount());
|
||||
if (request.packageCount() != null) order.setPackageCount(request.packageCount());
|
||||
if (request.totalWeightKg() != null) order.setTotalWeightKg(request.totalWeightKg()); if (request.totalVolumeCbm() != null) order.setTotalVolumeCbm(request.totalVolumeCbm());
|
||||
if (request.budgetAmount() != null) order.setBudgetAmount(request.budgetAmount()); if (request.currency() != null) order.setCurrency(defaultCurrency(request.currency()));
|
||||
setText(request.contactName(), order::setContactName); setText(request.contactInfo(), order::setContactInfo);
|
||||
if (request.requestedDate() != null) order.setRequestedDate(request.requestedDate()); setPresent(request.remark(), order::setRemark);
|
||||
touch(order); orders.updateById(order);
|
||||
order.setOriginContactName(request.originContactName().trim()); order.setOriginContactPhone(request.originContactPhone().trim());
|
||||
order.setDestinationContactName(request.destinationContactName().trim()); order.setDestinationContactPhone(request.destinationContactPhone().trim());
|
||||
order.setPickupAt(request.pickupAt()); order.setDeliveryAt(request.deliveryAt()); setPresent(request.remark(), order::setRemark);
|
||||
touch(order); repository.updateOrder(order);
|
||||
history(order, "UPDATE", order.getStatus(), order.getStatus(), operatorId, null, "UPDATE", LocalDateTime.now());
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
/** 按版本幂等取消允许取消的订单。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail changeStatus(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.ChangeStatus request) {
|
||||
@@ -172,6 +150,7 @@ public class UsTruckOrderService {
|
||||
order.getStatus(), CANCELLED, request.remark());
|
||||
}
|
||||
|
||||
/** 审核订单,仅对真实已知预报建立待收托盘。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail review(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Review request) {
|
||||
@@ -186,6 +165,7 @@ public class UsTruckOrderService {
|
||||
PENDING_REVIEW, next, request.remark());
|
||||
}
|
||||
|
||||
/** 记录实际收货,首次实收确定独立订单托数。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail receivePallets(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.ReceivePallets request) {
|
||||
@@ -198,9 +178,12 @@ public class UsTruckOrderService {
|
||||
}
|
||||
|
||||
createPlannedPallets(order);
|
||||
List<UsTruckOrderPallet> current = pallets.listOrder(customerId, order.getId());
|
||||
int planned = value(order.getPalletCount());
|
||||
List<UsTruckOrderPallet> current = repository.pallets(customerId, order.getId());
|
||||
int actual = request.actualPalletCount();
|
||||
// 独立下单没有预报托数,首次实收形成确认基准,后续超量仍需审核。
|
||||
boolean firstReceipt = value(order.getPalletCount()) == 0;
|
||||
int planned = firstReceipt ? actual : value(order.getPalletCount());
|
||||
order.setPalletCount(planned);
|
||||
List<Integer> packageCounts = request.palletPackageCounts() == null ? List.of()
|
||||
: request.palletPackageCounts();
|
||||
if (!packageCounts.isEmpty() && packageCounts.size() != actual) {
|
||||
@@ -213,7 +196,7 @@ public class UsTruckOrderService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (int sequence = 1; sequence <= actual; sequence++) {
|
||||
UsTruckOrderPallet pallet = sequence <= current.size() ? current.get(sequence - 1)
|
||||
: newPallet(order, sequence, "PDA_OVERAGE", now);
|
||||
: newPallet(order, sequence, firstReceipt ? "PDA_RECEIPT" : "PDA_OVERAGE", now);
|
||||
pallet.setStatus(sequence > planned ? "OVERAGE_PENDING_REVIEW" : "RECEIVED");
|
||||
if (!packageCounts.isEmpty()) pallet.setPackageCount(packageCounts.get(sequence - 1));
|
||||
else if (actual == 1) pallet.setPackageCount(request.packageCount());
|
||||
@@ -221,9 +204,9 @@ public class UsTruckOrderService {
|
||||
pallet.setUpdatedAt(now);
|
||||
if (pallet.getId() == null) {
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallets.insert(pallet);
|
||||
repository.insertPallet(pallet);
|
||||
} else {
|
||||
pallets.updateById(pallet);
|
||||
repository.updatePallet(pallet);
|
||||
}
|
||||
}
|
||||
order.setReceivedPalletCount(actual);
|
||||
@@ -232,12 +215,13 @@ public class UsTruckOrderService {
|
||||
: actual == planned ? WAITING_CONSOLIDATION : RECEIVING;
|
||||
order.setStatus(next);
|
||||
touch(order);
|
||||
orders.updateById(order);
|
||||
repository.updateOrder(order);
|
||||
recordOperation(order, "RECEIVE_PALLETS", request.idempotencyKey(), request, operatorId, now);
|
||||
history(order, "RECEIVE_PALLETS", from, next, operatorId, request.idempotencyKey(), request.remark(), now);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
/** 审核后续实收超出已确认托数的部分。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail reviewPalletOverage(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.ReviewPalletOverage request) {
|
||||
@@ -250,14 +234,14 @@ public class UsTruckOrderService {
|
||||
throw conflict("Only pallet-overage-review orders can be reviewed");
|
||||
}
|
||||
boolean approve = "APPROVE".equals(norm(request.decision()));
|
||||
List<UsTruckOrderPallet> rows = pallets.listOrder(customerId, order.getId());
|
||||
List<UsTruckOrderPallet> rows = repository.pallets(customerId, order.getId());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
int accepted = value(order.getPalletCount());
|
||||
for (UsTruckOrderPallet pallet : rows) {
|
||||
if (!"OVERAGE_PENDING_REVIEW".equals(pallet.getStatus())) continue;
|
||||
pallet.setStatus(approve ? "RECEIVED" : "OVERAGE_REJECTED");
|
||||
pallet.setUpdatedAt(now);
|
||||
pallets.updateById(pallet);
|
||||
repository.updatePallet(pallet);
|
||||
}
|
||||
if (approve) {
|
||||
accepted = value(order.getReceivedPalletCount());
|
||||
@@ -267,13 +251,14 @@ public class UsTruckOrderService {
|
||||
}
|
||||
order.setStatus(WAITING_CONSOLIDATION);
|
||||
touch(order);
|
||||
orders.updateById(order);
|
||||
repository.updateOrder(order);
|
||||
recordOperation(order, "REVIEW_PALLET_OVERAGE", request.idempotencyKey(), request, operatorId, now);
|
||||
history(order, "REVIEW_PALLET_OVERAGE", PALLET_OVERAGE_REVIEW, WAITING_CONSOLIDATION,
|
||||
operatorId, request.idempotencyKey(), request.remark(), now);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
/** 分配已实收且尚未分配的订单托盘。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail allocatePallets(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.AllocatePallets request) {
|
||||
@@ -287,7 +272,7 @@ public class UsTruckOrderService {
|
||||
}
|
||||
Set<Long> requested = new HashSet<>(request.palletIds());
|
||||
if (requested.size() != request.palletIds().size()) throw badRequest("palletIds must be unique");
|
||||
List<UsTruckOrderPallet> rows = pallets.listOrder(customerId, order.getId());
|
||||
List<UsTruckOrderPallet> rows = repository.pallets(customerId, order.getId());
|
||||
List<UsTruckOrderPallet> selected = rows.stream().filter(row -> requested.contains(row.getId())).toList();
|
||||
if (selected.size() != requested.size() || selected.stream().anyMatch(row -> !"RECEIVED".equals(row.getStatus()))) {
|
||||
throw conflict("Selected pallets must belong to the order and be received but unallocated");
|
||||
@@ -296,7 +281,7 @@ public class UsTruckOrderService {
|
||||
for (UsTruckOrderPallet pallet : selected) {
|
||||
pallet.setStatus("ALLOCATED");
|
||||
pallet.setUpdatedAt(now);
|
||||
pallets.updateById(pallet);
|
||||
repository.updatePallet(pallet);
|
||||
}
|
||||
int allocated = (int) rows.stream().filter(row -> "ALLOCATED".equals(row.getStatus())).count();
|
||||
int accepted = value(order.getReceivedPalletCount());
|
||||
@@ -306,13 +291,14 @@ public class UsTruckOrderService {
|
||||
order.setAllocatedPalletCount(allocated);
|
||||
order.setStatus(next);
|
||||
touch(order);
|
||||
orders.updateById(order);
|
||||
repository.updateOrder(order);
|
||||
recordOperation(order, "ALLOCATE_PALLETS", request.idempotencyKey(), request, operatorId, now);
|
||||
history(order, "ALLOCATE_PALLETS", from, next, operatorId,
|
||||
request.idempotencyKey(), request.remark(), now);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
/** 将全部分配完成的订单推进到待派车。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail prepareDispatch(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Command request) {
|
||||
@@ -320,24 +306,28 @@ public class UsTruckOrderService {
|
||||
PENDING_DISPATCH, request);
|
||||
}
|
||||
|
||||
/** 确认订单已派车并记录操作历史。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail dispatch(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Command request) {
|
||||
return command(customerId, orderNo, operatorId, "DISPATCH", PENDING_DISPATCH, DISPATCHED, request);
|
||||
}
|
||||
|
||||
/** 确认订单进入运输阶段。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail startTransit(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Command request) {
|
||||
return command(customerId, orderNo, operatorId, "START_TRANSIT", DISPATCHED, IN_TRANSIT, request);
|
||||
}
|
||||
|
||||
/** 确认运输完成并保留幂等结果。 */
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail complete(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.Command request) {
|
||||
return command(customerId, orderNo, operatorId, "COMPLETE", IN_TRANSIT, COMPLETED, request);
|
||||
}
|
||||
|
||||
/** 复核状态版本后执行单一履约动作。 */
|
||||
private UsTruckOrderResponses.Detail command(String customerId, String orderNo, String operatorId,
|
||||
String operationType, String expectedStatus, String nextStatus,
|
||||
UsTruckOrderRequests.Command request) {
|
||||
@@ -357,22 +347,24 @@ public class UsTruckOrderService {
|
||||
expectedStatus, nextStatus, request.remark());
|
||||
}
|
||||
|
||||
/** 原子更新状态并写入操作和历史。 */
|
||||
private UsTruckOrderResponses.Detail transition(UsTruckOrder order, String operatorId,
|
||||
String operationType, String idempotencyKey, Object request,
|
||||
String from, String to, String remark) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
order.setStatus(to);
|
||||
touch(order);
|
||||
orders.updateById(order);
|
||||
repository.updateOrder(order);
|
||||
recordOperation(order, operationType, idempotencyKey, request, operatorId, now);
|
||||
history(order, operationType, from, to, operatorId, idempotencyKey, remark, now);
|
||||
return detail(order.getCustomerId(), order.getOrderNo());
|
||||
}
|
||||
|
||||
/** 核对操作幂等键和请求摘要。 */
|
||||
private UsTruckOrderResponses.Detail replayOperation(UsTruckOrder order, String operationType,
|
||||
String idempotencyKey, Object request) {
|
||||
String key = idempotencyKey.trim();
|
||||
UsTruckOrderOperation existing = operations.findKey(order.getCustomerId(), order.getId(), key);
|
||||
UsTruckOrderOperation existing = repository.findOperation(order.getCustomerId(), order.getId(), key);
|
||||
if (existing == null) return null;
|
||||
if (!operationType.equals(existing.getOperationType()) || !hash(request).equals(existing.getRequestHash())) {
|
||||
throw conflict("Idempotency key was reused with a different truck operation payload");
|
||||
@@ -380,6 +372,7 @@ public class UsTruckOrderService {
|
||||
return detail(order.getCustomerId(), order.getOrderNo());
|
||||
}
|
||||
|
||||
/** 记录不可变操作摘要和结果版本。 */
|
||||
private void recordOperation(UsTruckOrder order, String operationType, String idempotencyKey,
|
||||
Object request, String operatorId, LocalDateTime now) {
|
||||
UsTruckOrderOperation operation = new UsTruckOrderOperation();
|
||||
@@ -392,19 +385,23 @@ public class UsTruckOrderService {
|
||||
operation.setResultVersion(order.getVersion());
|
||||
operation.setCreatedBy(operatorId);
|
||||
operation.setCreatedAt(now);
|
||||
operations.insert(operation);
|
||||
repository.insertOperation(operation);
|
||||
}
|
||||
|
||||
/** 阻止旧版本覆盖当前订单。 */
|
||||
private void requireVersion(UsTruckOrder order, Long requestedVersion) {
|
||||
if (!requestedVersion.equals(value(order.getVersion()))) {
|
||||
throw conflict("Order was changed; refresh and retry");
|
||||
}
|
||||
}
|
||||
|
||||
private UsTruckOrder lock(String customerId, String orderNo) { UsTruckOrder o = orders.lockNo(customerId, orderNo); if (o == null) throw notFound(); fleetPool.requireStandalone(customerId, "US_TRUCK", o.getId()); return o; }
|
||||
/** 按客户范围锁单并校验独立订单资格。 */
|
||||
private UsTruckOrder lock(String customerId, String orderNo) { UsTruckOrder o = repository.lockNo(customerId, orderNo); if (o == null) throw notFound(); fleetPool.requireStandalone(customerId, "US_TRUCK", o.getId()); return o; }
|
||||
/** 复核创建请求摘要并返回既有订单。 */
|
||||
private UsTruckOrderResponses.Detail replay(String customerId, UsTruckOrder order, String hash) { if (!hash.equals(order.getRequestHash())) throw conflict("Idempotency key was reused with different truck data"); return detail(customerId, order.getOrderNo()); }
|
||||
/** 读取订单资料与实际托盘和操作记录。 */
|
||||
private UsTruckOrderResponses.Detail detail(UsTruckOrder o, List<UsTruckOrderStatusHistory> hs) {
|
||||
List<UsTruckOrderResponses.Pallet> palletDetails = pallets.listOrder(o.getCustomerId(), o.getId()).stream()
|
||||
List<UsTruckOrderResponses.Pallet> palletDetails = repository.pallets(o.getCustomerId(), o.getId()).stream()
|
||||
.map(this::pallet)
|
||||
.toList();
|
||||
return new UsTruckOrderResponses.Detail(summary(o), o.getOriginAddress(), o.getDestinationAddress(),
|
||||
@@ -413,30 +410,37 @@ public class UsTruckOrderService {
|
||||
h.getFromStatus(), h.getToStatus(), h.getOperatorId(), h.getIdempotencyKey(),
|
||||
h.getRemark(), h.getCreatedAt())).toList());
|
||||
}
|
||||
/** 将实际托盘记录转换为响应。 */
|
||||
private UsTruckOrderResponses.Pallet pallet(UsTruckOrderPallet p) {
|
||||
return new UsTruckOrderResponses.Pallet(p.getId(), p.getPalletNo(), value(p.getSequenceNo()),
|
||||
p.getSource(), p.getStatus(), p.getWarehousePalletId(), p.getPackageCount(), p.getReceivedAt());
|
||||
}
|
||||
/** 构造客户及业务订单关联键。 */
|
||||
private com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.OrderKey poolKey(UsTruckOrder o) { return new com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.OrderKey(o.getCustomerId(), "US_TRUCK", o.getId()); }
|
||||
/** 转换需求字段与承运安排摘要。 */
|
||||
private UsTruckOrderResponses.Summary summary(UsTruckOrder o) { return summary(o, fleetPool.arrangement(poolKey(o))); }
|
||||
private UsTruckOrderResponses.Summary summary(UsTruckOrder o, com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.Arrangement arrangement) { return new UsTruckOrderResponses.Summary(o.getId(), o.getCustomerId(), o.getOrderNo(), o.getTransportType(), o.getOrigin(), o.getDestination(), o.getContainerType(), value(o.getPalletCount()), value(o.getPackageCount()), value(o.getReceivedPalletCount()), value(o.getAllocatedPalletCount()), o.getTotalWeightKg(), o.getTotalVolumeCbm(), o.getBudgetAmount(), o.getCurrency(), o.getContactName(), o.getContactInfo(), o.getRequestedDate(), o.getStatus(), value(o.getVersion()), o.getCreatedAt(), o.getUpdatedAt(), arrangement); }
|
||||
/** 转换需求字段与承运安排摘要。 */
|
||||
private UsTruckOrderResponses.Summary summary(UsTruckOrder o, com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.Arrangement arrangement) { return new UsTruckOrderResponses.Summary(o.getId(), o.getCustomerId(), o.getOrderNo(), o.getVehicleType(), o.getOrigin(), o.getDestination(), value(o.getPalletCount()), value(o.getPackageCount()), value(o.getReceivedPalletCount()), value(o.getAllocatedPalletCount()), o.getTotalWeightKg(), o.getTotalVolumeCbm(), o.getBudgetAmount(), o.getCurrency(), o.getOriginContactName(), o.getOriginContactPhone(), o.getDestinationContactName(), o.getDestinationContactPhone(), o.getPickupAt(), o.getDeliveryAt(), o.getStatus(), value(o.getVersion()), o.getCreatedAt(), o.getUpdatedAt(), arrangement); }
|
||||
/** 保留订单状态变更和操作人证据。 */
|
||||
private void history(UsTruckOrder o, String operationType, String from, String to, String operator,
|
||||
String idempotencyKey, String remark, LocalDateTime at) {
|
||||
UsTruckOrderStatusHistory h = new UsTruckOrderStatusHistory();
|
||||
h.setId(IdWorker.getId()); h.setOrderId(o.getId()); h.setCustomerId(o.getCustomerId());
|
||||
h.setOperationType(operationType); h.setFromStatus(from); h.setToStatus(to);
|
||||
h.setOperatorId(operator); h.setIdempotencyKey(trim(idempotencyKey)); h.setRemark(trim(remark));
|
||||
h.setCreatedAt(at); histories.insert(h);
|
||||
h.setCreatedAt(at); repository.insertHistory(h);
|
||||
}
|
||||
/** 仅为已知的预报数量建立待收托盘。 */
|
||||
private void createPlannedPallets(UsTruckOrder order) {
|
||||
int existing = pallets.listOrder(order.getCustomerId(), order.getId()).size();
|
||||
int existing = repository.pallets(order.getCustomerId(), order.getId()).size();
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (int sequence = existing + 1; sequence <= value(order.getPalletCount()); sequence++) {
|
||||
UsTruckOrderPallet pallet = newPallet(order, sequence, "ORDER_PLAN", now);
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallets.insert(pallet);
|
||||
repository.insertPallet(pallet);
|
||||
}
|
||||
}
|
||||
/** 建立指定序号的订单托盘记录。 */
|
||||
private UsTruckOrderPallet newPallet(UsTruckOrder order, int sequence, String source, LocalDateTime now) {
|
||||
UsTruckOrderPallet pallet = new UsTruckOrderPallet();
|
||||
pallet.setOrderId(order.getId()); pallet.setCustomerId(order.getCustomerId());
|
||||
@@ -445,17 +449,45 @@ public class UsTruckOrderService {
|
||||
pallet.setCreatedAt(now); pallet.setUpdatedAt(now);
|
||||
return pallet;
|
||||
}
|
||||
private void validateQuery(UsTruckOrderRequests.Query q) { if (StringUtils.hasText(q.status()) && !STATUSES.contains(norm(q.status()))) throw badRequest("Unsupported truck order status"); if (StringUtils.hasText(q.transportType())) validateTransport(q.transportType()); if (q.requestedFrom() != null && q.requestedTo() != null && q.requestedFrom().isAfter(q.requestedTo())) throw badRequest("requestedFrom must not be after requestedTo"); }
|
||||
private void validateTransport(String v) { if (!TRANSPORT_TYPES.contains(norm(v))) throw badRequest("Unsupported transport type"); }
|
||||
/** 校验状态、车型及取货时间筛选区间。 */
|
||||
private void validateQuery(UsTruckOrderRequests.Query q) {
|
||||
if (StringUtils.hasText(q.status()) && !STATUSES.contains(norm(q.status()))) throw badRequest("Unsupported truck order status");
|
||||
if (StringUtils.hasText(q.vehicleType()) && !VEHICLE_TYPES.contains(q.vehicleType())) throw badRequest("请选择有效的车辆类型");
|
||||
if (q.pickupFrom() != null && q.pickupTo() != null && q.pickupFrom().isAfter(q.pickupTo())) throw badRequest("取货时间起点不能晚于终点");
|
||||
}
|
||||
|
||||
/** 校验两端必填资料和先取后送的时间顺序;不进行时区转换。 */
|
||||
private void validateRequirements(String vehicleType, String originName, String originPhone,
|
||||
String destinationName, String destinationPhone,
|
||||
LocalDateTime pickupAt, LocalDateTime deliveryAt) {
|
||||
if (vehicleType == null || !VEHICLE_TYPES.contains(vehicleType)) throw badRequest("请选择有效的车辆类型");
|
||||
if (!StringUtils.hasText(originName) || !StringUtils.hasText(originPhone)
|
||||
|| !StringUtils.hasText(destinationName) || !StringUtils.hasText(destinationPhone)) throw badRequest("请完整填写起运地和目的地联系人及联系电话");
|
||||
if (pickupAt == null || deliveryAt == null) throw badRequest("请填写取货时间和送货时间");
|
||||
if (deliveryAt.isBefore(pickupAt)) throw badRequest("送货时间不能早于取货时间");
|
||||
}
|
||||
/** 计算请求摘要以校验幂等重放。 */
|
||||
private String hash(Object v) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(objectMapper.writeValueAsBytes(v))); } catch (JsonProcessingException | NoSuchAlgorithmException e) { throw new IllegalStateException("Cannot hash truck request", e); } }
|
||||
/** 推进订单版本和更新时间。 */
|
||||
private static void touch(UsTruckOrder o) { o.setVersion(value(o.getVersion()) + 1); o.setUpdatedAt(LocalDateTime.now()); }
|
||||
/** 仅更新非空业务文字。 */
|
||||
private static void setText(String v, Consumer<String> c) { if (StringUtils.hasText(v)) c.accept(v.trim()); }
|
||||
/** 更新请求明确携带的可选文字。 */
|
||||
private static void setPresent(String v, Consumer<String> c) { if (v != null) c.accept(trim(v)); }
|
||||
/** 统一处理可选文字空值。 */
|
||||
private static String trim(String v) { return v == null || v.isBlank() ? null : v.trim(); }
|
||||
/** 统一状态与币种编码大小写。 */
|
||||
private static String norm(String v) { return v.trim().toUpperCase(); }
|
||||
/** 未指定币种时使用美元。 */
|
||||
private static String defaultCurrency(String v) { return StringUtils.hasText(v) ? norm(v) : "USD"; }
|
||||
private static int value(Integer v) { return v == null ? 0 : v; } private static long value(Long v) { return v == null ? 0 : v; }
|
||||
/** 将计数空值转换为零。 */
|
||||
private static int value(Integer v) { return v == null ? 0 : v; }
|
||||
/** 将计数空值转换为零。 */
|
||||
private static long value(Long v) { return v == null ? 0 : v; }
|
||||
/** 构造请求校验错误。 */
|
||||
private static ResponseStatusException badRequest(String m) { return new ResponseStatusException(HttpStatus.BAD_REQUEST, m); }
|
||||
/** 构造状态或并发冲突错误。 */
|
||||
private static ResponseStatusException conflict(String m) { return new ResponseStatusException(HttpStatus.CONFLICT, m); }
|
||||
/** 构造客户范围内订单不存在错误。 */
|
||||
private static ResponseStatusException notFound() { return new ResponseStatusException(HttpStatus.NOT_FOUND, "US truck order not found"); }
|
||||
}
|
||||
|
||||
@@ -2784,13 +2784,12 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order (
|
||||
id BIGINT NOT NULL COMMENT '主键ID',
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||
order_no VARCHAR(64) NOT NULL COMMENT '订单号',
|
||||
transport_type VARCHAR(32) NOT NULL COMMENT '运输类型',
|
||||
vehicle_type VARCHAR(32) NULL COMMENT '车辆类型:PALLET_15为15拖,PALLET_30为30拖;拼车未提供时为空',
|
||||
origin VARCHAR(255) NOT NULL COMMENT '起运地',
|
||||
origin_address VARCHAR(500) NULL COMMENT '起运地址',
|
||||
destination VARCHAR(255) NOT NULL COMMENT '目的地',
|
||||
destination_address VARCHAR(500) NULL COMMENT '目的地址',
|
||||
container_type VARCHAR(32) NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜',
|
||||
pallet_count INT NOT NULL DEFAULT 1 COMMENT '客户申报托盘数',
|
||||
pallet_count INT NOT NULL DEFAULT 0 COMMENT '确认托盘数;独立下单首次实收确定,拼车来源取真实预报',
|
||||
package_count INT NULL COMMENT '可选箱数,零或空值均有效',
|
||||
received_pallet_count INT NOT NULL DEFAULT 0 COMMENT 'PDA实收托盘数',
|
||||
allocated_pallet_count INT NOT NULL DEFAULT 0 COMMENT '已分配到合车行程的托盘数',
|
||||
@@ -2798,9 +2797,12 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order (
|
||||
total_volume_cbm DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '总体积(立方米)',
|
||||
budget_amount DECIMAL(18,2) NULL COMMENT '预算金额',
|
||||
currency VARCHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种',
|
||||
contact_name VARCHAR(128) NOT NULL COMMENT '联系人姓名',
|
||||
contact_info VARCHAR(128) NOT NULL COMMENT '联系人联系方式',
|
||||
requested_date DATE NOT NULL COMMENT '要求服务日期',
|
||||
origin_contact_name VARCHAR(128) NULL COMMENT '起运地联系人',
|
||||
origin_contact_phone VARCHAR(128) NULL COMMENT '起运地联系电话',
|
||||
destination_contact_name VARCHAR(128) NULL COMMENT '目的地联系人',
|
||||
destination_contact_phone VARCHAR(128) NULL COMMENT '目的地联系电话',
|
||||
pickup_at DATETIME NULL COMMENT '取货时间,按填写的日期时分保存,不转换时区',
|
||||
delivery_at DATETIME NULL COMMENT '送货时间,按填写的日期时分保存;拼车来源未提供时为空',
|
||||
status VARCHAR(32) NOT NULL COMMENT '美国卡车订单状态',
|
||||
remark VARCHAR(1000) NULL COMMENT '备注',
|
||||
idempotency_key VARCHAR(255) NOT NULL COMMENT '业务幂等键',
|
||||
@@ -2813,7 +2815,7 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order (
|
||||
UNIQUE KEY uk_ll_us_truck_order_no(customer_id,order_no),
|
||||
UNIQUE KEY uk_ll_us_truck_order_key(customer_id,idempotency_key),
|
||||
KEY idx_ll_us_truck_order_list(customer_id,status,created_at),
|
||||
KEY idx_ll_us_truck_order_date(customer_id,requested_date,created_at)
|
||||
KEY idx_ll_us_truck_order_date(customer_id,pickup_at,created_at)
|
||||
) ENGINE=InnoDB COMMENT='美国卡车运输订单';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_us_truck_order_pallet (
|
||||
|
||||
@@ -1421,9 +1421,9 @@ paths:
|
||||
- { name: orderNo, in: query, schema: { type: string } }
|
||||
- { name: routeKeyword, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { $ref: '#/components/schemas/TruckStatus' } }
|
||||
- { name: transportType, in: query, schema: { type: string } }
|
||||
- { name: requestedFrom, in: query, schema: { type: string, format: date } }
|
||||
- { name: requestedTo, in: query, schema: { type: string, format: date } }
|
||||
- { name: vehicleType, in: query, schema: { type: string } }
|
||||
- { name: pickupFrom, in: query, schema: { type: string, format: date-time } }
|
||||
- { name: pickupTo, in: query, schema: { type: string, format: date-time } }
|
||||
responses: { '200': { description: 当前客户本土卡订单分页, content: { application/json: { schema: { $ref: '#/components/schemas/TruckPageResponse' } } } } }
|
||||
post:
|
||||
summary: 客户创建美国本土卡订单
|
||||
@@ -3610,11 +3610,10 @@ components:
|
||||
id: { type: string, pattern: '^[0-9]+$' }
|
||||
customerId: { type: string }
|
||||
orderNo: { type: string }
|
||||
transportType: { type: string, nullable: true }
|
||||
vehicleType: { type: string, enum: [PALLET_15, PALLET_30], description: '15拖或30拖;拼车来源未知时为空', nullable: true }
|
||||
origin: { type: string }
|
||||
destination: { type: string }
|
||||
containerType: { type: string, nullable: true, enum: [40GP, 40HQ, 45HQ], description: 已知时为平台统一柜型;池询价未提供时未知,不默认40HQ }
|
||||
palletCount: { type: integer }
|
||||
palletCount: { type: integer, description: 已确认托数;新建独立订单为0,首次实收确定,不从车型推导 }
|
||||
packageCount: { type: integer }
|
||||
receivedPalletCount: { type: integer }
|
||||
allocatedPalletCount: { type: integer }
|
||||
@@ -3622,9 +3621,12 @@ components:
|
||||
totalVolumeCbm: { type: number }
|
||||
budgetAmount: { type: number, nullable: true }
|
||||
currency: { type: string, nullable: true }
|
||||
contactName: { type: string, nullable: true }
|
||||
contactInfo: { type: string, nullable: true }
|
||||
requestedDate: { type: string, format: date, nullable: true }
|
||||
originContactName: { type: string, nullable: true, maxLength: 128 }
|
||||
originContactPhone: { type: string, nullable: true, maxLength: 128 }
|
||||
destinationContactName: { type: string, nullable: true, maxLength: 128 }
|
||||
destinationContactPhone: { type: string, nullable: true, maxLength: 128 }
|
||||
pickupAt: { type: string, nullable: true, format: date-time, description: '取货日期时分,使用无偏移量的本地时间,不转换时区' }
|
||||
deliveryAt: { type: string, nullable: true, format: date-time, description: '送货日期时分,不能早于取货时间,不转换时区' }
|
||||
status: { $ref: '#/components/schemas/TruckStatus' }
|
||||
version: { type: integer, format: int64 }
|
||||
createdAt: { type: string, format: date-time }
|
||||
@@ -4031,45 +4033,47 @@ components:
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
TruckCreate:
|
||||
type: object
|
||||
required: [origin, destination, palletCount, totalWeightKg, totalVolumeCbm, contactName, contactInfo, requestedDate, idempotencyKey]
|
||||
required: [vehicleType, origin, destination, totalWeightKg, totalVolumeCbm, originContactName, originContactPhone, destinationContactName, destinationContactPhone, pickupAt, deliveryAt, idempotencyKey]
|
||||
properties:
|
||||
transportType: { type: string, enum: [PORT_PICKUP, WAREHOUSE_TO_WAREHOUSE, WAREHOUSE_TO_DOOR] }
|
||||
vehicleType: { type: string, enum: [PALLET_15, PALLET_30], description: '15拖或30拖;拼车来源未知时为空' }
|
||||
origin: { type: string, maxLength: 255 }
|
||||
originAddress: { type: string, nullable: true, maxLength: 500 }
|
||||
destination: { type: string, maxLength: 255 }
|
||||
destinationAddress: { type: string, nullable: true, maxLength: 500 }
|
||||
containerType: { type: string, enum: [40GP, 40HQ, 45HQ], default: 40HQ, maxLength: 32 }
|
||||
palletCount: { type: integer, minimum: 1 }
|
||||
packageCount: { type: integer, nullable: true, minimum: 0 }
|
||||
totalWeightKg: { type: number, minimum: 0 }
|
||||
totalVolumeCbm: { type: number, minimum: 0 }
|
||||
budgetAmount: { type: number, nullable: true, minimum: 0 }
|
||||
currency: { type: string, minLength: 3, maxLength: 3, default: USD }
|
||||
contactName: { type: string, maxLength: 128 }
|
||||
contactInfo: { type: string, maxLength: 128 }
|
||||
requestedDate: { type: string, format: date }
|
||||
originContactName: { type: string, maxLength: 128 }
|
||||
originContactPhone: { type: string, maxLength: 128 }
|
||||
destinationContactName: { type: string, maxLength: 128 }
|
||||
destinationContactPhone: { type: string, maxLength: 128 }
|
||||
pickupAt: { type: string, format: date-time, description: '取货日期时分,使用无偏移量的本地时间,不转换时区' }
|
||||
deliveryAt: { type: string, format: date-time, description: '送货日期时分,不能早于取货时间,不转换时区' }
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
idempotencyKey: { type: string, maxLength: 255 }
|
||||
TruckUpdate:
|
||||
type: object
|
||||
required: [version]
|
||||
required: [version, vehicleType, originContactName, originContactPhone, destinationContactName, destinationContactPhone, pickupAt, deliveryAt]
|
||||
properties:
|
||||
version: { type: integer, format: int64, minimum: 0 }
|
||||
transportType: { type: string, enum: [PORT_PICKUP, WAREHOUSE_TO_WAREHOUSE, WAREHOUSE_TO_DOOR] }
|
||||
vehicleType: { type: string, enum: [PALLET_15, PALLET_30], description: '15拖或30拖;拼车来源未知时为空' }
|
||||
origin: { type: string, maxLength: 255 }
|
||||
originAddress: { type: string, nullable: true, maxLength: 500 }
|
||||
destination: { type: string, maxLength: 255 }
|
||||
destinationAddress: { type: string, nullable: true, maxLength: 500 }
|
||||
containerType: { type: string, enum: [40GP, 40HQ, 45HQ], maxLength: 32 }
|
||||
palletCount: { type: integer, minimum: 1 }
|
||||
packageCount: { type: integer, minimum: 0 }
|
||||
totalWeightKg: { type: number, minimum: 0 }
|
||||
totalVolumeCbm: { type: number, minimum: 0 }
|
||||
budgetAmount: { type: number, nullable: true, minimum: 0 }
|
||||
currency: { type: string, minLength: 3, maxLength: 3 }
|
||||
contactName: { type: string, maxLength: 128 }
|
||||
contactInfo: { type: string, maxLength: 128 }
|
||||
requestedDate: { type: string, format: date }
|
||||
originContactName: { type: string, maxLength: 128 }
|
||||
originContactPhone: { type: string, maxLength: 128 }
|
||||
destinationContactName: { type: string, maxLength: 128 }
|
||||
destinationContactPhone: { type: string, maxLength: 128 }
|
||||
pickupAt: { type: string, format: date-time, description: '取货日期时分,使用无偏移量的本地时间,不转换时区' }
|
||||
deliveryAt: { type: string, format: date-time, description: '送货日期时分,不能早于取货时间,不转换时区' }
|
||||
remark: { type: string, nullable: true, maxLength: 1000 }
|
||||
LooseboardStatus: { type: string, enum: [PENDING_ACCEPTANCE, WAITING_INBOUND, RECEIVING, WAITING_CONSOLIDATION, PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, CANCELLED] }
|
||||
LooseboardStatusChange:
|
||||
|
||||
+4
-4
@@ -96,14 +96,14 @@ class FleetPoolOrderBridgeTest {
|
||||
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
|
||||
assertThat(looseOrder.getPalletRequired()).isNull();assertThat(looseOrder.getContainerType()).isNull();
|
||||
}
|
||||
@Test void truckInquiryPreservesUnknownContainerWhileZeroActualCountsRemainRealUntilReceipt(){
|
||||
@Test void truckInquiryPreservesUnknownVehicleWhileZeroActualCountsRemainRealUntilReceipt(){
|
||||
var source=input("US_TRUCK");service.confirm(source,"op");
|
||||
assertThat(truckOrder.getContainerType()).isNull();assertThat(truckOrder.getPalletCount()).isEqualTo(source.expectedPalletCount());
|
||||
assertThat(truckOrder.getVehicleType()).isNull();assertThat(truckOrder.getPalletCount()).isEqualTo(source.expectedPalletCount());
|
||||
assertThat(truckOrder.getReceivedPalletCount()).isZero();assertThat(truckOrder.getAllocatedPalletCount()).isZero();
|
||||
assertThat(truckOrder.getPackageCount()).isEqualTo(source.packageCount());assertThat(truckOrder.getTransportType()).isEqualTo("WAREHOUSE_TO_WAREHOUSE");
|
||||
assertThat(truckOrder.getPackageCount()).isEqualTo(source.packageCount());assertThat(truckOrder.getDestinationContactName()).isNull();assertThat(truckOrder.getDeliveryAt()).isNull();
|
||||
assertThat(truckOrder.getOriginAddress()).isNull();assertThat(truckOrder.getDestinationAddress()).isNull();assertThat(truckOrder.getBudgetAmount()).isNull();
|
||||
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
|
||||
assertThat(truckOrder.getReceivedPalletCount()).isEqualTo(2);assertThat(truckOrder.getContainerType()).isNull();
|
||||
assertThat(truckOrder.getReceivedPalletCount()).isEqualTo(2);assertThat(truckOrder.getVehicleType()).isNull();
|
||||
}
|
||||
@Test void incomingMustMatchCustomerWarehouseAndDestinationAndNeverBindSecondTailOrder(){
|
||||
service.confirm(input("LOCAL_LOOSEBOARD"),"op");
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package com.baiyue.lastleg.trucking;
|
||||
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
|
||||
import com.baiyue.lastleg.trucking.mapper.*;
|
||||
import com.baiyue.lastleg.trucking.repository.UsTruckOrderRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
class UsTruckOrderPersistenceTest {
|
||||
/** 使用模块基线和真实 Mapper 验证新字段保存、回读、更新与客户隔离。 */
|
||||
@Test
|
||||
void contactsVehicleAndLocalMinuteTimesRoundTripThroughActualMapper() throws Exception {
|
||||
var ds = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
String baseline;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
int start = baseline.indexOf("CREATE TABLE IF NOT EXISTS ll_us_truck_order (");
|
||||
String ddl = baseline.substring(start, baseline.indexOf(';', start));
|
||||
// H2 使用真实字段定义,去掉 MySQL 专属的索引和表选项。
|
||||
ddl = ddl.replaceAll("(?m)^\\s*(?:UNIQUE KEY|KEY) [^\\n]*\\n?", "");
|
||||
ddl = ddl.substring(0, ddl.indexOf(") ENGINE=")) + ")";
|
||||
ddl = ddl.replaceAll(",\\s*\\)$", ")");
|
||||
new JdbcTemplate(ds).execute(ddl);
|
||||
var config = new MybatisConfiguration();
|
||||
config.setMapUnderscoreToCamelCase(true);
|
||||
config.addMapper(UsTruckOrderMapper.class);
|
||||
var factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(ds); factory.setConfiguration(config);
|
||||
var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(UsTruckOrderMapper.class);
|
||||
var repository = new UsTruckOrderRepository(mapper, mock(UsTruckOrderPalletMapper.class), mock(UsTruckOrderStatusHistoryMapper.class), mock(UsTruckOrderOperationMapper.class));
|
||||
var order = new UsTruckOrder();
|
||||
order.setId(1L); order.setCustomerId("C-1"); order.setOrderNo("TRUCK-TEST-1");
|
||||
order.setVehicleType("PALLET_15"); order.setOrigin("Los Angeles"); order.setDestination("Ontario");
|
||||
order.setOriginContactName("发货人"); order.setOriginContactPhone("+1 (555) 0100");
|
||||
order.setDestinationContactName("收货人"); order.setDestinationContactPhone("+1-555-0101");
|
||||
order.setPickupAt(LocalDateTime.of(2026, 10, 20, 9, 30)); order.setDeliveryAt(LocalDateTime.of(2026, 10, 20, 15, 45));
|
||||
order.setTotalWeightKg(BigDecimal.ZERO); order.setTotalVolumeCbm(BigDecimal.ZERO);
|
||||
order.setCurrency("USD"); order.setStatus("PENDING_REVIEW"); order.setIdempotencyKey("roundtrip"); order.setRequestHash("0".repeat(64));
|
||||
repository.insertOrder(order);
|
||||
var saved = repository.findNo("C-1", order.getOrderNo());
|
||||
assertEquals("PALLET_15", saved.getVehicleType());
|
||||
assertEquals("发货人", saved.getOriginContactName()); assertEquals("+1 (555) 0100", saved.getOriginContactPhone());
|
||||
assertEquals("收货人", saved.getDestinationContactName()); assertEquals("+1-555-0101", saved.getDestinationContactPhone());
|
||||
assertEquals(order.getPickupAt(), saved.getPickupAt()); assertEquals(order.getDeliveryAt(), saved.getDeliveryAt());
|
||||
assertEquals(0, saved.getPalletCount());
|
||||
assertNull(repository.findNo("C-2", order.getOrderNo())); assertNull(repository.findKey("C-2", "roundtrip"));
|
||||
saved.setVehicleType("PALLET_30"); saved.setDestinationContactPhone("+1-555-0102"); saved.setDeliveryAt(saved.getDeliveryAt().plusMinutes(15));
|
||||
repository.updateOrder(saved);
|
||||
var updated = repository.findKey("C-1", "roundtrip");
|
||||
assertEquals("PALLET_30", updated.getVehicleType()); assertEquals("+1-555-0102", updated.getDestinationContactPhone());
|
||||
assertEquals(LocalDateTime.of(2026, 10, 20, 16, 0), updated.getDeliveryAt());
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package com.baiyue.lastleg.trucking;
|
||||
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import jakarta.validation.Validation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.stream.Collectors;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class UsTruckOrderRequestsTest {
|
||||
/** 新增的七项需求必须由服务端校验,不能仅依赖页面星号。 */
|
||||
@Test
|
||||
void bothEndpointsVehicleAndTimesAreMandatory() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
var request = new UsTruckOrderRequests.Create(null, "LA", null, "ONT", null, 0,
|
||||
BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", " ", null, null, "", null, null, null, "required");
|
||||
var fields = factory.getValidator().validate(request).stream().map(v -> v.getPropertyPath().toString()).collect(Collectors.toSet());
|
||||
assertEquals(java.util.Set.of("vehicleType", "originContactName", "originContactPhone", "destinationContactName", "destinationContactPhone", "pickupAt", "deliveryAt"), fields);
|
||||
}
|
||||
}
|
||||
|
||||
/** 两种当前车型及国际电话原样受理,未知车型拒绝。 */
|
||||
@Test
|
||||
void onlyCurrentVehicleTypesAreAccepted() {
|
||||
try (var factory = Validation.buildDefaultValidatorFactory()) {
|
||||
for (String vehicle : java.util.List.of("PALLET_15", "PALLET_30", "OTHER")) {
|
||||
var request = new UsTruckOrderRequests.Create(vehicle, "LA", null, "ONT", null, 0,
|
||||
BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "A", "+1 (555) 0100", "B", "+1-555-0101",
|
||||
LocalDateTime.of(2026, 10, 20, 9, 30), LocalDateTime.of(2026, 10, 20, 15, 45), null, "vehicle");
|
||||
assertEquals(!vehicle.equals("OTHER"), factory.getValidator().validate(request).isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-4
@@ -18,7 +18,7 @@ class UsTruckOrderSchemaContractTest {
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_status_history"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_pallet"));
|
||||
assertTrue(sql.contains("pallet_count INT NOT NULL DEFAULT 1"));
|
||||
assertTrue(sql.contains("pallet_count INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("received_pallet_count INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("allocated_pallet_count INT NOT NULL DEFAULT 0"));
|
||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_us_truck_order_operation"));
|
||||
@@ -50,7 +50,7 @@ class UsTruckOrderSchemaContractTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void poolUnknownContainerSurvivesOmittedNullInsertColumnInBothBaselines() throws Exception {
|
||||
void newTruckDemandFieldsExistAndRemovedInputsAreAbsentInBothBaselines() throws Exception {
|
||||
String baseline;
|
||||
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
|
||||
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||
@@ -59,8 +59,9 @@ class UsTruckOrderSchemaContractTest {
|
||||
for (String sql : java.util.List.of(baseline, unified)) {
|
||||
String normalized = sql.toLowerCase().replace("create table if not exists", "create table");
|
||||
String block = tableBlock(normalized, "ll_us_truck_order");
|
||||
assertTrue(java.util.regex.Pattern.compile("(?:^|,)\\s*container_type\\s+varchar\\(32\\)\\s+(?:collate\\s+\\w+\\s+)?(?:null\\s+)?default\\s+null\\b").matcher(block).find(),
|
||||
"Unknown container must remain null when MyBatis omits its insert column");
|
||||
for (String column : java.util.List.of("vehicle_type", "origin_contact_name", "origin_contact_phone", "destination_contact_name", "destination_contact_phone", "pickup_at", "delivery_at")) assertTrue(block.contains(column));
|
||||
for (String removed : java.util.List.of("transport_type", "container_type", "requested_date")) org.junit.jupiter.api.Assertions.assertFalse(block.contains(removed));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -75,9 +75,9 @@ class UsTruckOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void platformCreateUsesExplicitCustomerAndCreatePermission() {
|
||||
var request = new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null,
|
||||
"Ontario", null, "40HQ", 1, 0, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO,
|
||||
null, "USD", "Alice", "+1-555-0100", java.time.LocalDate.now(), null, "create-key");
|
||||
var request = new UsTruckOrderRequests.Create("PALLET_15", "Los Angeles", null,
|
||||
"Ontario", null, 0, java.math.BigDecimal.ZERO, java.math.BigDecimal.ZERO,
|
||||
null, "USD", "Alice", "+1-555-0100", "Bob", "+1-555-0101", java.time.LocalDateTime.now(), java.time.LocalDateTime.now().plusHours(1), null, "create-key");
|
||||
platform.create(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_CREATE)),
|
||||
"100", request);
|
||||
verify(service).create("100", "7", request);
|
||||
@@ -85,8 +85,8 @@ class UsTruckOrderControllerTest {
|
||||
|
||||
@Test
|
||||
void platformUpdateUsesExplicitCustomerAndEditPermission() {
|
||||
var request = new UsTruckOrderRequests.Update(0L, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null, null, null);
|
||||
var request = new UsTruckOrderRequests.Update(0L, "PALLET_30", null, null, null, null,
|
||||
null, null, null, null, null, "Alice", "100", "Bob", "200", java.time.LocalDateTime.now(), java.time.LocalDateTime.now().plusHours(1), null);
|
||||
platform.update(user(0L, "PLATFORM", Set.of(com.baiyue.lastleg.security.OverseasPermissions.TRUCK_EDIT)),
|
||||
"100", "TK1", request);
|
||||
verify(service).update("100", "TK1", "7", request);
|
||||
|
||||
+38
-20
@@ -54,18 +54,15 @@ class UsTruckOrderServiceTest {
|
||||
void setUp() {
|
||||
service = new UsTruckOrderService();
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "fleetPool", org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderQueries.class));
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "pallets", pallets);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "operations", operations);
|
||||
ReflectionTestUtils.setField(service, "repository", new com.baiyue.lastleg.trucking.repository.UsTruckOrderRepository(orders, pallets, histories, operations));
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", orderNoGenerator);
|
||||
order = order(UsTruckOrderService.PENDING_REVIEW, 0L, 2);
|
||||
lenient().when(orders.lockNo("100", "TK1")).thenAnswer(call -> order);
|
||||
when(orders.findNo("100", "TK1")).thenAnswer(call -> order);
|
||||
when(pallets.listOrder(anyString(), anyLong())).thenAnswer(call -> palletRows);
|
||||
lenient().when(orders.findNo("100", "TK1")).thenAnswer(call -> order);
|
||||
lenient().when(pallets.listOrder(anyString(), anyLong())).thenAnswer(call -> palletRows);
|
||||
lenient().when(histories.listOrder("100", 1L)).thenAnswer(call -> historyRows);
|
||||
when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
|
||||
lenient().when(histories.insert(any(UsTruckOrderStatusHistory.class))).thenAnswer(call -> {
|
||||
historyRows.add(call.getArgument(0)); return 1;
|
||||
});
|
||||
lenient().when(pallets.insert(any(UsTruckOrderPallet.class))).thenAnswer(call -> {
|
||||
@@ -82,18 +79,39 @@ class UsTruckOrderServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void createDefaultsTheContainerTypeAndRejectsRemovedTypes() {
|
||||
void createStoresBothContactsAndMinuteTimesWithoutInventingPallets() {
|
||||
when(orderNoGenerator.next(anyString(), anyString(), anyLong())).thenReturn("TK1");
|
||||
when(histories.listOrder(anyString(), anyLong())).thenReturn(historyRows);
|
||||
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> {
|
||||
order = call.getArgument(0);
|
||||
return 1;
|
||||
});
|
||||
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> { order = call.getArgument(0); return 1; });
|
||||
var request = create("PALLET_15", "new-truck");
|
||||
var detail = service.create("100", "operator", request);
|
||||
assertEquals("PALLET_15", detail.summary().vehicleType());
|
||||
assertEquals("Alice", detail.summary().originContactName());
|
||||
assertEquals("Bob", detail.summary().destinationContactName());
|
||||
assertEquals("+1-555-0101", detail.summary().destinationContactPhone());
|
||||
assertEquals(request.pickupAt(), detail.summary().pickupAt());
|
||||
assertEquals(request.deliveryAt(), detail.summary().deliveryAt());
|
||||
assertEquals(0, detail.summary().palletCount());
|
||||
assertTrue(detail.pallets().isEmpty());
|
||||
assertThrows(ResponseStatusException.class, () -> service.create("100", "operator", create("OTHER", "invalid")));
|
||||
}
|
||||
|
||||
var detail = service.create("100", "operator", create(null, "default-type"));
|
||||
@Test
|
||||
void firstActualReceiptAcceptsRealQuantityWithoutForecastOverage() {
|
||||
order.setPalletCount(0);
|
||||
service.review("100", "TK1", "operator", new UsTruckOrderRequests.Review(0L, "APPROVE", "review-new", null));
|
||||
assertTrue(palletRows.isEmpty());
|
||||
var received = service.receivePallets("100", "TK1", "receiver", new UsTruckOrderRequests.ReceivePallets(1L, 3, 6, List.of(2, 2, 2), "first-real", null));
|
||||
assertEquals(UsTruckOrderService.WAITING_CONSOLIDATION, received.summary().status());
|
||||
assertEquals(3, received.summary().receivedPalletCount());
|
||||
assertTrue(palletRows.stream().allMatch(row -> "RECEIVED".equals(row.getStatus())));
|
||||
}
|
||||
|
||||
assertEquals("40HQ", detail.summary().containerType());
|
||||
assertThrows(BizException.class, () -> service.create("100", "operator", create("40HC", "retired-type")));
|
||||
@Test
|
||||
void reversedTimesAreRejectedBeforeCreatingAnOrder() {
|
||||
var request = create("PALLET_30", "reversed");
|
||||
var invalid = new UsTruckOrderRequests.Create(request.vehicleType(), request.origin(), request.originAddress(), request.destination(), request.destinationAddress(), request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(), request.currency(), request.originContactName(), request.originContactPhone(), request.destinationContactName(), request.destinationContactPhone(), request.deliveryAt(), request.pickupAt(), request.remark(), request.idempotencyKey());
|
||||
assertThrows(ResponseStatusException.class, () -> service.create("100", "operator", invalid));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,16 +206,16 @@ class UsTruckOrderServiceTest {
|
||||
return new UsTruckOrderRequests.Command(version, key, key);
|
||||
}
|
||||
|
||||
private UsTruckOrderRequests.Create create(String containerType, String key) {
|
||||
return new UsTruckOrderRequests.Create("PORT_PICKUP", "Los Angeles", null, "Ontario", null,
|
||||
containerType, 1, 0, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "Alice",
|
||||
"+1-555-0100", LocalDate.now().plusDays(1), null, key);
|
||||
private UsTruckOrderRequests.Create create(String vehicleType, String key) {
|
||||
return new UsTruckOrderRequests.Create(vehicleType, "Los Angeles", null, "Ontario", null,
|
||||
0, BigDecimal.ZERO, BigDecimal.ZERO, null, "USD", "Alice", "+1-555-0100", "Bob", "+1-555-0101",
|
||||
LocalDateTime.of(2026, 10, 20, 9, 30), LocalDateTime.of(2026, 10, 20, 15, 45), null, key);
|
||||
}
|
||||
|
||||
private UsTruckOrder order(String status, long version, int pallets) {
|
||||
UsTruckOrder value = new UsTruckOrder();
|
||||
value.setId(1L); value.setCustomerId("100"); value.setOrderNo("TK1");
|
||||
value.setTransportType("PORT_PICKUP"); value.setOrigin("Los Angeles"); value.setDestination("Ontario");
|
||||
value.setVehicleType("PALLET_15"); value.setOrigin("Los Angeles"); value.setDestination("Ontario");
|
||||
value.setPalletCount(pallets); value.setPackageCount(0); value.setReceivedPalletCount(0);
|
||||
value.setAllocatedPalletCount(0); value.setStatus(status); value.setVersion(version);
|
||||
value.setCreatedAt(LocalDateTime.now()); value.setUpdatedAt(value.getCreatedAt());
|
||||
|
||||
@@ -56,11 +56,13 @@ test('customer identity joins use the same explicit collation as existing fulfil
|
||||
})
|
||||
|
||||
test('commercial schemas do not invent container or palletizing facts for pool orders', () => {
|
||||
for (const name of ['ll_local_looseboard_order', 'll_us_truck_order']) {
|
||||
for (const name of ['ll_local_looseboard_order']) {
|
||||
const definition = table(overseas, 'baiyue_lastleg', name)
|
||||
assert.match(definition, /container_type\s+varchar\(32\)(?:\s+COLLATE\s+\w+)?\s+(?:NULL\s+)?DEFAULT NULL/i)
|
||||
assert.doesNotMatch(definition, /container_type[^\n]*DEFAULT '40HQ'/i)
|
||||
}
|
||||
assert.doesNotMatch(table(overseas, 'baiyue_lastleg', 'll_us_truck_order'), /container_type/i)
|
||||
assert.match(table(overseas, 'baiyue_lastleg', 'll_us_truck_order'), /vehicle_type/i)
|
||||
assert.match(table(overseas, 'baiyue_lastleg', 'll_local_looseboard_order'), /pallet_required\s+varchar\(8\)(?:\s+COLLATE\s+\w+)?\s+(?:NULL\s+)?DEFAULT NULL/i)
|
||||
const sql = read('deploy/sql/migrations/overseas/baiyue_lastleg/V12__preserve_unknown_pool_order_fields.sql')
|
||||
assert.equal((sql.match(/MODIFY COLUMN/g) || []).length, 3)
|
||||
|
||||
@@ -270,11 +270,11 @@ async function procurementFlow(context, prerequisite) {
|
||||
function truckPayload(context) {
|
||||
const suffix = context.marker.replace(/[^A-Za-z0-9]/g, '').slice(-16)
|
||||
return {
|
||||
transportType: 'PORT_PICKUP', origin: `验收起运-${suffix}`, originAddress: 'Long Beach',
|
||||
destination: `验收目的-${suffix}`, destinationAddress: 'Ontario', containerType: '40HQ',
|
||||
palletCount: 1, packageCount: 2, totalWeightKg: 100, totalVolumeCbm: 2.5, budgetAmount: 88,
|
||||
currency: 'USD', contactName: '四期验收联系人', contactInfo: '13800000000',
|
||||
requestedDate: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10), remark: context.marker,
|
||||
vehicleType: 'PALLET_15', origin: `验收起运-${suffix}`, originAddress: 'Long Beach',
|
||||
destination: `验收目的-${suffix}`, destinationAddress: 'Ontario',
|
||||
packageCount: 2, totalWeightKg: 100, totalVolumeCbm: 2.5, budgetAmount: 88,
|
||||
currency: 'USD', originContactName: '验收发货人', originContactPhone: '+1-555-0100', destinationContactName: '验收收货人', destinationContactPhone: '+1-555-0101',
|
||||
pickupAt: `${new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10)}T09:30:00`, deliveryAt: `${new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10)}T15:45:00`, remark: context.marker,
|
||||
idempotencyKey: `${context.marker}-phase4-truck`.slice(0, 255),
|
||||
}
|
||||
}
|
||||
@@ -367,7 +367,7 @@ async function truckFlow(context, customerId) {
|
||||
const detail = await overseas(context, 'GET', '查询 marker 本土卡车详情', path)
|
||||
const before = summary(detail.data)
|
||||
const edited = await overseas(context, 'PATCH', '编辑 marker 本土卡车订单', path,
|
||||
{ version: Number(before?.version), destination: `${payload.destination}-EDIT`, remark: `${context.marker}-truck-edited` })
|
||||
{ ...payload, idempotencyKey: undefined, version: Number(before?.version), destination: `${payload.destination}-EDIT`, remark: `${context.marker}-truck-edited` })
|
||||
const after = summary(edited.data)
|
||||
context.assert(MODULE, '卡车编辑版本递增且普通字段回显', edited.response,
|
||||
after?.destination === `${payload.destination}-EDIT` && Number(after?.version) === Number(before?.version) + 1
|
||||
|
||||
Reference in New Issue
Block a user