暂存提交
This commit is contained in:
+9
@@ -0,0 +1,9 @@
|
||||
package com.baiyue.lastleg.trucking;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.baiyue.lastleg.trucking.mapper")
|
||||
public class TruckingMapperConfiguration {
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.lastleg.trucking.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
|
||||
import com.baiyue.lastleg.trucking.service.UsTruckOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/merchant/v1/last-leg/us-truck-orders")
|
||||
public class MerchantUsTruckOrderController {
|
||||
@Resource private UsTruckOrderService service;
|
||||
|
||||
@PostMapping
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid @RequestBody UsTruckOrderRequests.Create request) {
|
||||
return ApiResponse.success(service.create(customer(user), operator(user), request));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<UsTruckOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid UsTruckOrderRequests.Query query) {
|
||||
return ApiResponse.success(service.page(customer(user), query));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String orderNo) {
|
||||
return ApiResponse.success(service.detail(customer(user), orderNo));
|
||||
}
|
||||
|
||||
@PatchMapping("/{orderNo}")
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable String orderNo, @Valid @RequestBody UsTruckOrderRequests.Update request) {
|
||||
return ApiResponse.success(service.update(customer(user), orderNo, request));
|
||||
}
|
||||
|
||||
private String customer(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||
}
|
||||
return String.valueOf(user.tenantId());
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package com.baiyue.lastleg.trucking.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
|
||||
import com.baiyue.lastleg.trucking.service.UsTruckOrderService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/last-leg/us-truck-orders")
|
||||
public class PlatformUsTruckOrderController {
|
||||
@Resource private UsTruckOrderService service;
|
||||
|
||||
@GetMapping
|
||||
public ApiResponse<PageResult<UsTruckOrderResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@Valid UsTruckOrderRequests.Query query) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.page(null, query));
|
||||
}
|
||||
|
||||
@GetMapping("/{orderNo}")
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String customerId, @PathVariable String orderNo) {
|
||||
requirePlatform(user);
|
||||
return ApiResponse.success(service.detail(requiredCustomer(customerId), orderNo));
|
||||
}
|
||||
|
||||
@PatchMapping("/{orderNo}/status")
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> changeStatus(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String customerId, @PathVariable String orderNo,
|
||||
@Valid @RequestBody UsTruckOrderRequests.ChangeStatus request) {
|
||||
requirePermission(user);
|
||||
return ApiResponse.success(service.changeStatus(requiredCustomer(customerId), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
@PostMapping("/{orderNo}/receive-pallets")
|
||||
public ApiResponse<UsTruckOrderResponses.Detail> receivePallets(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam String customerId, @PathVariable String orderNo,
|
||||
@Valid @RequestBody UsTruckOrderRequests.ReceivePallets request) {
|
||||
requirePermission(user);
|
||||
return ApiResponse.success(service.receivePallets(requiredCustomer(customerId), orderNo, operator(user), request));
|
||||
}
|
||||
|
||||
private void requirePlatform(AuthenticatedUser user) {
|
||||
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform identity is required");
|
||||
}
|
||||
}
|
||||
|
||||
private void requirePermission(AuthenticatedUser user) {
|
||||
requirePlatform(user);
|
||||
if (user.permissions() == null || !user.permissions().contains(UsTruckOrderService.MANAGE_PERMISSION)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,
|
||||
UsTruckOrderService.MANAGE_PERMISSION + " permission is required");
|
||||
}
|
||||
}
|
||||
|
||||
private String requiredCustomer(String customerId) {
|
||||
if (customerId == null || customerId.isBlank() || "0".equals(customerId.trim())) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Target customerId is required");
|
||||
}
|
||||
return customerId.trim();
|
||||
}
|
||||
|
||||
private String operator(AuthenticatedUser user) {
|
||||
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||
}
|
||||
}
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
package com.baiyue.lastleg.trucking.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public final class UsTruckOrderRequests {
|
||||
private UsTruckOrderRequests() {}
|
||||
|
||||
public record Create(
|
||||
@Size(max = 32) String transportType,
|
||||
@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,
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {}
|
||||
|
||||
public record Update(
|
||||
@NotNull @Min(0) Long version,
|
||||
@Size(max = 32) String transportType,
|
||||
@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,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record ChangeStatus(
|
||||
@NotNull @Min(0) Long version,
|
||||
@NotBlank @Size(max = 32) String status,
|
||||
@Size(max = 1000) String remark) {}
|
||||
|
||||
public record ReceivePallets(
|
||||
@NotNull @Min(0) Long version,
|
||||
@NotNull @Min(1) Integer actualPalletCount,
|
||||
@Min(0) Integer packageCount,
|
||||
@Size(max = 500) List<@Min(0) Integer> palletPackageCounts,
|
||||
@NotBlank @Size(max = 255) String idempotencyKey,
|
||||
@Size(max = 1000) String remark) {
|
||||
public ReceivePallets(Long version, Integer actualPalletCount, Integer packageCount,
|
||||
String idempotencyKey, String remark) {
|
||||
this(version, actualPalletCount, packageCount, List.of(), idempotencyKey, remark);
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
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() {}
|
||||
|
||||
public record Summary(
|
||||
Long id, String customerId, String orderNo, String transportType,
|
||||
String origin, String destination, String containerType, int palletCount, int packageCount,
|
||||
int receivedPalletCount, int allocatedPalletCount, int shippedPalletCount,
|
||||
BigDecimal totalWeightKg, BigDecimal totalVolumeCbm, BigDecimal budgetAmount,
|
||||
String currency, String contactName, String contactInfo, LocalDate requestedDate,
|
||||
String status, long version, LocalDateTime createdAt, LocalDateTime updatedAt) {}
|
||||
|
||||
public record History(
|
||||
String fromStatus, String toStatus, String operatorId, String remark, LocalDateTime createdAt) {}
|
||||
|
||||
public record Pallet(Long id, String palletNo, int sequenceNo, String source, String status,
|
||||
Long warehousePalletId, Integer packageCount, LocalDateTime receivedAt) {}
|
||||
|
||||
public record Detail(
|
||||
Summary summary, String originAddress, String destinationAddress, String remark,
|
||||
String createdBy, List<Pallet> pallets, List<History> histories) {}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.lastleg.trucking.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_us_truck_order")
|
||||
public class UsTruckOrder {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private String customerId;
|
||||
private String orderNo;
|
||||
private String transportType;
|
||||
private String origin;
|
||||
private String originAddress;
|
||||
private String destination;
|
||||
private String destinationAddress;
|
||||
private String containerType;
|
||||
private Integer palletCount;
|
||||
private Integer packageCount;
|
||||
private Integer receivedPalletCount;
|
||||
private Integer allocatedPalletCount;
|
||||
private Integer shippedPalletCount;
|
||||
private BigDecimal totalWeightKg;
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private BigDecimal budgetAmount;
|
||||
private String currency;
|
||||
private String contactName;
|
||||
private String contactInfo;
|
||||
private LocalDate requestedDate;
|
||||
private String status;
|
||||
private String remark;
|
||||
private String idempotencyKey;
|
||||
private String requestHash;
|
||||
private String receiptIdempotencyKey;
|
||||
private String receiptRequestHash;
|
||||
private String createdBy;
|
||||
private Long version;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package com.baiyue.lastleg.trucking.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_us_truck_order_pallet")
|
||||
public class UsTruckOrderPallet {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long orderId;
|
||||
private String customerId;
|
||||
private String palletNo;
|
||||
private Integer sequenceNo;
|
||||
private String source;
|
||||
private String status;
|
||||
private Long warehousePalletId;
|
||||
private Integer packageCount;
|
||||
private LocalDateTime receivedAt;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.trucking.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@TableName("ll_us_truck_order_status_history")
|
||||
public class UsTruckOrderStatusHistory {
|
||||
@TableId(type = IdType.INPUT) private Long id;
|
||||
private Long orderId;
|
||||
private String customerId;
|
||||
private String fromStatus;
|
||||
private String toStatus;
|
||||
private String operatorId;
|
||||
private String remark;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.baiyue.lastleg.trucking.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface UsTruckOrderMapper extends BaseMapper<UsTruckOrder> {
|
||||
@Select("SELECT * FROM ll_us_truck_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1")
|
||||
UsTruckOrder findNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
|
||||
|
||||
@Select("SELECT * FROM ll_us_truck_order WHERE customer_id=#{customerId} AND idempotency_key=#{key} LIMIT 1")
|
||||
UsTruckOrder findKey(@Param("customerId") String customerId, @Param("key") String key);
|
||||
|
||||
@Select("SELECT * FROM ll_us_truck_order WHERE customer_id=#{customerId} AND order_no=#{orderNo} LIMIT 1 FOR UPDATE")
|
||||
UsTruckOrder lockNo(@Param("customerId") String customerId, @Param("orderNo") String orderNo);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.baiyue.lastleg.trucking.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderPallet;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface UsTruckOrderPalletMapper extends BaseMapper<UsTruckOrderPallet> {
|
||||
@Select("SELECT * FROM ll_us_truck_order_pallet WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY sequence_no")
|
||||
List<UsTruckOrderPallet> listOrder(@Param("customerId") String customerId, @Param("orderId") Long orderId);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.baiyue.lastleg.trucking.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrderStatusHistory;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface UsTruckOrderStatusHistoryMapper extends BaseMapper<UsTruckOrderStatusHistory> {
|
||||
@Select("SELECT * FROM ll_us_truck_order_status_history WHERE customer_id=#{customerId} AND order_id=#{orderId} ORDER BY created_at,id")
|
||||
List<UsTruckOrderStatusHistory> listOrder(@Param("customerId") String customerId,
|
||||
@Param("orderId") Long orderId);
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
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.lastleg.trucking.dto.UsTruckOrderRequests;
|
||||
import com.baiyue.lastleg.trucking.dto.UsTruckOrderResponses;
|
||||
import com.baiyue.lastleg.trucking.entity.UsTruckOrder;
|
||||
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.UsTruckOrderPalletMapper;
|
||||
import com.baiyue.lastleg.trucking.mapper.UsTruckOrderStatusHistoryMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
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.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Service
|
||||
public class UsTruckOrderService {
|
||||
public static final String MANAGE_PERMISSION = "lastleg:us-trucking:manage";
|
||||
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";
|
||||
public static final String PALLET_OVERAGE_REVIEW = "PALLET_OVERAGE_REVIEW";
|
||||
public static final String PARTIALLY_ALLOCATED = "PARTIALLY_ALLOCATED";
|
||||
public static final String ALLOCATED = "ALLOCATED";
|
||||
public static final String PENDING_DISPATCH = "PENDING_DISPATCH";
|
||||
public static final String DISPATCHED = "DISPATCHED";
|
||||
public static final String IN_TRANSIT = "IN_TRANSIT";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
public static final String REJECTED = "REJECTED";
|
||||
public static final String CANCELLED = "CANCELLED";
|
||||
private static final Set<String> STATUSES = Set.of(PENDING_REVIEW, WAITING_INBOUND, RECEIVING,
|
||||
WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, PARTIALLY_ALLOCATED, ALLOCATED,
|
||||
PENDING_DISPATCH, DISPATCHED, IN_TRANSIT, COMPLETED, REJECTED, CANCELLED);
|
||||
private static final java.util.Map<String, Set<String>> TRANSITIONS = java.util.Map.ofEntries(
|
||||
java.util.Map.entry(PENDING_REVIEW, Set.of(WAITING_INBOUND, REJECTED, CANCELLED)),
|
||||
java.util.Map.entry(WAITING_INBOUND, Set.of(RECEIVING, CANCELLED)),
|
||||
java.util.Map.entry(RECEIVING, Set.of(WAITING_CONSOLIDATION, PALLET_OVERAGE_REVIEW, CANCELLED)),
|
||||
java.util.Map.entry(PALLET_OVERAGE_REVIEW, Set.of(WAITING_CONSOLIDATION, CANCELLED)),
|
||||
java.util.Map.entry(WAITING_CONSOLIDATION, Set.of(PARTIALLY_ALLOCATED, ALLOCATED, CANCELLED)),
|
||||
java.util.Map.entry(PARTIALLY_ALLOCATED, Set.of(ALLOCATED, CANCELLED)),
|
||||
java.util.Map.entry(ALLOCATED, Set.of(PENDING_DISPATCH, CANCELLED)),
|
||||
java.util.Map.entry(PENDING_DISPATCH, Set.of(DISPATCHED, CANCELLED)),
|
||||
java.util.Map.entry(DISPATCHED, Set.of(IN_TRANSIT)),
|
||||
java.util.Map.entry(IN_TRANSIT, Set.of(COMPLETED)));
|
||||
private static final Set<String> TRANSPORT_TYPES = Set.of("PORT_PICKUP", "WAREHOUSE_TO_WAREHOUSE", "WAREHOUSE_TO_DOOR");
|
||||
|
||||
@Resource private UsTruckOrderMapper orders;
|
||||
@Resource private UsTruckOrderPalletMapper pallets;
|
||||
@Resource private UsTruckOrderStatusHistoryMapper histories;
|
||||
@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);
|
||||
if (replay != null) return replay(customerId, replay, hash);
|
||||
if (StringUtils.hasText(request.transportType())) validateTransport(request.transportType());
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
UsTruckOrder order = new UsTruckOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setOrderNo("TK" + now.format(DateTimeFormatter.BASIC_ISO_DATE) + order.getId());
|
||||
order.setTransportType(StringUtils.hasText(request.transportType()) ? norm(request.transportType()) : null);
|
||||
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()) ? norm(request.containerType()) : null);
|
||||
order.setPalletCount(request.palletCount()); order.setPackageCount(value(request.packageCount()));
|
||||
order.setReceivedPalletCount(0); order.setAllocatedPalletCount(0); order.setShippedPalletCount(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.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); }
|
||||
catch (DuplicateKeyException e) {
|
||||
UsTruckOrder concurrent = orders.findKey(customerId, key);
|
||||
if (concurrent != null) return replay(customerId, concurrent, hash);
|
||||
throw e;
|
||||
}
|
||||
history(order, null, PENDING_REVIEW, operatorId, request.remark(), now);
|
||||
return detail(customerId, order.getOrderNo());
|
||||
}
|
||||
|
||||
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);
|
||||
return new PageResult<>(result.getRecords().stream().map(this::summary).toList(), result.getCurrent(),
|
||||
result.getSize(), result.getTotal(), result.getPages());
|
||||
}
|
||||
|
||||
public UsTruckOrderResponses.Detail detail(String customerId, String orderNo) {
|
||||
UsTruckOrder order = orders.findNo(customerId, orderNo);
|
||||
if (order == null) throw notFound();
|
||||
return detail(order, histories.listOrder(customerId, order.getId()));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail update(String customerId, String orderNo, 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())); }
|
||||
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(norm(v)));
|
||||
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); return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail changeStatus(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.ChangeStatus request) {
|
||||
UsTruckOrder order = lock(customerId, orderNo);
|
||||
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
|
||||
String next = norm(request.status()); if (!STATUSES.contains(next)) throw badRequest("Unsupported truck order status");
|
||||
String from = order.getStatus();
|
||||
if (!next.equals(from)) {
|
||||
if (!TRANSITIONS.getOrDefault(from, Set.of()).contains(next)) {
|
||||
throw conflict("Unsupported truck order transition: " + from + " -> " + next);
|
||||
}
|
||||
order.setStatus(next);
|
||||
if (PENDING_REVIEW.equals(from) && WAITING_INBOUND.equals(next)) createPlannedPallets(order);
|
||||
touch(order); orders.updateById(order);
|
||||
history(order, from, next, operatorId, request.remark(), LocalDateTime.now());
|
||||
}
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UsTruckOrderResponses.Detail receivePallets(String customerId, String orderNo, String operatorId,
|
||||
UsTruckOrderRequests.ReceivePallets request) {
|
||||
UsTruckOrder order = lock(customerId, orderNo);
|
||||
String key = request.idempotencyKey().trim();
|
||||
String requestHash = hash(request);
|
||||
if (key.equals(order.getReceiptIdempotencyKey())) {
|
||||
if (!requestHash.equals(order.getReceiptRequestHash())) {
|
||||
throw conflict("Receipt idempotency key was reused with different pallet data");
|
||||
}
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
if (!request.version().equals(value(order.getVersion()))) throw conflict("Order was changed; refresh and retry");
|
||||
if (!Set.of(WAITING_INBOUND, RECEIVING).contains(order.getStatus())) {
|
||||
throw conflict("Only waiting or receiving orders can receive pallets");
|
||||
}
|
||||
|
||||
createPlannedPallets(order);
|
||||
List<UsTruckOrderPallet> current = pallets.listOrder(customerId, order.getId());
|
||||
int planned = value(order.getPalletCount());
|
||||
int actual = request.actualPalletCount();
|
||||
List<Integer> packageCounts = request.palletPackageCounts() == null ? List.of()
|
||||
: request.palletPackageCounts();
|
||||
if (!packageCounts.isEmpty() && packageCounts.size() != actual) {
|
||||
throw badRequest("palletPackageCounts must be empty or contain one value per actual pallet");
|
||||
}
|
||||
if (!packageCounts.isEmpty() && request.packageCount() != null
|
||||
&& packageCounts.stream().mapToInt(Integer::intValue).sum() != request.packageCount()) {
|
||||
throw badRequest("Per-pallet package counts must equal the received package total");
|
||||
}
|
||||
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);
|
||||
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());
|
||||
pallet.setReceivedAt(now);
|
||||
pallet.setUpdatedAt(now);
|
||||
if (pallet.getId() == null) {
|
||||
pallet.setId(IdWorker.getId());
|
||||
pallets.insert(pallet);
|
||||
} else {
|
||||
pallets.updateById(pallet);
|
||||
}
|
||||
}
|
||||
order.setReceivedPalletCount(actual);
|
||||
order.setReceiptIdempotencyKey(key);
|
||||
order.setReceiptRequestHash(requestHash);
|
||||
String from = order.getStatus();
|
||||
String next = actual > planned ? PALLET_OVERAGE_REVIEW
|
||||
: actual == planned ? WAITING_CONSOLIDATION : RECEIVING;
|
||||
order.setStatus(next);
|
||||
touch(order);
|
||||
orders.updateById(order);
|
||||
history(order, from, next, operatorId, request.remark(), now);
|
||||
return detail(customerId, orderNo);
|
||||
}
|
||||
|
||||
private UsTruckOrder lock(String customerId, String orderNo) { UsTruckOrder o = orders.lockNo(customerId, orderNo); if (o == null) throw notFound(); 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()
|
||||
.map(this::pallet)
|
||||
.toList();
|
||||
return new UsTruckOrderResponses.Detail(summary(o), o.getOriginAddress(), o.getDestinationAddress(),
|
||||
o.getRemark(), o.getCreatedBy(), palletDetails,
|
||||
hs.stream().map(h -> new UsTruckOrderResponses.History(h.getFromStatus(), h.getToStatus(),
|
||||
h.getOperatorId(), 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 UsTruckOrderResponses.Summary summary(UsTruckOrder o) { 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()), value(o.getShippedPalletCount()), o.getTotalWeightKg(), o.getTotalVolumeCbm(), o.getBudgetAmount(), o.getCurrency(), o.getContactName(), o.getContactInfo(), o.getRequestedDate(), o.getStatus(), value(o.getVersion()), o.getCreatedAt(), o.getUpdatedAt()); }
|
||||
private void history(UsTruckOrder o, String from, String to, String operator, String remark, LocalDateTime at) { UsTruckOrderStatusHistory h = new UsTruckOrderStatusHistory(); h.setId(IdWorker.getId()); h.setOrderId(o.getId()); h.setCustomerId(o.getCustomerId()); h.setFromStatus(from); h.setToStatus(to); h.setOperatorId(operator); h.setRemark(trim(remark)); h.setCreatedAt(at); histories.insert(h); }
|
||||
private void createPlannedPallets(UsTruckOrder order) {
|
||||
int existing = pallets.listOrder(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);
|
||||
}
|
||||
}
|
||||
private UsTruckOrderPallet newPallet(UsTruckOrder order, int sequence, String source, LocalDateTime now) {
|
||||
UsTruckOrderPallet pallet = new UsTruckOrderPallet();
|
||||
pallet.setOrderId(order.getId()); pallet.setCustomerId(order.getCustomerId());
|
||||
pallet.setPalletNo(order.getOrderNo() + "-P" + String.format("%03d", sequence));
|
||||
pallet.setSequenceNo(sequence); pallet.setSource(source); pallet.setStatus("EXPECTED");
|
||||
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 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 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"); }
|
||||
}
|
||||
Reference in New Issue
Block a user