This commit is contained in:
2026-08-19 14:02:11 +08:00
590 changed files with 23946 additions and 5168 deletions
@@ -0,0 +1,24 @@
package com.baiyue.firstleg.client;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "iam-service", contextId = "firstlegIamCustomerSnapshotClient",
url = "${baiyue.clients.iam.url:}", path = "/internal/iam")
public interface IamCustomerSnapshotClient {
@GetMapping("/finance-customers")
ApiResponse<PageResult<Customer>> page(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("page") int page,
@RequestParam("size") int size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "customerId", required = false) Long customerId);
record Customer(long id, String tenantCode, String tenantName, String status,
String contactName, String contactPhone) {}
}
@@ -5,6 +5,7 @@ import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.http.ResponseEntity;
/** 仅用于确认证据文件属于当前客户和入库单,不在 firstleg 复制文件元数据。 */
@FeignClient(name = "file-service", contextId = "firstlegLoadingEvidenceFileClient",
@@ -15,5 +16,10 @@ public interface LoadingEvidenceFileClient {
@PathVariable long tenantId, @PathVariable long inboundId,
@PathVariable long fileId);
@GetMapping("/{tenantId}/{inboundId}/{fileId}/content")
ResponseEntity<byte[]> download(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long tenantId, @PathVariable long inboundId,
@PathVariable long fileId);
record FileMetadata(Long fileId, String fileName, String businessType, String contentType) {}
}
@@ -31,16 +31,34 @@ public interface OrderCargoClient {
@PathVariable long tenantId,
@PathVariable long orderId);
@GetMapping("/{tenantId}/{orderId}/measurement-declaration")
ApiResponse<MeasurementDeclarationSnapshot> measurementDeclaration(
@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long tenantId, @PathVariable long orderId);
record MeasurementDeclarationSnapshot(
String referenceId, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm, String defaultActualGoodsName) {}
record CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
Integer unitsPerPackage, Integer totalQuantity, BigDecimal netWeightKg,
String cargoRemark) {
String cargoRemark, String customerRemark, String latestInternalNote) {
public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount,
originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null);
originalGrossWeightKg, originalVolumeCbm, null, null, null, null, null, null, null, null);
}
public CargoBinding(Long cargoId, Long orderId, Integer itemNo, String cargoName,
Integer originalPackageCount, BigDecimal originalGrossWeightKg,
BigDecimal originalVolumeCbm, String skuNo, String cargoNameEn,
Integer unitsPerPackage, Integer totalQuantity, BigDecimal netWeightKg,
String cargoRemark) {
this(cargoId, orderId, itemNo, cargoName, originalPackageCount, originalGrossWeightKg,
originalVolumeCbm, skuNo, cargoNameEn, unitsPerPackage, totalQuantity, netWeightKg,
cargoRemark, null, null);
}
}
@@ -13,39 +13,44 @@ import java.time.LocalDateTime;
import java.util.List;
@FeignClient(name = "warehouse-service", contextId = "warehousePalletLocationClient",
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/pallet-locations")
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1")
public interface WarehousePalletLocationClient {
@PostMapping("/reservations")
@GetMapping("/pallets/snapshots")
ApiResponse<List<PalletSnapshot>> palletSnapshots(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("warehouseId") long warehouseId);
@PostMapping("/pallet-locations/reservations")
ApiResponse<BatchResult> reserve(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody ReserveRequest request);
@PostMapping("/confirm")
@PostMapping("/pallet-locations/confirm")
ApiResponse<BatchResult> confirm(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody ConfirmRequest request);
@PostMapping("/release")
@PostMapping("/pallet-locations/release")
ApiResponse<List<ActiveResource>> release(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody ReleaseRequest request);
@PostMapping("/release-selected")
@PostMapping("/pallet-locations/release-selected")
ApiResponse<List<SelectiveReleaseResult>> releaseSelected(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody SelectiveReleaseRequest request);
@PostMapping("/transfer")
@PostMapping("/pallet-locations/transfer")
ApiResponse<List<TransferResult>> transfer(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody TransferRequest request);
@PostMapping("/settlements")
@PostMapping("/pallet-locations/settlements")
ApiResponse<SettlementResult> settle(@RequestHeader("X-Internal-Service-Token") String token,
@RequestBody SettlementRequest request);
@GetMapping("/settlements/target")
@GetMapping("/pallet-locations/settlements/target")
ApiResponse<Long> settlementTarget(@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("sourceBusinessId") long sourceBusinessId,
@RequestParam("idempotencyKey") String idempotencyKey);
@GetMapping("/occupied-resources")
@GetMapping("/pallet-locations/occupied-resources")
ApiResponse<List<OccupiedResource>> occupiedResources(
@RequestHeader("X-Internal-Service-Token") String token,
@RequestParam("tenantId") long tenantId,
@@ -54,18 +59,19 @@ public interface WarehousePalletLocationClient {
record ReserveRequest(long tenantId, long warehouseId, String sourceService, long businessId,
String inboundCode, String idempotencyKey, long operatorId,
BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm, String palletType,
BigDecimal totalVolumeCbm, List<PalletReference> pallets,
LocalDateTime expiresAt, boolean measurementCompleted) {}
record PalletReference(long weightResultId, long palletId) {}
record ConfirmRequest(long tenantId, long warehouseId, String sourceService, long businessId,
long operatorId, BigDecimal totalVolumeCbm, BigDecimal palletCapacityCbm,
int packageCount) {}
long operatorId, BigDecimal totalVolumeCbm, int packageCount) {}
record ReleaseRequest(long tenantId, String sourceService, long businessId, long operatorId,
String reason) {}
record SelectiveReleaseRequest(long tenantId, String sourceService, long businessId,
String idempotencyKey, long operatorId, String reason,
List<Long> locationIds) {}
record AllocationResult(long reservationId, String reservationNo, int allocationNo,
long palletId, String palletCode, long locationId, String locationCode,
long palletId, String palletCode, BigDecimal palletCapacityCbm,
long locationId, String locationCode,
BigDecimal allocatedVolumeCbm, String state) {}
record BatchResult(int palletCount, List<AllocationResult> allocations) {}
record ActiveResource(long reservationId, long warehouseId, long palletId, String palletCode,
@@ -90,6 +96,8 @@ public interface WarehousePalletLocationClient {
List<SettlementTransferItem> remainingItems) {}
record SettlementResult(List<SelectiveReleaseResult> released, List<TransferResult> transferred) {}
record OccupiedResource(long reservationId, long occupancyId, int allocationNo, long warehouseId,
long palletId, long locationId, String locationCode, boolean sharedOverflow,
int allocatedPackageCount) {}
long palletId, long locationId, String locationCode, boolean sharedOverflow,
int allocatedPackageCount) {}
record PalletSnapshot(long palletId, String palletCode, String palletType,
BigDecimal capacityCbm, BigDecimal tareWeightKg) {}
}
@@ -0,0 +1,20 @@
package com.baiyue.firstleg.client;
import com.baiyue.common.core.api.ApiResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "warehouse-service", contextId = "firstlegWarehouseSupplierClient",
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/suppliers")
public interface WarehouseSupplierClient {
@GetMapping("/{supplierId}")
ApiResponse<Snapshot> get(@RequestHeader("X-Internal-Service-Token") String token,
@PathVariable long supplierId,
@RequestParam(defaultValue = "true") boolean requireActive,
@RequestParam(required = false) String category);
record Snapshot(long supplierId, String name, String category, String status) {}
}
@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
@@ -27,17 +28,19 @@ public class InternalFinanceFeeSourceController {
@GetMapping("/inbounds/{inboundId}/fees/{feeId}/source-snapshot")
public ApiResponse<FinanceFeeSourceSnapshot> inbound(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable long inboundId, @PathVariable long feeId) {
@PathVariable long inboundId, @PathVariable long feeId,
@RequestParam @jakarta.validation.constraints.Min(1) int sourceVersion) {
authenticate(token);
return ApiResponse.success(service.inbound(inboundId, feeId));
return ApiResponse.success(service.inbound(inboundId, feeId, sourceVersion));
}
@GetMapping("/containers/{containerId}/fees/{feeId}/source-snapshot")
public ApiResponse<FinanceFeeSourceSnapshot> container(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@PathVariable long containerId, @PathVariable long feeId) {
@PathVariable long containerId, @PathVariable long feeId,
@RequestParam @jakarta.validation.constraints.Min(1) int sourceVersion) {
authenticate(token);
return ApiResponse.success(service.container(containerId, feeId));
return ApiResponse.success(service.container(containerId, feeId, sourceVersion));
}
private void authenticate(String token) {
@@ -9,6 +9,7 @@ import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.OrderApprovedInboundCommand;
import com.baiyue.firstleg.dto.InboundOrderReference;
import com.baiyue.firstleg.service.InboundOrderCreationService;
import com.baiyue.firstleg.service.InboundCartonLabelService;
import com.baiyue.firstleg.service.UnifiedInboundQueryService;
import com.baiyue.firstleg.dto.UnifiedInboundDetail;
import com.baiyue.firstleg.dto.UnifiedInboundQuery;
@@ -35,6 +36,8 @@ public class InternalInboundOrderController {
@Resource
private InboundOrderCreationService service;
@Resource
private InboundCartonLabelService cartonLabels;
@Resource
private UnifiedInboundQueryService unifiedQueryService;
@Value("${baiyue.security.internal-service-token}")
private String internalToken;
@@ -59,7 +62,9 @@ public class InternalInboundOrderController {
|| command.totalPackageCount() != null && command.totalPackageCount() < 1) {
throw new BizException(ResultCode.BAD_REQUEST, "订单入库快照不完整");
}
return ApiResponse.success(service.createFromApprovedOrder(command));
long inboundId = service.createFromApprovedOrder(command);
cartonLabels.getOrCreate(command.tenantId(), inboundId);
return ApiResponse.success(inboundId);
}
@PostMapping("/order-preparing")
@@ -31,12 +31,12 @@ public class InternalPriceBookController {
}
@PostMapping("/channels/validate")
public ApiResponse<Void> validateChannel(
public ApiResponse<PriceBookChannelValidation.Response> validateChannel(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody PriceBookChannelValidation.Request request) {
requireToken(token);
service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(), request.channelCode());
return ApiResponse.success(null);
return ApiResponse.success(service.requireAvailableChannel(request.tenantId(), request.originWarehouseId(),
request.channelCode(), request.destinationType(), request.requireInboundCodePrefix()));
}
private void requireToken(String token) {
@@ -6,8 +6,10 @@ import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.ContainerDtos.FullUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.Query;
import com.baiyue.firstleg.dto.ContainerDtos.SaveRequest;
import com.baiyue.firstleg.dto.ContainerDtos.TransportUpdateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.SimulationCreateRequest;
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingLinkRequest;
import com.baiyue.firstleg.dto.ContainerDtos.OceanBookingUnlinkRequest;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.service.ContainerService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
@@ -55,6 +57,18 @@ public class PlatformContainerController {
public ApiResponse<Overview> overview(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id) {
FirstlegScopeGuard.platform(user); return ApiResponse.success(overview.overview(id));
}
@GetMapping("/{id}/pda/photos/{fileId}/download")
@PreAuthorize("hasAuthority('platform:firstleg:container:list')")
public ResponseEntity<byte[]> downloadPdaPhoto(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long id,
@PathVariable @Positive long fileId) {
FirstlegScopeGuard.platform(user);
var value = overview.downloadPhoto(id, fileId);
return ResponseEntity.ok().cacheControl(CacheControl.noStore())
.header(HttpHeaders.CONTENT_DISPOSITION, org.springframework.http.ContentDisposition.inline()
.filename(value.fileName(), StandardCharsets.UTF_8).build().toString())
.contentType(value.contentType()).contentLength(value.content().length).body(value.content());
}
@PostMapping("/{id}/exception") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
public ApiResponse<Void> markException(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,
@RequestHeader("Idempotency-Key") String key,
@@ -70,15 +84,21 @@ public class PlatformContainerController {
@GetMapping("/{id}/documents/{documentType}") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
public ApiResponse<BusinessDocument> logisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.containerLogisticsDocument(id,documentType));}
@PostMapping(value="/{id}/documents/{documentType}/upload",consumes=MediaType.MULTIPART_FORM_DATA_VALUE) @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestPart("expectedVersion") @Min(0) int version,@RequestPart(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
public ApiResponse<BusinessDocument> uploadLogisticsDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@RequestHeader("Idempotency-Key") String key,@RequestPart("file") MultipartFile file,@RequestParam("expectedVersion") @Min(0) int version,@RequestParam(value="operationReason",required=false) String reason){FirstlegScopeGuard.platform(user);return ApiResponse.success(loadingLists.uploadContainerLogisticsDocument(id,documentType,user.userId(),user.username(),key,version,reason,file));}
@GetMapping("/{id}/documents/{documentType}/{fileId}/download") @PreAuthorize("hasAuthority('platform:firstleg:container:list')")
public ResponseEntity<byte[]> downloadDocument(@AuthenticationPrincipal AuthenticatedUser user,@PathVariable @Positive long id,@PathVariable String documentType,@PathVariable @Positive long fileId){FirstlegScopeGuard.platform(user);var d=loadingLists.downloadContainerDocument(id,documentType,fileId);return ResponseEntity.ok().cacheControl(CacheControl.noStore()).header(HttpHeaders.CONTENT_DISPOSITION,org.springframework.http.ContentDisposition.attachment().filename(d.fileName(),StandardCharsets.UTF_8).build().toString()).contentType(d.contentType()).contentLength(d.content().length).body(d.content());}
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:container:create')")
@BusinessAudit(event="CONTAINER_CREATE", resourceType="CONTAINER", description="新增货柜基础资料", captureBefore=false)
public ApiResponse<ContainerBookingEntity> create(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody SaveRequest request) {
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.create(user.userId(), user.username(), request));
@Valid @RequestBody SimulationCreateRequest request) {
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.createSimulation(user.userId(), user.username(), request));
}
@PostMapping("/{id}/ocean-booking-link") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
@BusinessAudit(event="CONTAINER_OCEAN_BOOKING_LINK", resourceType="CONTAINER", resourceId="#id", description="关联真实订舱柜")
public ApiResponse<ContainerBookingEntity> linkOceanBooking(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody OceanBookingLinkRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.linkOceanBooking(id, user.userId(), user.username(), request)); }
@DeleteMapping("/{id}/ocean-booking-link") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
@BusinessAudit(event="CONTAINER_OCEAN_BOOKING_UNLINK", resourceType="CONTAINER", resourceId="#id", description="解除真实订舱柜关联")
public ApiResponse<ContainerBookingEntity> unlinkOceanBooking(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id, @Valid @RequestBody OceanBookingUnlinkRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.unlinkOceanBooking(id, user.userId(), user.username(), request)); }
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:container:edit')")
@BusinessAudit(event="CONTAINER_UPDATE", resourceType="CONTAINER", resourceId="#id", description="完整编辑货柜基础资料")
@Operation(summary="完整编辑货柜", description="仅待装柜或预装柜中时允许")
@@ -127,8 +147,8 @@ public class PlatformContainerController {
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long id,
@RequestHeader("Idempotency-Key") String key,
@RequestPart("file") MultipartFile file,
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
@RequestParam("expectedVersion") @Min(0) int expectedVersion,
@RequestParam(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(loadingLists.uploadContainerClearance(
id, user.userId(), user.username(), key, expectedVersion, operationReason, file));
@@ -76,7 +76,7 @@ public class PlatformContainerFeeDetailController {
@PathVariable long loadingPlanId, @PathVariable long feeId,
@Valid @RequestBody AdjustRequest request) {
return ApiResponse.success(service.adjust(FirstlegScopeGuard.platform(user, accountId),
loadingPlanId, feeId, request.expectedSourceVersion(), user.userId(), request.values()));
loadingPlanId, feeId, user.userId(), request));
}
@PostMapping("/{feeId}/void")
@@ -79,7 +79,7 @@ public class PlatformFbaPricingController {
@PutMapping("/published")
@PreAuthorize("hasAuthority('firstleg:fba-pricing:adjust')")
@BusinessAudit(event = "FBA_FEE_ADJUST", resourceType = "INBOUND", resourceId = "#inboundId",
description = "调整已发布入库费用,不回退入库状态")
description = "调整初算或已发布入库费用,不回退待分柜状态")
public ApiResponse<FeeSnapshotResponse> adjustPublished(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId,
@PathVariable long inboundId, @RequestHeader("Idempotency-Key") String idempotencyKey,
@@ -41,8 +41,10 @@ public class PlatformInboundFeeDetailController {
public ApiResponse<List<InboundFeeDetailEntity>> list(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long accountId,
@RequestParam(defaultValue = "false") boolean includeHistory,
@PathVariable long inboundId) {
return ApiResponse.success(service.list(FirstlegScopeGuard.platform(user, accountId), inboundId));
return ApiResponse.success(service.list(
FirstlegScopeGuard.platform(user, accountId), inboundId, includeHistory));
}
@PostMapping
@@ -33,6 +33,8 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.MediaType;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
/** 平台后台入库测量录入、提交和 OCR 重量结果维护接口。 */
@Tag(name = "平台入库测量", description = "维护测量草稿、箱规明细和可追溯 OCR 重量结果。")
@RestController
@@ -53,6 +55,19 @@ public class PlatformInboundMeasurementController {
return ApiResponse.success(service.detail(tenantId, inboundId));
}
@Operation(summary = "查询称重可选托盘快照",
description = "按入库单所属仓库返回未删除的托盘只读快照,不校验托盘状态或占用资格。")
@GetMapping("/weight-pallet-options")
@PreAuthorize("hasAnyAuthority('platform:firstleg:inbound:measure:view',"
+ " 'platform:firstleg:inbound:weight:edit')")
public ApiResponse<List<InboundMeasurementResponses.PalletSnapshot>> weightPalletOptions(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long inboundId) {
requirePlatformTenant(user, tenantId);
return ApiResponse.success(service.weightPalletOptions(tenantId, inboundId));
}
@BusinessAudit(event = "INBOUND_MEASUREMENT_DRAFT_SAVE", resourceType = "INBOUND",
resourceId = "#inboundId", description = "保存入库测量草稿")
@Operation(summary = "保存测量草稿", description = "保存人工测量批次和服务端计算明细,不推进入库状态。")
@@ -114,7 +129,7 @@ public class PlatformInboundMeasurementController {
@RequestPart("image") MultipartFile image) {
requirePlatformTenant(user, tenantId);
CartonMeasurementCommand command = new CartonMeasurementCommand(
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
request.boxCode(), request.length(), request.width(), request.height(),
request.machineNo(), request.measuredAt(), image, request.scannedShipmentId());
return ApiResponse.success(cartonMeasurements.upsertForInbound(
tenantId, user.userId(), inboundId, command));
@@ -134,7 +149,7 @@ public class PlatformInboundMeasurementController {
@RequestPart("image") MultipartFile image) {
requirePlatformTenant(user, tenantId);
CartonMeasurementCommand command = new CartonMeasurementCommand(
request.boxCode(), request.length(), request.width(), request.height(), request.weight(),
request.boxCode(), request.length(), request.width(), request.height(),
request.machineNo(), request.measuredAt(), image, request.scannedShipmentId());
return ApiResponse.success(cartonMeasurements.upsertManualForInbound(
tenantId, user.userId(), inboundId, command));
@@ -142,7 +157,8 @@ public class PlatformInboundMeasurementController {
@BusinessAudit(event = "INBOUND_WEIGHT_OCR_RECORD", resourceType = "INBOUND",
resourceId = "#inboundId", description = "登记称重图片 OCR 原值")
@Operation(summary = "登记 OCR 重量结果", description = "按图片文件 ID 保存不可覆盖的 OCR 原值。")
@Operation(summary = "登记 OCR 重量结果",
description = "按图片文件 ID 和托盘 ID 保存 OCR 毛重原值及同仓托盘快照。")
@PostMapping("/weight-results")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:edit')")
public ApiResponse<InboundMeasurementResponses.WeightResult> createWeightResult(
@@ -173,7 +189,8 @@ public class PlatformInboundMeasurementController {
@BusinessAudit(event = "INBOUND_WEIGHT_OCR_CONFIRM", resourceType = "INBOUND_WEIGHT_RESULT",
resourceId = "#resultId", description = "确认 OCR 重量并累加实际总毛重")
@Operation(summary = "确认 OCR 重量",
description = "可在同一事务中携带人工修改值并确认;统一换算为 KG 后重算所有已确认图片的总重量。")
description = "可在同一事务中携带人工毛重修改值并确认;统一换算 KG、扣除托盘皮重后,"
+ "按所有已确认结果分别重算入库单毛重与扣皮后的货物净重。")
@PostMapping("/weight-results/{resultId}/confirm")
@PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:confirm')"
+ " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))")
@@ -6,13 +6,12 @@ import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.firstleg.dto.ExceptionReleaseRequest;
import com.baiyue.firstleg.dto.MeasurementReviewRequest;
import com.baiyue.firstleg.dto.MeasurementApprovalResponse;
import com.baiyue.firstleg.entity.MeasureReviewRecordEntity;
import com.baiyue.firstleg.service.MeasurementReviewService;
import com.baiyue.firstleg.service.AutomaticInboundCompletionService;
import com.baiyue.firstleg.service.InitialInboundPricingService;
import com.baiyue.firstleg.service.FirstlegScopeGuard;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -36,15 +35,15 @@ import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/api/platform/v1/inbounds")
public class PlatformInboundMeasurementReviewController {
@Resource private MeasurementReviewService service;
@Resource private AutomaticInboundCompletionService automaticCompletion;
@Resource private InitialInboundPricingService initialPricing;
@BusinessAudit(event = "INBOUND_MEASURE_APPROVE", resourceType = "INBOUND",
resourceId = "#inboundId", description = "测量审核通过")
@Operation(summary = "测量审核通过",
description = "客服确认测量数据后直接进入待分柜;费用和其他资料可在后续继续维护。")
description = "客服确认测量数据后直接进入待分柜,并按已审核测量数据尝试生成初算费用;初算失败不阻塞状态流转。")
@PostMapping("/{inboundId}/measurements/approve")
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review')")
public ApiResponse<FeeSnapshotResponse> approve(
public ApiResponse<MeasurementApprovalResponse> approve(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long inboundId,
@@ -52,47 +51,9 @@ public class PlatformInboundMeasurementReviewController {
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody MeasurementReviewRequest request) {
validateScope(user, tenantId);
// 审核服务事务在此调用返回时已提交;随后自动编排从新事务/新查询读取 WAIT_PRICING,
// 避免在未提交的审核事务中读取旧状态。自动流程失败时保留已审核状态,原样返回业务错误,
// 使用同一幂等键重试会从持久化阶段继续推进。
service.approve(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
return ApiResponse.success(automaticCompletion.completeApproved(
tenantId, inboundId, user.userId(), user.username(), request.measurementId(), request.expectedVersion() + 1));
}
@BusinessAudit(event = "INBOUND_MEASURE_REJECT", resourceType = "INBOUND",
resourceId = "#inboundId", description = "测量审核驳回")
@Operation(summary = "测量审核驳回",
description = "驳回原因必填,只退回测量数据修改;已确认的托盘和库位占用保持不动。")
@PostMapping("/{inboundId}/measurements/reject")
@PreAuthorize("hasAuthority('firstleg:inbound:measurement-review')")
public ApiResponse<Void> reject(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long inboundId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody MeasurementReviewRequest request) {
validateScope(user, tenantId);
service.reject(tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
return ApiResponse.success(null);
}
@BusinessAudit(event = "INBOUND_MEASURE_EXCEPTION_RELEASE", resourceType = "INBOUND",
resourceId = "#inboundId", description = "测量驳回异常放行")
@Operation(summary = "测量驳回异常放行",
description = "Nacos 开关开启、有异常放行权限且原因非空时,才可从 MEASURE_REJECTED 推进至 WAIT_PRICING。")
@PostMapping("/{inboundId}/measurements/exception-release")
@PreAuthorize("hasAuthority('firstleg:inbound:exception-release')")
public ApiResponse<Void> exceptionRelease(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long tenantId,
@PathVariable long inboundId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody ExceptionReleaseRequest request) {
validateScope(user, tenantId);
service.exceptionRelease(
tenantId, user.userId(), user.username(), inboundId, idempotencyKey, request);
return ApiResponse.success(null);
return ApiResponse.success(initialPricing.tryCalculate(tenantId, user.userId(), inboundId,
request.measurementId(), request.expectedVersion() + 1));
}
@Operation(summary = "查询测量审核历史")
@@ -9,6 +9,8 @@ import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadDetail;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadQuery;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadSummary;
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmPreloadRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.PreloadConfirmationResponse;
import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
import com.baiyue.firstleg.dto.LoadingListDtos.BusinessDocument;
import com.baiyue.firstleg.dto.LoadingListDtos.DocumentAttachRequest;
@@ -66,7 +68,7 @@ public class PlatformLoadingListController {
return ApiResponse.success(service.preloadDetail(planId, warehouseId));
}
@Operation(summary = "修改装柜运输字段", description = "仅装柜中可修改集装箱号与航次。")
@Operation(summary = "修改装柜运输字段", description = "已推送且仍为 EDITING 的预装清单可补录或修改集装箱号与航次;LOADING 阶段按稳定柜次身份规则维护,不能清空已创建 PDA 任务的柜号。")
@PatchMapping("/loading-plans/{planId}/transport")
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
public ApiResponse<PreloadDetail> updateTransport(
@@ -80,6 +82,19 @@ public class PlatformLoadingListController {
planId, warehouseId, user.userId(), user.username(), idempotencyKey, request));
}
@Operation(summary = "确认整份预装计划", description = "按 loadingPlanId 原子确认全部关联入库预装票据;要求已推送、真实集装箱号已录入、关联订舱已放舱。成功后唯一创建待领取 PDA 任务,claim 后才进入 LOADING。")
@PostMapping("/loading-plans/{planId}/confirm")
@PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
public ApiResponse<PreloadConfirmationResponse> confirmPreload(
@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long planId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@Valid @RequestBody ConfirmPreloadRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.confirmPreload(
planId, user.userId(), user.username(), idempotencyKey, request));
}
@Operation(summary = "分页查询完成装柜清单", description = "只读取 COMPLETED 状态的装柜计划。")
@GetMapping("/loading-lists")
@PreAuthorize("hasAuthority('platform:firstleg:loading-list:list')")
@@ -129,8 +144,8 @@ public class PlatformLoadingListController {
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long planId,
@RequestParam @Positive long warehouseId, @RequestHeader("Idempotency-Key") String key,
@RequestPart("file") MultipartFile file,
@RequestPart("expectedVersion") @Min(0) int expectedVersion,
@RequestPart(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
@RequestParam("expectedVersion") @Min(0) int expectedVersion,
@RequestParam(value = "operationReason", required = false) @Size(max = 500) String operationReason) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.uploadLoadingDeclaration(planId, warehouseId,
user.userId(), user.username(), key, expectedVersion, operationReason, file));
@@ -45,12 +45,11 @@ public class PlatformLoadingPdaController {
@GetMapping("/tasks")
@PreAuthorize("hasAuthority('platform:firstleg:pda:view')")
public ApiResponse<Task> task(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String containerNo,
@RequestParam(required = false) String cabinetNo,
@RequestParam(required = false) Long loadingPlanId,
@RequestParam @Positive long warehouseId) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.task(containerNo, cabinetNo, loadingPlanId, warehouseId));
return ApiResponse.success(service.task(cabinetNo, loadingPlanId, warehouseId));
}
@GetMapping("/tasks/pending")
@@ -22,6 +22,8 @@ public class PlatformSmartLoadingController {
public ApiResponse<Plan> recommend(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@Valid @RequestBody RecommendRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.recommend(planId,r,u.userId(),u.username()));}
@PatchMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:assign')")
public ApiResponse<Plan> adjust(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody AdjustRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.adjust(planId,itemId,r,u.userId(),u.username()));}
@PatchMapping("/plans/{planId}/items/{itemId}/warehouse-remark") @PreAuthorize("hasAuthority('platform:firstleg:preload:edit')")
public ApiResponse<Plan> warehouseRemark(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody WarehouseRemarkRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.updateWarehouseRemark(planId,itemId,r,u.userId(),u.username()));}
@DeleteMapping("/plans/{planId}/items/{itemId}") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:delete')")
public ApiResponse<Plan> delete(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long planId,@PathVariable @Positive long itemId,@Valid @RequestBody DeleteRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.delete(planId,itemId,r,u.userId(),u.username()));}
@PostMapping("/plans/{planId}/push-preload") @PreAuthorize("hasAuthority('platform:firstleg:smart-loading:push')")
@@ -1,12 +1,12 @@
package com.baiyue.firstleg.controller;
import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.*; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import com.baiyue.firstleg.service.*; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.Positive; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime;
import com.baiyue.common.audit.BusinessAudit; import com.baiyue.common.core.api.*; import com.baiyue.common.security.AuthenticatedUser; import com.baiyue.firstleg.dto.TruckAppointmentDtos.*; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import com.baiyue.firstleg.service.*; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Positive; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.time.LocalDateTime;
@Tag(name="拖车预约") @Validated @RestController @RequestMapping("/api/platform/v1/truck-appointments") public class PlatformTruckAppointmentController { @Resource TruckAppointmentService service;
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse<PageResult<TruckAppointmentEntity>> page(@AuthenticationPrincipal AuthenticatedUser u,@Valid Query q){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.page(q));}
@GetMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse<TruckAppointmentEntity> detail(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.detail(id));}
@GetMapping("/{id}/history") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse<java.util.List<com.baiyue.firstleg.entity.TruckAppointmentHistoryEntity>> history(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.history(id));}
@GetMapping("/{id}/attachments") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:list')") public ApiResponse<java.util.List<com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity>> attachments(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.attachments(id));}
@PostMapping("/{id}/attachments") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity> attach(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@Valid @RequestBody AttachmentRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.attach(id,r,u.userId()));}
@PostMapping(value="/{id}/attachments/upload",consumes="multipart/form-data") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity> upload(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@RequestParam(required=false) String attachmentType,@RequestPart("file") org.springframework.web.multipart.MultipartFile file){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.upload(id,attachmentType,file,u.userId()));}
@PostMapping(value="/{id}/attachments/upload",consumes="multipart/form-data") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<com.baiyue.firstleg.entity.TruckAppointmentAttachmentEntity> upload(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@RequestParam @NotBlank @Pattern(regexp="BILL_OF_LADING|DISPATCH_ORDER|DRIVER_DOCUMENT|VEHICLE_DOCUMENT|OTHER") String attachmentType,@RequestPart("file") org.springframework.web.multipart.MultipartFile file){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.upload(id,attachmentType,file,u.userId()));}
@DeleteMapping("/{id}/attachments/{attachmentId}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<Void> deleteAttachment(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable @Positive long id,@PathVariable @Positive long attachmentId){FirstlegScopeGuard.platform(u);service.deleteAttachment(id,attachmentId);return ApiResponse.success(null);}
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:create')") @BusinessAudit(event="TRUCK_APPOINTMENT_CREATE",resourceType="TRUCK_APPOINTMENT",captureBefore=false) public ApiResponse<TruckAppointmentEntity> create(@AuthenticationPrincipal AuthenticatedUser u,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.create(u.userId(),u.username(),r));}
@PutMapping("/{id}") @PreAuthorize("hasAuthority('platform:firstleg:truck-appointment:edit')") public ApiResponse<TruckAppointmentEntity> edit(@AuthenticationPrincipal AuthenticatedUser u,@PathVariable long id,@Valid @RequestBody SaveRequest r){FirstlegScopeGuard.platform(u);return ApiResponse.success(service.edit(id,u.userId(),u.username(),r));}
@@ -29,7 +29,7 @@ public interface CustomerTransitStatisticsMapper {
JOIN fl_ocean_booking b ON b.id = c.ocean_booking_id
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND b.status <> 'CANCELLED'
AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan p
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id
@@ -43,7 +43,7 @@ public interface CustomerTransitStatisticsMapper {
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
WHERE c.actual_sailing_at IS NOT NULL
AND c.actual_warehouse_arrival_at IS NULL
AND b.status NOT IN ('CANCELLED', 'FAILED')
AND b.status <> 'CANCELLED'
AND i.account_id IS NOT NULL AND i.account_id > 0
) x
WHERE x.customer_id = #{customerId}
@@ -51,7 +51,8 @@ public final class CustomsDtos {
LocalDateTime supplementDueAt,
@Min(0) int expectedVersion) {}
public record SubmitBatchRequest(@NotBlank @Size(max = 128) String customsAgentName,
public record SubmitBatchRequest(@NotNull @jakarta.validation.constraints.Positive Long customsAgentId,
@Size(max = 128) String customsAgentName,
@Size(max = 128) String externalReference,
@Size(max = 500) String note,
@Min(0) int expectedVersion) {}
@@ -11,6 +11,7 @@ public class CustomsSubmissionBatchEntity {
private Long customsCaseId;
private String batchNo;
private Integer batchVersion;
private Long customsAgentId;
private String customsAgentName;
private String externalReference;
private String fileSnapshotJson;
@@ -8,6 +8,7 @@ import com.baiyue.firstleg.customs.client.CustomsFileClient;
import com.baiyue.firstleg.customs.dto.CustomsDtos.*;
import com.baiyue.firstleg.customs.entity.*;
import com.baiyue.firstleg.customs.repository.CustomsRepository;
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -36,6 +37,7 @@ public class CustomsService {
@Resource private CustomsRepository repository;
@Resource private CustomsFileClient files;
@Resource private ObjectMapper objectMapper;
@Resource private WarehouseSupplierSnapshotService suppliers;
@Value("${baiyue.security.internal-service-token}") private String internalToken;
public PageResult<CustomsCaseEntity> page(Query query) { return repository.page(query); }
@@ -177,7 +179,10 @@ public class CustomsService {
}
CustomsSubmissionBatchEntity batch = new CustomsSubmissionBatchEntity();
batch.setId(IdWorker.getId()); batch.setCustomsCaseId(caseId); batch.setBatchVersion(repository.batches(caseId).size() + 1);
batch.setBatchNo("CB" + batch.getId()); batch.setCustomsAgentName(request.customsAgentName().trim());
batch.setBatchNo("CB" + batch.getId());
var customsAgent = suppliers.customs(request.customsAgentId(), request.customsAgentName());
batch.setCustomsAgentId(customsAgent == null ? null : customsAgent.supplierId());
batch.setCustomsAgentName(customsAgent == null ? null : customsAgent.name());
batch.setExternalReference(trim(request.externalReference())); batch.setFileSnapshotJson(json(current.stream()
.map(v -> Map.of("documentId", v.getId(), "fileId", v.getFileId(), "itemCode", v.getChecklistItemCode(),
"documentVersion", v.getDocumentVersion(), "fileHash", v.getFileHash())).toList()));
@@ -0,0 +1,43 @@
package com.baiyue.firstleg.domain;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import java.math.BigDecimal;
import java.util.Locale;
import java.util.Map;
/** Platform-controlled container specifications shared by booking and simulation flows. */
public final class ContainerSpecificationCatalog {
private static final Map<String, Specification> VALUES = Map.of(
"20GP", new Specification("20GP", new BigDecimal("33"), new BigDecimal("28000")),
"40GP", new Specification("40GP", new BigDecimal("67"), new BigDecimal("28000")),
"40HC", new Specification("40HC", new BigDecimal("76"), new BigDecimal("28000")),
"40NR", new Specification("40NR", new BigDecimal("67"), new BigDecimal("28000")),
"45HC", new Specification("45HC", new BigDecimal("86"), new BigDecimal("28000"))
);
private ContainerSpecificationCatalog() {}
public static Specification require(String containerType) {
String normalized = containerType == null ? "" : containerType.trim().toUpperCase(Locale.ROOT);
Specification specification = VALUES.get(normalized);
if (specification == null) {
throw new BizException(ResultCode.VALIDATION_FAILED, "柜型必须使用平台固定规格");
}
return specification;
}
public static Specification requireMatch(String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg) {
Specification specification = require(containerType);
if (capacityCbm == null || maxWeightKg == null
|| specification.capacityCbm().compareTo(capacityCbm) != 0
|| specification.maxWeightKg().compareTo(maxWeightKg) != 0) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"柜型容量和限重必须使用平台固定规格");
}
return specification;
}
public record Specification(String containerType, BigDecimal capacityCbm, BigDecimal maxWeightKg) {}
}
@@ -8,29 +8,33 @@ public final class PriceBookChannelCatalog {
private PriceBookChannelCatalog() {}
public static final List<ChannelDefinition> CHANNELS = List.of(
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡"),
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡"),
fba("EXX_TRUCK", "EXX海卡", "EXX海卡"),
fba("HEDE_TRUCK", "合德海卡", "合德海卡"),
fba("EXX_RAIL_NY", "纽约EXX海铁", "纽约EXX海铁"),
fba("OA_LA_TRUCK", "洛杉矶普船海卡", "洛杉矶普船海卡"),
fba("OA_LA_FULL_TRUCK", "洛杉矶普船全卡", "洛杉矶普船全卡"),
fba("OA_CHICAGO_RAIL", "芝加哥普船海铁", "芝加哥普船海铁"),
fba("OA_HOUSTON_TRUCK", "休斯顿普船海卡", "休斯顿普船海卡"),
fba("OA_NEW_YORK_TRUCK", "纽约普船海卡", "纽约普船海卡"),
fba("OA_SAVANNAH_TRUCK", "萨凡纳普船海卡", "萨凡纳普船海卡"),
parcel("MATSON_CLX_PARCEL", "美森正班海派", "海派"),
parcel("MATSON_MAX_PARCEL", "美森加班海派", "海派"),
parcel("HEDE_PARCEL", "合德海派", "海派"),
parcel("OA_PARCEL", "普船海派", "海派")
fba("MATSON_CLX_TRUCK", "美森正班海卡", "美森正班海卡", "ZBYHK", "ZBYHWK"),
fba("MATSON_MAX_TRUCK", "美森加班海卡", "美森加班海卡", "BYHK", "BYHWK"),
fba("EXX_TRUCK", "EXX海卡", "EXX海卡", "EXXHK", "EXXHWK"),
fba("HEDE_TRUCK", "合德海卡", "合德海卡", "HDHK", "HDHWK"),
fba("EXX_RAIL_NY", "纽约EXX海铁", "纽约EXX海铁", "NYEXX", null),
fba("OA_LA_TRUCK", "洛杉矶普船海卡", "洛杉矶普船海卡", "PCHK", "PCHWK"),
fba("OA_LA_FULL_TRUCK", "洛杉矶普船全卡", "洛杉矶普船全卡", "PCKK", null),
fba("OA_CHICAGO_RAIL", "芝加哥普船海铁", "芝加哥普船海铁", "CPHT", null),
fba("OA_HOUSTON_TRUCK", "休斯顿普船海卡", "休斯顿普船海卡", "HPHK", null),
fba("OA_NEW_YORK_TRUCK", "纽约普船海卡", "纽约普船海卡", "NPHK", "NPHWK"),
fba("OA_SAVANNAH_TRUCK", "萨凡纳普船海卡", "萨凡纳普船海卡", "SPHK", null),
parcel("MATSON_CLX_PARCEL", "美森正班海派", "海派", "ZBYHP", null),
parcel("MATSON_MAX_PARCEL", "美森加班海派", "海派", "BYHP", null),
parcel("HEDE_PARCEL", "合德海派", "海派", "HDHP", null),
parcel("OA_PARCEL", "普船海派", "海派", "PCHP", null)
);
private static ChannelDefinition fba(String code, String name, String sheet) {
return new ChannelDefinition(code, name, PriceModel.FBA_WAREHOUSE_RATE, sheet, Set.of(name));
private static ChannelDefinition fba(String code, String name, String sheet,
String inboundCodePrefix, String overseasInboundCodePrefix) {
return new ChannelDefinition(code, name, PriceModel.FBA_WAREHOUSE_RATE, sheet, Set.of(name),
inboundCodePrefix, overseasInboundCodePrefix);
}
private static ChannelDefinition parcel(String code, String name, String sheet) {
return new ChannelDefinition(code, name, PriceModel.REGION_WEIGHT_TIER, sheet, Set.of(name));
private static ChannelDefinition parcel(String code, String name, String sheet,
String inboundCodePrefix, String overseasInboundCodePrefix) {
return new ChannelDefinition(code, name, PriceModel.REGION_WEIGHT_TIER, sheet, Set.of(name),
inboundCodePrefix, overseasInboundCodePrefix);
}
public record ChannelDefinition(
@@ -38,6 +42,8 @@ public final class PriceBookChannelCatalog {
String displayName,
PriceModel priceModel,
String sourceSheetName,
Set<String> aliases
Set<String> aliases,
String inboundCodePrefix,
String overseasInboundCodePrefix
) {}
}
@@ -11,7 +11,6 @@ public record CartonMeasurementCommand(
BigDecimal lengthCm,
BigDecimal widthCm,
BigDecimal heightCm,
BigDecimal unitWeightKg,
String machineNo,
LocalDateTime measuredAt,
MultipartFile image,
@@ -14,7 +14,6 @@ public record CartonMeasurementResult(
BigDecimal lengthCm,
BigDecimal widthCm,
BigDecimal heightCm,
BigDecimal unitWeightKg,
Long imageFileId,
String machineNo,
LocalDateTime measuredAt,
@@ -25,6 +25,7 @@ public final class ContainerDtos {
@Size(max = 64) String cabinetNo,
@Size(max = 128) String billOfLadingNo,
@Size(max = 128) String keyword,
@Positive Long customerId,
@Positive Long warehouseId,
@Size(max = 32) String containerType,
@Positive Long shippingCompanyId,
@@ -46,7 +47,6 @@ public final class ContainerDtos {
@Schema(name = "ContainerSaveRequest", description = "货柜基础资料;智能装柜草稿阶段可完整编辑")
public record SaveRequest(
@Size(max = 64) String containerNo,
@NotBlank @Size(max = 64) String cabinetNo,
@NotNull @Positive Long warehouseId,
@Size(max = 128) String warehouseName,
@Size(max = 128) String billOfLadingNo,
@@ -63,6 +63,7 @@ public final class ContainerDtos {
@Size(max = 128) String destinationWarehouseName,
@DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
@Positive Long terminalTruckingCompanyId,
@Size(max = 128) String terminalTruckingCompany,
LocalDateTime loadingAt,
LocalDateTime estimatedSailingAt,
@@ -91,4 +92,19 @@ public final class ContainerDtos {
this(containerNo, voyageNo, version, "运输信息更新");
}
}
@Schema(name = "ContainerSimulationCreateRequest", description = "新增内部模拟柜;内部柜号由服务端生成")
public record SimulationCreateRequest(@NotBlank @Size(max = 64) String channelCode,
@NotNull @Positive Long warehouseId, @NotBlank @Size(max = 32) String containerType,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {}
@Schema(name = "ContainerOceanBookingLinkRequest", description = "将现有模拟柜原地关联到已发布订舱箱型需求并补录真实柜号")
public record OceanBookingLinkRequest(@NotNull @Positive Long oceanBookingId,
@NotNull @Positive Long requirementId, @NotBlank @Size(max = 64) String containerNo,
@NotNull @PositiveOrZero Integer expectedVersion) {}
@Schema(name = "ContainerOceanBookingUnlinkRequest", description = "解除空模拟柜的真实订舱关联")
public record OceanBookingUnlinkRequest(@NotNull @PositiveOrZero Integer expectedVersion,
@NotBlank @Size(max = 500) String reason) {}
}
@@ -20,29 +20,16 @@ public final class ContainerFeeDetailDtos {
@NotNull LocalDate businessDate,
@NotNull @DecimalMin("0.00") BigDecimal amount,
@NotBlank @Pattern(regexp = "CNY") String currency,
@NotNull Boolean settlementIncluded,
Long partnerId,
@Size(max = 200) String partnerName,
Long supplierId,
@Size(max = 500) String remark
) {
}
public record AdjustRequest(
@NotNull @Min(1) Integer expectedSourceVersion,
@NotNull @Min(1) Long feeItemId,
@NotBlank @Pattern(regexp = "RECEIVABLE|PAYABLE") String direction,
@NotNull LocalDate businessDate,
@NotNull @DecimalMin("0.00") BigDecimal amount,
@NotBlank @Pattern(regexp = "CNY") String currency,
@NotNull Boolean settlementIncluded,
Long partnerId,
@Size(max = 200) String partnerName,
@Size(max = 500) String remark
@NotNull @DecimalMin("0.00") BigDecimal amount
) {
public SaveRequest values() {
return new SaveRequest(feeItemId, direction, businessDate, amount, currency,
settlementIncluded, partnerId, partnerName, remark);
}
}
public record VersionRequest(@NotNull @Min(1) Integer expectedSourceVersion) {
@@ -16,7 +16,7 @@ public final class ContainerOverviewDtos {
String status, Long warehouseId, String warehouseName, String voyageNo, String vesselName,
Integer version) {}
public record PdaSummary(Long taskId, Long loadingPlanId, Long containerBookingId, String taskStatus,
Integer taskVersion, Integer itemCount, Integer completedItemCount, Integer unresolvedItemCount,
String workflowStatus, Integer taskVersion, Integer itemCount, Integer completedItemCount, Integer unresolvedItemCount,
List<PhotoCount> photoCounts, List<Photo> photos, List<String> exceptionReasons) {}
public record PhotoCount(String evidenceType, Integer count) {}
public record Photo(Long evidenceId, Long tenantId, Long inboundId, String evidenceType,
@@ -0,0 +1,11 @@
package com.baiyue.firstleg.dto;
import java.time.LocalDateTime;
/** 当前业务单元所关联柜次的正式运输事实。 */
public record CurrentContainerTransport(
Long inboundId, Long accountId, Long containerBookingId,
String containerNo, String cabinetNo, String vesselName, String voyageNo,
LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt,
LocalDateTime estimatedPortArrivalAt, LocalDateTime actualPortArrivalAt
) {}
@@ -6,6 +6,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
@@ -52,7 +53,9 @@ public final class FbaPricingDtos {
public record CalculateRequest(LocalDateTime pricingAt) {}
public record AdjustFeeRequest(
@NotNull @DecimalMin("0.00") BigDecimal subtotalAmount,
@NotNull @Positive Integer expectedFeeVersion,
@NotNull @DecimalMin(value = "0", inclusive = false) BigDecimal chargeableQuantity,
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
@NotNull @DecimalMin("0.00") BigDecimal additionalAmount,
@NotBlank @Size(max = 500) String reason
) {}
@@ -14,6 +14,7 @@ public final class FeeItemDtos {
@NotBlank @Size(max = 64) String category,
@NotBlank @Pattern(regexp = "DOMESTIC|INTERNATIONAL") String tradeScope,
@Size(max = 50) @Pattern(regexp = "[A-Za-z0-9_-]{1,50}") String feeCode,
@Pattern(regexp = "BOOKING|CUSTOMS|TRUCKING|CERTIFICATE") String supplierCategory,
@NotBlank @Size(max = 128) String feeTypeName,
@NotNull Boolean pickupPayable,
@NotNull Boolean pickupReceivable,
@@ -22,12 +23,22 @@ public final class FeeItemDtos {
@NotNull Boolean allocationFee,
@Size(max = 500) String description
) {
public SaveRequest(String category, String tradeScope, String feeCode,
String feeTypeName, Boolean pickupPayable,
Boolean pickupReceivable, Boolean warehousePayable,
Boolean warehouseReceivable, Boolean allocationFee,
String description) {
this(category, tradeScope, feeCode, null, feeTypeName, pickupPayable,
pickupReceivable, warehousePayable, warehouseReceivable,
allocationFee, description);
}
/** Backward-compatible constructor for callers predating the stable fee code. */
public SaveRequest(String category, String tradeScope, String feeTypeName,
Boolean pickupPayable, Boolean pickupReceivable,
Boolean warehousePayable, Boolean warehouseReceivable,
Boolean allocationFee, String description) {
this(category, tradeScope, null, feeTypeName, pickupPayable, pickupReceivable,
this(category, tradeScope, null, null, feeTypeName, pickupPayable, pickupReceivable,
warehousePayable, warehouseReceivable, allocationFee, description);
}
}
@@ -18,8 +18,9 @@ public record FinanceFeeSourceSnapshot(
String containerNo,
long customerId,
String customerName,
Long partnerId,
String partnerName,
Long supplierId,
String supplierName,
String supplierCategory,
Long feeItemId,
String feeItemCode,
String feeItemName,
@@ -29,7 +30,6 @@ public record FinanceFeeSourceSnapshot(
BigDecimal totalAmount,
String reviewStatus,
String publicationStatus,
boolean settlementIncluded,
LocalDateTime publishedAt,
BigDecimal chargeableQuantity,
String chargeUnit,
@@ -21,15 +21,13 @@ public final class InboundFeeDetailDtos {
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
@NotBlank @Pattern(regexp = "[A-Z]{3}") String currency,
@NotNull @DecimalMin("0.00") BigDecimal amount,
@NotNull Boolean settlementIncluded,
Long partnerId,
@Size(max = 200) String partnerName,
Long supplierId,
@Size(max = 500) String remark
) {
public SaveRequest(Long feeItemId, String direction, LocalDate businessDate,
BigDecimal unitPrice, String currency, BigDecimal amount,
String remark) {
this(feeItemId, direction, businessDate, unitPrice, currency, amount, false, null, null, remark);
this(feeItemId, direction, businessDate, unitPrice, currency, amount, null, remark);
}
}
@@ -1,5 +1,6 @@
package com.baiyue.firstleg.dto;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.DecimalMax;
@@ -26,12 +27,13 @@ public final class InboundMeasurementRequests {
public record SaveDraftRequest(
Long measurementId,
@NotNull @PositiveOrZero Integer version,
@Size(max = 64) String originalDocumentNo,
@DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal originalGrossWeightKg,
@DecimalMin("0.0000") @Digits(integer = 14, fraction = 4) BigDecimal originalVolumeCbm,
@Size(max = 255) String actualGoodsName,
@NotEmpty @Size(max = 500) List<@Valid ItemRequest> items
) {
@JsonAnySetter
public void rejectUnknownField(String fieldName, Object ignored) {
throw new IllegalArgumentException("测量草稿不接受字段: " + fieldName);
}
}
public record ItemRequest(
@@ -42,7 +44,6 @@ public final class InboundMeasurementRequests {
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal lengthCm,
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal widthCm,
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal heightCm,
@NotNull @DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal unitWeightKg,
@Positive Long intelligentImageFileId,
@Size(max = 500) String overLimitWarning,
@DecimalMin("0.000") @Digits(integer = 9, fraction = 3) BigDecimal outerLengthCm,
@@ -70,7 +71,6 @@ public final class InboundMeasurementRequests {
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal length,
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal width,
@NotNull @DecimalMin("0.001") @Digits(integer = 9, fraction = 3) BigDecimal height,
@DecimalMin("0.001") @Digits(integer = 15, fraction = 3) BigDecimal weight,
@Size(max = 64) String scannedShipmentId,
@NotBlank @Size(max = 64) String machineNo,
@NotNull LocalDateTime measuredAt
@@ -79,6 +79,7 @@ public final class InboundMeasurementRequests {
public record CreateWeightResultRequest(
@NotNull @Positive Long fileId,
@NotNull @Positive Long palletId,
@Size(max = 64) String ocrProvider,
@Size(max = 1000) String ocrRawText,
@DecimalMin("0.000") @Digits(integer = 15, fraction = 3) BigDecimal ocrWeightValue,
@@ -40,6 +40,7 @@ public final class InboundMeasurementResponses {
Integer originalPackageCount,
Integer actualPackageCount,
BigDecimal actualGrossWeightKg,
BigDecimal actualNetWeightKg,
BigDecimal actualVolumeCbm,
BigDecimal volumeWeightKg,
String actualGoodsName,
@@ -58,9 +59,7 @@ public final class InboundMeasurementResponses {
BigDecimal widthCm,
BigDecimal heightCm,
BigDecimal girthCm,
BigDecimal unitWeightKg,
BigDecimal unitVolumeCbm,
BigDecimal totalWeightKg,
BigDecimal totalVolumeCbm,
BigDecimal volumeWeightKg,
Long intelligentImageFileId,
@@ -81,14 +80,20 @@ public final class InboundMeasurementResponses {
public record WeightResult(
Long id,
Long fileId,
Long palletId,
String palletCode,
String palletType,
BigDecimal palletCapacityCbm,
BigDecimal palletTareWeightKg,
String ocrProvider,
String ocrRawText,
BigDecimal ocrWeightValue,
String ocrWeightUnit,
BigDecimal confidence,
BigDecimal grossWeightKg,
BigDecimal adjustedWeightValue,
String adjustedWeightUnit,
BigDecimal finalWeightKg,
BigDecimal netWeightKg,
String recognitionStatus,
String adjustmentReason,
Long adjustedBy,
@@ -99,6 +104,15 @@ public final class InboundMeasurementResponses {
) {
}
public record PalletSnapshot(
Long palletId,
String palletCode,
String palletType,
BigDecimal capacityCbm,
BigDecimal tareWeightKg
) {
}
public record DraftSaved(Long measurementId, Integer version, String status) {
}
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.dto;
import com.baiyue.firstleg.entity.InboundOrderEntity;
import com.baiyue.firstleg.entity.InboundTransportSnapshotEntity;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@@ -12,7 +13,7 @@ public record InboundOrderSummary(
String processStatus, String orderStatusSnapshot, String measureReviewStatus,
String feeStatus, String feeReviewStatus, String notificationStatus,
Integer version,
LocalDateTime expectedArrivalAt,
LocalDateTime expectedArrivalAt, LocalDateTime requestedPickupAt,
Long customerId, String customerName, Long companyId, String companyName,
Long warehouseId, String warehouseName,
Long deliveryWarehouseId, String deliveryWarehouseName,
@@ -26,18 +27,33 @@ public record InboundOrderSummary(
String locationCodes,
LocalDateTime measureCompletedAt, LocalDateTime inboundedAt,
String expressCompanyCode, String expressCompanyName, String expressTrackingNo,
String loadingStatus, String containerNo, String cabinetNo,
String vesselName, String voyageNo, LocalDateTime sailingAt, LocalDateTime portArrivalAt,
String containerNo, String cabinetNo,
String vesselName, String voyageNo,
LocalDateTime estimatedSailingAt, LocalDateTime actualSailingAt,
LocalDateTime estimatedPortArrivalAt, LocalDateTime actualPortArrivalAt,
LocalDateTime createdAt, LocalDateTime updatedAt
) {
/** 非统一入仓接口保留既有快照适配;统一订单不得调用此重载。 */
public static InboundOrderSummary from(
InboundOrderEntity order, InboundTransportSnapshotEntity transport, String locationCodes) {
InboundOrderEntity order, InboundTransportSnapshotEntity transport,
InboundPickupSnapshotEntity pickup, String locationCodes) {
CurrentContainerTransport adapted = transport == null ? null : new CurrentContainerTransport(
order.getId(), order.getTenantId(), null, transport.getContainerNo(), transport.getCabinetNo(),
transport.getVesselName(), transport.getVoyageNo(), transport.getSailingAt(), null,
transport.getPortArrivalAt(), null);
return from(order, adapted, pickup, locationCodes);
}
public static InboundOrderSummary from(
InboundOrderEntity order, CurrentContainerTransport transport,
InboundPickupSnapshotEntity pickup, String locationCodes) {
return new InboundOrderSummary(
order.getId(), order.getTenantId(), order.getOrderId(), order.getOrderNo(), order.getInboundCode(),
order.getProcessStatus(), order.getOrderStatusSnapshot(), order.getMeasureReviewStatus(),
order.getFeeStatus(), order.getFeeReviewStatus(), order.getNotificationStatus(),
order.getVersion(),
order.getExpectedArrivalAt(), order.getCustomerId(), order.getCustomerName(),
order.getExpectedArrivalAt(), pickup == null ? null : pickup.getPickupAt(),
order.getCustomerId(), order.getCustomerName(),
order.getCompanyId(), order.getCompanyName(), order.getWarehouseId(), order.getWarehouseName(),
order.getDeliveryWarehouseId(), order.getDeliveryWarehouseName(), order.getBusinessType(),
order.getChannelCode(), order.getChannelName(), order.getTaxMode(), order.getPricingTaxMode(),
@@ -50,13 +66,14 @@ public record InboundOrderSummary(
locationCodes,
order.getMeasureCompletedAt(), order.getInboundedAt(), order.getExpressCompanyCode(),
order.getExpressCompanyName(), order.getExpressTrackingNo(),
transport == null ? null : transport.getLoadingStatus(),
transport == null ? null : transport.getContainerNo(),
transport == null ? null : transport.getCabinetNo(),
transport == null ? null : transport.getVesselName(),
transport == null ? null : transport.getVoyageNo(),
transport == null ? null : transport.getSailingAt(),
transport == null ? null : transport.getPortArrivalAt(),
transport == null ? null : transport.containerNo(),
transport == null ? null : transport.cabinetNo(),
transport == null ? null : transport.vesselName(),
transport == null ? null : transport.voyageNo(),
transport == null ? null : transport.estimatedSailingAt(),
transport == null ? null : transport.actualSailingAt(),
transport == null ? null : transport.estimatedPortArrivalAt(),
transport == null ? null : transport.actualPortArrivalAt(),
order.getCreatedAt(), order.getUpdatedAt()
);
}
@@ -18,6 +18,7 @@ public final class LoadingListDtos {
public record PreloadQuery(
@Size(max = 128) String keyword,
String status,
@Positive Long customerId,
@Min(1) Long warehouseId,
LocalDateTime createdFrom,
LocalDateTime createdTo,
@@ -32,6 +33,7 @@ public final class LoadingListDtos {
public record ConfirmedQuery(
@Size(max = 128) String keyword,
@Positive Long customerId,
@Min(1) Long warehouseId,
LocalDateTime createdFrom,
LocalDateTime createdTo,
@@ -81,12 +83,43 @@ public final class LoadingListDtos {
public record Customer(Long accountId, String customerName) {
}
public record PreloadReadiness(
boolean preloadPushed,
boolean containerNoReady,
int preloadLinkCount,
int confirmedPreloadCount,
Long oceanBookingId,
String oceanBookingStatus,
Long pdaTaskId,
String pdaTaskStatus,
String confirmationStatus,
boolean canConfirm,
boolean readyForPda,
List<String> blockingReasons
) {
public PreloadReadiness {
blockingReasons = blockingReasons == null ? List.of() : List.copyOf(blockingReasons);
}
}
public record ConfirmPreloadRequest(@NotNull @Min(0) Integer expectedVersion) {
}
public record PreloadConfirmationResponse(
Long planId,
Long containerBookingId,
String confirmationStatus,
Integer version,
PreloadReadiness readiness
) {
}
public record PreloadSummary(
Long planId,
Long containerBookingId,
String preloadNo,
String containerNo,
String internalContainerNo,
String cabinetNo,
List<Customer> customers,
Long warehouseId,
String warehouseName,
@@ -99,7 +132,8 @@ public final class LoadingListDtos {
BigDecimal totalVolumeCbm,
BigDecimal totalGrossWeightKg,
LocalDateTime createdAt,
Integer version
Integer version,
PreloadReadiness readiness
) {
public PreloadSummary {
customers = customers == null ? List.of() : List.copyOf(customers);
@@ -113,7 +147,7 @@ public final class LoadingListDtos {
Long warehouseId,
String warehouseName,
String containerNo,
String internalContainerNo,
String cabinetNo,
String status,
long totalPackageCount,
BigDecimal totalGrossWeightKg,
@@ -132,9 +166,12 @@ public final class LoadingListDtos {
Long itemId,
Long accountId,
String customerName,
Long orderId,
String orderNo,
Long inboundId,
Long inboundDetailId,
String inboundCode,
String containerNo,
String skuCode,
String destinationWarehouse,
String chineseName,
@@ -155,7 +192,8 @@ public final class LoadingListDtos {
String customerRemark,
String warehouseRemark,
int loadSequence,
LocalDateTime frozenAt
LocalDateTime frozenAt,
Integer itemVersion
) {
}
@@ -60,17 +60,10 @@ public final class LoadingPickDtos {
@NotNull @Min(1) Long warehouseId,
@NotNull @Min(1) Long loadingPlanId,
@NotNull @Min(1) Long containerBookingId,
@Size(max = 64) String containerNo,
@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 64) String inventorySnapshotToken,
@NotEmpty @Size(max = 500) List<@Valid BatchItemRequest> items
) {
@Deprecated
public BatchRequest(Long warehouseId, String containerNo, Integer expectedVersion,
String inventorySnapshotToken, List<BatchItemRequest> items) {
this(warehouseId, null, null, containerNo, expectedVersion, inventorySnapshotToken, items);
}
}
) {}
public record BatchItemRequest(
@NotNull @Min(1) Long itemId,
@@ -132,8 +125,10 @@ public final class LoadingPickDtos {
Long operatorId, String operatorName, LocalDateTime createdAt) {
}
public record Task(Long id, Long loadingPlanId, Long containerBookingId, String cabinetNo,
String currentContainerNo, String taskStatus,
public record Task(Long id, Long loadingPlanId, Long containerBookingId, Long inboundId,
Integer inboundVersion, Integer loadingVersion, String cabinetNo,
String currentContainerNo, String sourceType, String recordStatus,
Long mergedIntoContainerBookingId, String taskStatus,
String workflowStatus, Integer version, Assignment assignment,
Review review, List<PickItem> items) {
}
@@ -141,6 +136,7 @@ public final class LoadingPickDtos {
/** PDA 工作台只展示当前作业员可领取或已领取的进行中任务。 */
public record PendingTask(Long taskId, Long loadingPlanId, Long containerBookingId,
String cabinetNo, String currentContainerNo,
String sourceType, String recordStatus, Long mergedIntoContainerBookingId,
Long warehouseId, String warehouseName, String taskStatus,
String workflowStatus, Integer version, Integer assignmentVersion,
Integer itemCount, Integer pendingItemCount) {
@@ -0,0 +1,13 @@
package com.baiyue.firstleg.dto;
import com.baiyue.firstleg.dto.FbaPricingDtos.FeeSnapshotResponse;
public record MeasurementApprovalResponse(
Long inboundId,
Long measurementId,
String processStatus,
String feeStatus,
boolean initialPricingSucceeded,
String initialPricingMessage,
FeeSnapshotResponse initialFee
) {}
@@ -35,7 +35,6 @@ public record MeasurementBatchCommand(
BigDecimal lengthCm,
BigDecimal widthCm,
BigDecimal heightCm,
BigDecimal unitWeightKg,
Long intelligentImageFileId,
String overLimitWarning,
BigDecimal outerLengthCm,
@@ -19,7 +19,7 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
public record MeasurementSummary(
Long measurementId, String sourceType, String measurementStatus,
Integer actualPackageCount, BigDecimal actualGrossWeightKg,
Integer actualPackageCount, BigDecimal actualGrossWeightKg, BigDecimal actualNetWeightKg,
BigDecimal actualVolumeCbm, BigDecimal volumeWeightKg, String actualGoodsName) {
}
@@ -31,15 +31,16 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
public record MeasurementItem(
Long id, String itemNo, String boxSpecification, Integer packageCount,
BigDecimal lengthCm, BigDecimal widthCm, BigDecimal heightCm,
BigDecimal unitWeightKg, BigDecimal totalWeightKg,
BigDecimal unitVolumeCbm, BigDecimal totalVolumeCbm,
BigDecimal volumeWeightKg) {
}
public record WeightResult(
Long id, Long fileId, BigDecimal recognizedWeightValue, String recognizedWeightUnit,
BigDecimal adjustedWeightValue, String adjustedWeightUnit,
BigDecimal finalWeightKg, String recognitionStatus) {
Long id, Long fileId, Long palletId, String palletCode, String palletType,
BigDecimal palletCapacityCbm, BigDecimal palletTareWeightKg,
BigDecimal recognizedWeightValue, String recognizedWeightUnit,
BigDecimal grossWeightKg, BigDecimal adjustedWeightValue, String adjustedWeightUnit,
BigDecimal netWeightKg, String recognitionStatus) {
}
public record BillingBasis(
@@ -62,7 +63,7 @@ public record MerchantInboundBillingResponse(InboundOrderDetail inbound,
BigDecimal chargeableQuantity, String chargeUnit,
String feeTypeName, BigDecimal unitPrice, String currency, BigDecimal amount,
String reviewStatus, String publicationStatus,
LocalDateTime publishedAt, LocalDateTime inboundedAt, Boolean settlementIncluded
LocalDateTime publishedAt, LocalDateTime inboundedAt
) {
}
}
@@ -23,7 +23,6 @@ public final class MojiaMeasurementDtos {
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal length,
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal width,
@NotNull @DecimalMin("0.001") @DecimalMax("999999999.999") BigDecimal height,
@DecimalMin("0.001") @DecimalMax("999999999999999.999") BigDecimal weight,
@NotBlank @Size(max = 64) String machineNo,
LocalDateTime measuredAt,
@NotBlank @Size(max = 7_100_000) String picture
@@ -47,7 +47,6 @@ public final class OpenApiMeasurementDtos {
@NotNull @DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal widthCm,
@NotNull @DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal heightCm,
@NotNull @Min(1) @Max(1000000) Integer packageCount,
@DecimalMin(value = "0", inclusive = false) @DecimalMax("999999999.999") BigDecimal unitWeightKg,
@NotBlank @Size(max = 64) String boxSpecification,
@Size(max = 500) String remark) {
}
@@ -31,8 +31,25 @@ public record OrderApprovedInboundCommand(
String publicRemark,
String expressCompanyName,
String expressTrackingNo,
Integer totalPackageCount
Integer totalPackageCount,
String pickupMethod,
LocalDateTime requestedPickupAt
) {
public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
Long warehouseId, String warehouseName, Long deliveryWarehouseId, String deliveryWarehouseName,
LocalDateTime expectedArrivalAt, String businessType, String channelCode, String channelName,
Long carrierId, String carrierName, String taxMode, String pricingTaxMode, String containerMode,
Boolean customsInspection, String destinationType, Long destinationWarehouseId, String destinationCode,
String destinationName, String publicRemark, String expressCompanyName, String expressTrackingNo,
Integer totalPackageCount
) {
this(tenantId, orderId, orderNo, inboundCode, customerId, customerName, warehouseId, warehouseName,
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, totalPackageCount, null, null);
}
/** 兼容尚未携带订单总箱数的历史事件。 */
public OrderApprovedInboundCommand(
long tenantId, long orderId, String orderNo, String inboundCode, Long customerId, String customerName,
@@ -46,7 +63,7 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, pricingTaxMode, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null);
expressTrackingNo, null, null, null);
}
/** 兼容未携带报价包税维度的历史调用方。 */
@@ -62,6 +79,6 @@ public record OrderApprovedInboundCommand(
deliveryWarehouseId, deliveryWarehouseName, expectedArrivalAt, businessType, channelCode, channelName,
carrierId, carrierName, taxMode, null, containerMode, customsInspection, destinationType,
destinationWarehouseId, destinationCode, destinationName, publicRemark, expressCompanyName,
expressTrackingNo, null);
expressTrackingNo, null, null, null);
}
}
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.constraints.Pattern;
public final class PriceBookChannelValidation {
private PriceBookChannelValidation() {}
@@ -10,6 +11,10 @@ public final class PriceBookChannelValidation {
public record Request(
@Min(1) long tenantId,
@Min(1) long originWarehouseId,
@NotBlank @Size(max = 64) String channelCode
@NotBlank @Size(max = 64) String channelCode,
@NotBlank @Pattern(regexp = "FBA|OVERSEAS|PRIVATE") String destinationType,
boolean requireInboundCodePrefix
) {}
public record Response(String channelCode, String inboundCodePrefix) {}
}
@@ -3,17 +3,17 @@ import com.baiyue.firstleg.entity.LoadingPlanItemEntity; import jakarta.validati
public final class SmartLoadingDtos { private SmartLoadingDtos(){}
public record Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, String channelCode, String channelName, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName, String customerName, String goodsName, Integer availablePackageCount,
BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) {
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) {
public Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, String destinationWarehouseCode, String customerName, String goodsName, Integer availablePackageCount,
BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) {
this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm);
BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) {
this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,null,destinationWarehouseCode,null,customerName,goodsName,availablePackageCount,unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm);
}
public Candidate(Long accountId, Long inboundId, Long inboundDetailId, Long orderId, String orderNo, String inboundCode,
Long warehouseId, Long destinationWarehouseId, String destinationWarehouseCode, String destinationWarehouseName,
String customerName, String goodsName, Integer availablePackageCount, BigDecimal unitWeightKg, BigDecimal unitVolumeCbm) {
String customerName, String goodsName, Integer availablePackageCount, BigDecimal unitGrossWeightKg, BigDecimal unitNetWeightKg, BigDecimal unitVolumeCbm) {
this(accountId,inboundId,inboundDetailId,orderId,orderNo,inboundCode,warehouseId,null,null,destinationWarehouseId,
destinationWarehouseCode,destinationWarehouseName,customerName,goodsName,availablePackageCount,unitWeightKg,unitVolumeCbm);
destinationWarehouseCode,destinationWarehouseName,customerName,goodsName,availablePackageCount,unitGrossWeightKg,unitNetWeightKg,unitVolumeCbm);
}
}
public record CreateRequest(@NotNull @Positive Long bookingId, @Size(max=500) String remark) {}
@@ -25,6 +25,11 @@ public final class SmartLoadingDtos { private SmartLoadingDtos(){}
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
public record DeleteRequest(@NotNull @Min(0) Integer expectedPlanVersion,
@NotNull @Min(0) Integer expectedItemVersion,@NotBlank @Size(max=128) String operationId) {}
public record WarehouseRemarkRequest(@Size(max=1000) String warehouseRemark,
@NotNull @Min(0) Integer expectedPlanVersion, @NotNull @Min(0) Integer expectedItemVersion,
@NotBlank @Size(max=128) String operationId) {}
public record PushRequest(@NotNull @Min(0) Integer expectedVersion) {}
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,Integer version,List<LoadingPlanItemEntity> items) {}
public record Plan(Long id,String planNo,String status,Long bookingId,String containerNo,String voyageNo,
Integer version,Boolean canAssign,Boolean canEdit,String blockedReason,String finalCompletionStatus,
Long pdaTaskId,String pdaTaskStatus,Integer pdaTaskVersion,List<LoadingPlanItemEntity> items) {}
}
@@ -1,8 +1,8 @@
package com.baiyue.firstleg.dto;
import jakarta.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDateTime;
public final class TruckAppointmentDtos { private TruckAppointmentDtos(){}
public record Query(@Size(max=64) String keyword, com.baiyue.firstleg.domain.TruckAppointmentStatus status, @Positive Long containerBookingId, @Min(1) long page, @Min(1) @Max(200) long size){ public Query{if(page==0)page=1;if(size==0)size=20;} }
public record SaveRequest(@Positive Long outboundOrderId,@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId,@Positive Long warehouseId,@Positive Long customerId,@Size(max=128) String customerName,@Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@DecimalMin("0") BigDecimal receivableAmount,@Size(max=8) String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){}
public record Query(@Size(max=64) String keyword, com.baiyue.firstleg.domain.TruckAppointmentStatus status, @Positive Long customerId, @Positive Long containerBookingId, @Min(1) long page, @Min(1) @Max(200) long size){ public Query{if(page==0)page=1;if(size==0)size=20;} }
public record SaveRequest(@Positive Long outboundOrderId,@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId,@Positive Long warehouseId,@NotNull @Positive Long customerId,@Size(max=128) String customerName,@NotNull @Positive Long carrierId,@Size(max=128) String carrierName,@Size(max=128) String billOfLadingNo,@Size(max=64) String fleetContact,@Size(max=32) String fleetPhone,@Size(max=64) String driverName,@Size(max=32) String driverPhone,@Size(max=32) String driverIdCard,@Size(max=32) String vehicleNo,@Size(max=128) String storeName,@Size(max=64) String storeContact,@Size(max=32) String storePhone,@Size(max=255) String storeAddress,@Size(max=64) String storeCity,@Size(max=16) String storeZipcode,@PositiveOrZero Integer packageCount,@DecimalMin("0") BigDecimal grossWeightKg,@DecimalMin("0") BigDecimal volumeCbm,@PositiveOrZero Integer palletCount,@Size(max=255) String poNo,@NotNull LocalDateTime plannedLoadingAt,@PositiveOrZero Integer transitMinutes,@DecimalMin("0") BigDecimal payableAmount,@DecimalMin("0") BigDecimal receivableAmount,@Size(max=8) String currency,@Size(max=1000) String remark,@NotNull @PositiveOrZero Integer expectedVersion){}
public record ActionRequest(@NotNull @PositiveOrZero Integer expectedVersion,@Size(max=500) String reason){}
public record AttachmentRequest(@NotNull @Positive Long fileId,@Size(max=64) String attachmentType,@Size(max=255) String fileName,@Size(max=128) String fileHash){ }
public record AttachmentRequest(@NotNull @Positive Long fileId,@NotBlank @Pattern(regexp="BILL_OF_LADING|DISPATCH_ORDER|DRIVER_DOCUMENT|VEHICLE_DOCUMENT|OTHER") String attachmentType,@Size(max=255) String fileName,@Size(max=128) String fileHash){ }
}
@@ -20,10 +20,15 @@ public class ContainerBookingEntity {
private String bookingNo;
private String containerNo;
private String cabinetNo;
private String sourceType;
private String recordStatus;
private Long mergedIntoContainerBookingId;
private String channelCode;
private String channelName;
private Long oceanBookingId;
private Long oceanBookingRequirementId;
private Long customerId;
private String customerName;
private Long warehouseId;
private String warehouseName;
private String billOfLadingNo;
@@ -40,6 +45,7 @@ public class ContainerBookingEntity {
private String destinationWarehouseName;
private BigDecimal capacityCbm;
private BigDecimal maxWeightKg;
private Long terminalTruckingCompanyId;
private String terminalTruckingCompany;
private LocalDateTime loadingAt;
private LocalDateTime estimatedSailingAt;
@@ -56,5 +62,11 @@ public class ContainerBookingEntity {
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
@TableField(exist = false) private Long planId;
@TableField(exist = false) private String planNo;
@TableField(exist = false) private String planStatus;
@TableField(exist = false) private Boolean exceptionActive;
@TableField(exist = false) private String destinationClearanceDocumentStatus;
@TableField(exist = false) private String customsDeclarationDocumentStatus;
@TableField(exist = false) private String deliveryOrderDocumentStatus;
@TableField(exist = false) private String pickupOrderDocumentStatus;
}
@@ -25,12 +25,12 @@ public class ContainerFeeDetailEntity {
private String feeItemCodeSnapshot;
private String feeItemNameSnapshot;
private String direction;
private Long partnerId;
private String partnerName;
private Long supplierId;
private String supplierName;
private String supplierCategory;
private LocalDate businessDate;
private BigDecimal amount;
private String currency;
private Boolean settlementIncluded;
private String reviewStatus;
private String sourceStatus;
private Integer sourceVersion;
@@ -0,0 +1,39 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/** 柜次费用审核发布时固化的不可变版本快照。 */
@Getter
@Setter
@TableName("fl_container_fee_source_snapshot")
public class ContainerFeeSourceSnapshotEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long sourceFeeId;
private Integer sourceVersion;
private Long tenantId;
private Long loadingPlanId;
private Long containerBookingId;
private String containerNo;
private Long feeItemId;
private String feeItemCode;
private String feeItemName;
private String direction;
private Long supplierId;
private String supplierName;
private String supplierCategory;
private LocalDate businessDate;
private BigDecimal amount;
private String currency;
private String reviewStatus;
private LocalDateTime reviewedAt;
private LocalDateTime createdAt;
}
@@ -18,6 +18,7 @@ public class FeeItemEntity extends BaseEntity {
private String category;
private String tradeScope;
private String feeCode;
private String supplierCategory;
private String feeTypeName;
private Boolean pickupPayable;
private Boolean pickupReceivable;
@@ -28,4 +29,5 @@ public class FeeItemEntity extends BaseEntity {
private String status;
private String createdByName;
private String updatedByName;
}
@@ -27,8 +27,9 @@ public class InboundFeeDetailEntity extends BaseEntity {
private String sourceFeeNo;
private String inboundCode;
private String direction;
private Long partnerId;
private String partnerName;
private Long supplierId;
private String supplierName;
private String supplierCategory;
private LocalDate businessDate;
private Long customerId;
private String customerName;
@@ -62,5 +63,4 @@ public class InboundFeeDetailEntity extends BaseEntity {
private String publishedByName;
private LocalDateTime publishedAt;
private LocalDateTime inboundedAt;
private Boolean settlementIncluded;
}
@@ -29,11 +29,9 @@ public class InboundMeasurementItemEntity {
private BigDecimal widthCm;
private BigDecimal heightCm;
private BigDecimal girthCm;
private BigDecimal unitWeightKg;
private BigDecimal unitVolumeCbm;
private BigDecimal volumeRawCbm;
private BigDecimal volumeCbm;
private BigDecimal totalWeightKg;
private BigDecimal totalVolumeCbm;
private BigDecimal volumeWeightKg;
private String calculationRuleVersion;
@@ -0,0 +1,23 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/** 客户上门提货要求在入库域的快照。 */
@Getter
@Setter
@TableName("fl_inbound_pickup_snapshot")
public class InboundPickupSnapshotEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long inboundId;
private String pickupMethod;
private LocalDateTime pickupAt;
private LocalDateTime createdAt;
}
@@ -36,6 +36,7 @@ public class InboundPricingEntity extends BaseEntity {
private Long destinationWarehouseId;
private String destinationName;
private BigDecimal actualGrossWeightKg;
private BigDecimal actualNetWeightKg;
private BigDecimal actualVolumeCbm;
private BigDecimal chargeableWeightKg;
private BigDecimal chargeableVolumeCbm;
@@ -10,7 +10,7 @@ import lombok.Setter;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/** 称重图片 OCR 原值、人工修正值和最终确认重量。 */
/** 称重图片、托盘皮重快照、OCR 原值和最终确认净重。 */
@Getter
@Setter
@TableName("fl_inbound_weight_result")
@@ -20,6 +20,11 @@ public class InboundWeightResultEntity extends BaseEntity {
private Long tenantId;
private Long inboundId;
private Long fileId;
private Long palletId;
private String palletCode;
private String palletType;
private BigDecimal palletCapacityCbm;
private BigDecimal palletTareWeightKg;
private String ocrProvider;
private String ocrRawText;
private BigDecimal ocrWeightValue;
@@ -27,7 +32,8 @@ public class InboundWeightResultEntity extends BaseEntity {
private BigDecimal confidence;
private BigDecimal adjustedWeightValue;
private String adjustedWeightUnit;
private BigDecimal finalWeightKg;
private BigDecimal grossWeightKg;
private BigDecimal netWeightKg;
private String recognitionStatus;
private String adjustmentReason;
private Long adjustedBy;
@@ -25,7 +25,8 @@ public class InboundWeightRevisionEntity {
private String beforeWeightUnit;
private BigDecimal afterWeightValue;
private String afterWeightUnit;
private BigDecimal finalWeightKg;
private BigDecimal grossWeightKg;
private BigDecimal netWeightKg;
private String reason;
private Long operatorId;
private String operatorName;
@@ -10,6 +10,11 @@ public class LoadingPlanItemEntity {
private String fbaTrackingNo; private String shipmentNo; private String shippingMark; private Integer loadSequence; private String positionSnapshot;
private String internalRemark; private String customerRemark; private String warehouseRemark; private LocalDateTime frozenAt; private Long frozenBy;
private Long removedBy; private String removedByName; private LocalDateTime removedAt; @TableLogic private Boolean deleted;
@TableField(exist=false) private Long activeInboundDetailId; private Long createdBy; private String createdByName; private LocalDateTime createdAt;
@TableField(exist=false) private Long activeInboundDetailId;
@TableField(exist=false) private Integer pickedPackageCount;
@TableField(exist=false) private Integer minPackageCount;
@TableField(exist=false) private Boolean canAdjust;
@TableField(exist=false) private Boolean canRemove;
private Long createdBy; private String createdByName; private LocalDateTime createdAt;
private Long updatedBy; private String updatedByName; private LocalDateTime updatedAt; @Version private Integer version;
}
@@ -23,7 +23,6 @@ public class MojiaMeasurementReceiptEntity {
private BigDecimal lengthCm;
private BigDecimal widthCm;
private BigDecimal heightCm;
private BigDecimal weightKg;
private String machineNo;
private LocalDateTime measuredAt;
private Long imageFileId;
@@ -15,5 +15,7 @@ public class PriceChannelCatalogEntity {
private String priceModel;
private String sourceSheetName;
private String aliasesJson;
private String inboundCodePrefix;
private String overseasInboundCodePrefix;
private String status;
}
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Getter;
import lombok.Setter;
import java.math.BigDecimal;
@@ -22,6 +23,7 @@ public class TruckAppointmentEntity {
private String customerName;
private Long carrierId;
private String carrierName;
private String billOfLadingNo;
private String fleetContact;
private String fleetPhone;
private String driverName;
@@ -53,4 +55,13 @@ public class TruckAppointmentEntity {
private String remark;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@TableField(exist = false) private String oceanBookingNo;
@TableField(exist = false) private String warehouseName;
@TableField(exist = false) private Long shippingCompanyId;
@TableField(exist = false) private String shippingCompanyName;
@TableField(exist = false) private Long bookingAgentId;
@TableField(exist = false) private String bookingAgentName;
@TableField(exist = false) private String cabinetNo;
@TableField(exist = false) private String containerNo;
@TableField(exist = false) private Long attachmentCount;
}
@@ -17,7 +17,7 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
PreloadSnapshot preload(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Select("SELECT p.id,p.container_booking_id containerBookingId,b.container_no containerNo,"
+ "p.plan_status status,p.version FROM fl_loading_plan p JOIN fl_container_booking b "
+ "p.plan_status status,p.version,p.preload_pushed_at preloadPushedAt FROM fl_loading_plan p JOIN fl_container_booking b "
+ "ON b.id=p.container_booking_id WHERE p.id=#{loadingPlanId}")
LoadingPlanSnapshot loadingPlan(@Param("loadingPlanId") long loadingPlanId);
@@ -27,6 +27,11 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
int allocatedPackages(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("loadingPlanId") long loadingPlanId);
@Select("SELECT account_id tenantId,inbound_id inboundId,COALESCE(SUM(package_count),0) allocatedPackageCount "
+ "FROM fl_loading_plan_item WHERE loading_plan_id=#{loadingPlanId} AND deleted=0 "
+ "GROUP BY account_id,inbound_id HAVING SUM(package_count)>0")
List<AllocationScope> allocatedScopes(@Param("loadingPlanId") long loadingPlanId);
@Update("UPDATE fl_preload_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},"
+ "version=version+1 WHERE id=#{preloadPlanId} AND tenant_id=#{tenantId} "
+ "AND inbound_id=#{inboundId} AND status='PENDING' AND version=#{expectedVersion}")
@@ -34,6 +39,18 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
@Param("inboundId") long inboundId, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("UPDATE fl_preload_plan SET status='CONFIRMED',confirmed_by=#{operatorId},"
+ "confirmed_by_name=#{operatorName},confirmed_at=NOW(),updated_by=#{operatorId},"
+ "updated_by_name=#{operatorName},version=version+1 WHERE id=#{preloadPlanId} "
+ "AND tenant_id=#{tenantId} AND inbound_id=#{inboundId} AND status='PENDING' "
+ "AND version=#{expectedVersion}")
int confirmIncrementalPreload(@Param("preloadPlanId") long preloadPlanId,
@Param("tenantId") long tenantId,
@Param("inboundId") long inboundId,
@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("UPDATE fl_loading_plan SET updated_by=#{operatorId},updated_by_name=#{operatorName},"
+ "version=version+1 WHERE id=#{loadingPlanId} AND plan_status='EDITING' "
+ "AND version=#{expectedVersion}")
@@ -69,5 +86,11 @@ public interface LoadingModelBridgeMapper extends BaseMapper<PreloadLoadingPlanL
record PreloadSnapshot(Long id, Long tenantId, Long inboundId, String status, Integer version) {}
record LoadingPlanSnapshot(Long id, Long containerBookingId, String containerNo,
String status, Integer version) {}
String status, Integer version, java.time.LocalDateTime preloadPushedAt) {
public LoadingPlanSnapshot(Long id, Long containerBookingId, String containerNo,
String status, Integer version) {
this(id, containerBookingId, containerNo, status, version, null);
}
}
record AllocationScope(Long tenantId, Long inboundId, Integer allocatedPackageCount) {}
}
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
import com.baiyue.firstleg.loading.bridge.entity.PreloadLoadingPlanLinkEntity;
import com.baiyue.firstleg.loading.bridge.mapper.LoadingModelBridgeMapper;
import com.baiyue.firstleg.loading.bridge.mapper.LoadingModelBridgeMapper.AllocationScope;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
@@ -24,6 +25,7 @@ public class LoadingModelBridgeRepository {
public int allocatedPackages(long tenantId, long inboundId, long loadingPlanId) {
return mapper.allocatedPackages(tenantId, inboundId, loadingPlanId);
}
public List<AllocationScope> allocatedScopes(long loadingPlanId) { return mapper.allocatedScopes(loadingPlanId); }
public PreloadLoadingPlanLinkEntity byPair(long tenantId, long inboundId, long loadingPlanId) {
return mapper.selectOne(new LambdaQueryWrapper<PreloadLoadingPlanLinkEntity>()
@@ -40,11 +42,19 @@ public class LoadingModelBridgeRepository {
public void insert(PreloadLoadingPlanLinkEntity value) { mapper.insert(value); }
public void delete(PreloadLoadingPlanLinkEntity value) { mapper.deleteById(value); }
public int touchPreload(long preloadPlanId, long tenantId, long inboundId, int version,
long operatorId, String operatorName) {
return mapper.touchPreload(preloadPlanId, tenantId, inboundId, version, operatorId, operatorName);
}
public int confirmIncrementalPreload(long preloadPlanId, long tenantId, long inboundId, int version,
long operatorId, String operatorName) {
return mapper.confirmIncrementalPreload(preloadPlanId, tenantId, inboundId, version,
operatorId, operatorName);
}
public int touchLoadingPlan(long loadingPlanId, int version, long operatorId, String operatorName) {
return mapper.touchLoadingPlan(loadingPlanId, version, operatorId, operatorName);
}
@@ -7,6 +7,7 @@ import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.BindRequest
import com.baiyue.firstleg.loading.bridge.dto.LoadingModelBridgeDtos.Link;
import com.baiyue.firstleg.loading.bridge.entity.PreloadLoadingPlanLinkEntity;
import com.baiyue.firstleg.loading.bridge.repository.LoadingModelBridgeRepository;
import com.baiyue.firstleg.loading.bridge.mapper.LoadingModelBridgeMapper.AllocationScope;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
@@ -68,9 +69,117 @@ public class LoadingModelBridgeService {
return requireLink(tenantId, inboundId, loadingPlanId);
}
/**
* Creates every preload link required by a pushed loading plan before PDA can start.
* The caller owns the surrounding push transaction, so a missing preload or stale
* version rolls back the push and cannot leave a LOADING task without a link.
*/
@Transactional
public List<Link> bindAllocatedBeforeLoading(long loadingPlanId, long operatorId,
String operatorName, String orchestrationKey) {
var loadingPlan = repository.loadingPlan(loadingPlanId);
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
if (scopes.isEmpty()) throw conflict("装柜计划尚未分配入库单货物,不能开始PDA作业");
if (!"EDITING".equals(loadingPlan.status())) {
if (Set.of("LOADING", "COMPLETED").contains(loadingPlan.status())
&& scopes.stream().allMatch(scope -> repository.byPair(
scope.tenantId(), scope.inboundId(), loadingPlanId) != null)) {
return repository.byLoadingPlan(loadingPlanId);
}
throw conflict("PDA开始前的预装关联不完整,禁止在LOADING后补关联");
}
for (AllocationScope scope : scopes) {
// Internal push replay is pair-idempotent: an existing association has
// already advanced both versions, so calling public bind again would
// derive a different request hash for the same AUTO key.
if (repository.byPair(scope.tenantId(), scope.inboundId(), loadingPlanId) != null) {
continue;
}
var preload = repository.preload(scope.tenantId(), scope.inboundId());
var currentPlan = repository.loadingPlan(loadingPlanId);
if (preload == null || currentPlan == null) {
throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
}
String key = "AUTO:" + hash(orchestrationKey + "|" + scope.tenantId() + "|"
+ scope.inboundId() + "|" + loadingPlanId).substring(0, 60);
bind(scope.tenantId(), scope.inboundId(), loadingPlanId, operatorId, operatorName, key,
new BindRequest(preload.version(), currentPlan.version()));
}
return repository.byLoadingPlan(loadingPlanId);
}
/** Adds a newly allocated inbound to an already pushed EDITING/LOADING plan. */
@Transactional
public void bindIncrementalAllocation(long loadingPlanId, long tenantId, long inboundId,
long operatorId, String operatorName, String operationId) {
if (repository.byPair(tenantId, inboundId, loadingPlanId) != null) return;
var plan = repository.loadingPlan(loadingPlanId);
if (plan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
if (plan.preloadPushedAt() == null) return;
if (!Set.of("EDITING", "LOADING").contains(plan.status())) {
throw conflict("PDA任务正在最终确认或已经完成,不能新增预装关联");
}
if (repository.allocatedPackages(tenantId, inboundId, loadingPlanId) <= 0) {
throw conflict("柜次计划尚未分配当前入库单货物");
}
var preload = repository.preload(tenantId, inboundId);
if (preload == null) throw new BizException(ResultCode.NOT_FOUND, "入库单预装计划不存在");
if (!Set.of("PENDING", "CONFIRMED").contains(preload.status())) {
throw conflict("新增入库单预装状态不允许进入当前柜次");
}
PreloadLoadingPlanLinkEntity link = new PreloadLoadingPlanLinkEntity();
link.setTenantId(tenantId); link.setInboundId(inboundId); link.setPreloadPlanId(preload.id());
link.setLoadingPlanId(loadingPlanId);
link.setIdempotencyKey("INCREMENTAL:" + hash(operationId + "|" + tenantId + "|" + inboundId
+ "|" + loadingPlanId).substring(0, 52));
link.setRequestHash(hash("INCREMENTAL|" + tenantId + "|" + inboundId + "|" + loadingPlanId));
link.setCreatedBy(operatorId); link.setCreatedByName(operatorName);
try {
repository.insert(link);
} catch (DuplicateKeyException exception) {
if (repository.byPair(tenantId, inboundId, loadingPlanId) == null) {
throw conflict("新增入库单预装关联并发冲突,请刷新后重试");
}
}
// A running PDA task has already passed plan-level confirmation. Adding an inbound is the
// explicit Admin approval for that new allocation, so its fresh PENDING preload joins the
// same confirmed plan atomically instead of leaving an unreachable pending aggregate.
if ("LOADING".equals(plan.status()) && "PENDING".equals(preload.status())
&& repository.confirmIncrementalPreload(preload.id(), tenantId, inboundId,
preload.version(), operatorId, operatorName) != 1) {
throw conflict("新增入库单预装确认并发冲突,请刷新后重试");
}
}
/** Removes only a now-empty allocation link; picked-item protection is enforced by the PDA task lock. */
@Transactional
public void removeIfUnallocated(long loadingPlanId, long tenantId, long inboundId) {
if (repository.allocatedPackages(tenantId, inboundId, loadingPlanId) > 0) return;
PreloadLoadingPlanLinkEntity link = repository.byPair(tenantId, inboundId, loadingPlanId);
if (link != null) repository.delete(link);
}
public List<Link> byInbound(long tenantId, long inboundId) { return repository.byInbound(tenantId, inboundId); }
public List<Link> byLoadingPlan(long loadingPlanId) { return repository.byLoadingPlan(loadingPlanId); }
public List<Link> requireAllocatedLinksForConfirmation(long loadingPlanId) {
var loadingPlan = repository.loadingPlan(loadingPlanId);
if (loadingPlan == null) throw new BizException(ResultCode.NOT_FOUND, "柜次装柜计划不存在");
if (loadingPlan.preloadPushedAt() == null || !"EDITING".equals(loadingPlan.status())) {
throw conflict("仅已推送且仍为EDITING的预装计划可以确认");
}
List<AllocationScope> scopes = repository.allocatedScopes(loadingPlanId);
if (scopes.isEmpty()) throw conflict("装柜计划尚未分配入库单货物");
List<Link> links = repository.byLoadingPlan(loadingPlanId);
boolean complete = scopes.stream().allMatch(scope -> links.stream().anyMatch(link ->
Objects.equals(link.tenantId(), scope.tenantId())
&& Objects.equals(link.inboundId(), scope.inboundId())
&& link.allocatedPackageCount() != null && link.allocatedPackageCount() > 0));
if (!complete) throw conflict("装柜计划的预装关联不完整");
return links;
}
public void requireStarted(long tenantId, long inboundId) {
List<Link> links = requireLinks(tenantId, inboundId);
if (links.stream().anyMatch(link -> link.allocatedPackageCount() == null || link.allocatedPackageCount() <= 0
@@ -18,6 +18,7 @@ import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/** 平台端装柜证据和国内装柜完成操作。 */
@@ -58,6 +59,16 @@ public class PlatformLoadingDeliveryController {
return ApiResponse.success(service.evidence(tenantId, inboundId));
}
@Operation(summary = "PDA查看装柜证据照片")
@GetMapping("/{inboundId}/loading/evidence/{evidenceId}/content/pda")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ResponseEntity<byte[]> pdaEvidenceContent(
@AuthenticationPrincipal AuthenticatedUser user, @RequestParam long tenantId, @PathVariable long inboundId,
@PathVariable long evidenceId) {
FirstlegScopeGuard.platform(user, tenantId);
return service.evidenceContent(tenantId, inboundId, evidenceId);
}
@Operation(summary = "PDA作废错误装柜证据")
@PostMapping("/{inboundId}/loading/evidence/{evidenceId}/void/pda")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
@@ -7,7 +7,7 @@ public final class LoadingResponses {
private LoadingResponses() {}
public record EvidenceResponse(Long id, Long inboundId, Long tenantId, String evidenceType,
Long fileId, String fileName, String sealNo, String remark,
Long fileId, String fileName, String previewUrl, String sealNo, String remark,
Integer version, String status, String voidReason,
Long voidedBy, String voidedByName, LocalDateTime voidedAt,
LocalDateTime createdAt, LocalDateTime updatedAt) {}
@@ -12,20 +12,15 @@ public interface LoadingCompletionGateMapper {
COALESCE(SUM(CASE WHEN EXISTS (
SELECT 1 FROM fl_ocean_booking_container_allocation a
JOIN fl_ocean_booking b ON b.id=a.ocean_booking_id
WHERE a.container_booking_id=p.container_booking_id AND b.status='RELEASED') THEN 1 ELSE 0 END),0) oceanReady,
COALESCE(SUM(CASE WHEN EXISTS (
SELECT 1 FROM fl_truck_appointment t
WHERE t.container_booking_id=p.container_booking_id AND t.loading_plan_id=p.id
AND t.status='COMPLETED') THEN 1 ELSE 0 END),0) truckReady
WHERE a.container_booking_id=p.container_booking_id AND b.status='RELEASED') THEN 1 ELSE 0 END),0) oceanReady
FROM fl_preload_loading_plan_link l
JOIN fl_loading_plan p ON p.id=l.loading_plan_id
WHERE l.tenant_id=#{tenantId} AND l.inbound_id=#{inboundId}
""")
GateSnapshot snapshot(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
record GateSnapshot(Integer linkCount, Integer oceanReady, Integer truckReady) {
record GateSnapshot(Integer linkCount, Integer oceanReady) {
public boolean allBookingReady() { return count(linkCount) > 0 && count(linkCount) == count(oceanReady); }
public boolean allTruckReady() { return count(linkCount) > 0 && count(linkCount) == count(truckReady); }
private static int count(Integer value) { return value == null ? 0 : value; }
}
}
@@ -11,6 +11,16 @@ public interface LoadingInboundMapper {
+ "FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0")
Snapshot find(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Select("SELECT id, tenant_id AS tenantId, process_status AS processStatus, version "
+ "FROM fl_inbound_order WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 FOR UPDATE")
Snapshot findForUpdate(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId);
@Update("UPDATE fl_inbound_order SET process_status='LOADING', updated_by=#{operatorId}, "
+ "version=version+1 WHERE tenant_id=#{tenantId} AND id=#{inboundId} AND deleted=0 "
+ "AND process_status='WAIT_SPLIT' AND version=#{expectedVersion}")
int beginLoading(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId,
@Param("operatorId") long operatorId, @Param("expectedVersion") int expectedVersion);
@Select("""
SELECT batch.source_inbound_id AS sourceInboundId,
batch.loaded_child_inbound_id AS loadedChildInboundId,
@@ -25,6 +25,10 @@ public class LoadingRepository {
return inbounds.find(tenantId, inboundId);
}
public LoadingInboundMapper.Snapshot findInboundForUpdate(long tenantId, long inboundId) {
return inbounds.findForUpdate(tenantId, inboundId);
}
public int completeLoading(long tenantId, long inboundId, long operatorId, int expectedVersion) {
return inbounds.completeLoading(tenantId, inboundId, operatorId, expectedVersion);
}
@@ -11,13 +11,12 @@ import org.springframework.stereotype.Service;
public class LoadingCompletionGateService {
@Resource private LoadingCompletionGateMapper mapper;
public void requireBookingAndTransport(long tenantId, long inboundId) {
public void requireReleasedBooking(long tenantId, long inboundId) {
LoadingCompletionGateMapper.GateSnapshot gate = mapper.snapshot(tenantId, inboundId);
if (gate == null || gate.linkCount() == null || gate.linkCount() == 0) {
throw blocked("未找到有效柜次关联");
}
if (!gate.allBookingReady()) throw blocked("订舱尚未完成(必须为 RELEASED)");
if (!gate.allTruckReady()) throw blocked("拖车尚未完成(必须为 COMPLETED)");
}
private static BizException blocked(String message) {
@@ -24,6 +24,7 @@ import feign.FeignException;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -111,7 +112,7 @@ public class LoadingDeliveryService {
}
if (!"LOADING".equals(inbound.processStatus())) throw new BizException(ResultCode.CONFLICT, "当前订单不在装柜中");
loadingModelBridge.requireCompleted(tenantId, inboundId);
completionGate.requireBookingAndTransport(tenantId, inboundId);
completionGate.requireReleasedBooking(tenantId, inboundId);
requireCompletedPicking(tenantId, inboundId);
requireCompletionEvidence(tenantId, inboundId);
if (repository.completeLoading(tenantId, inboundId, operatorId, request.expectedVersion()) != 1) throw concurrent();
@@ -120,6 +121,38 @@ public class LoadingDeliveryService {
new LoadingActionResponse("LOADED", evidenceResponse(tenantId, inboundId)));
}
/**
* PDA settlement may increment the source version and may create a loaded fulfillment slice.
* Resolve and lock the canonical completion row after settlement, then retain optimistic locking
* with that current server version instead of reusing the pre-settlement task snapshot.
*/
public LoadingActionResponse completeAfterPdaSettlement(long tenantId, long inboundId,
long operatorId, String operatorName,
String key, String remark) {
CompletionTarget target = repository.completionTarget(tenantId, inboundId);
long completionInboundId = target == null ? inboundId : target.completionInboundId();
Snapshot current = repository.findInboundForUpdate(tenantId, completionInboundId);
if (current == null || current.version() == null) {
throw new BizException(ResultCode.CONFLICT, "PDA结算后的实装履约分片不存在,请刷新后重试");
}
return completeLoading(tenantId, inboundId, operatorId, operatorName, key,
new CompleteLoadingRequest(current.version(), remark));
}
/**
* Read-only preflight used before PDA settlement. The current plan may still be LOADING here;
* every external/business hard gate must pass before the PDA task is mutated.
*/
public void requirePdaCompletionReady(long tenantId, long inboundId) {
Snapshot inbound = requireInbound(tenantId, inboundId);
if (!"LOADING".equals(inbound.processStatus())) {
throw new BizException(ResultCode.CONFLICT, "PDA确认装柜完成前订单必须处于LOADING");
}
loadingModelBridge.requireStarted(tenantId, inboundId);
completionGate.requireReleasedBooking(tenantId, inboundId);
requireCompletionEvidence(tenantId, inboundId);
}
private LoadingActionResponse completeFulfillmentSlice(long tenantId, long evidenceInboundId,
long operatorId, String operatorName, String key,
CompleteLoadingRequest request,
@@ -132,7 +165,7 @@ public class LoadingDeliveryService {
throw new BizException(ResultCode.CONFLICT, "实装履约分片不在装柜中");
}
loadingModelBridge.requireCompleted(tenantId, target.sourceInboundId());
completionGate.requireBookingAndTransport(tenantId, target.sourceInboundId());
completionGate.requireReleasedBooking(tenantId, target.sourceInboundId());
requireCompletedPicking(tenantId, target.sourceInboundId());
requireCompletionEvidence(tenantId, target.sourceInboundId());
if (repository.completeLoadedChild(tenantId, target.sourceInboundId(), operatorId,
@@ -149,6 +182,22 @@ public class LoadingDeliveryService {
return evidenceResponse(tenantId, inboundId);
}
public ResponseEntity<byte[]> evidenceContent(long tenantId, long inboundId, long evidenceId) {
requireLoadingStage(tenantId, inboundId);
LoadingEvidenceEntity evidence = repository.listEvidence(tenantId, inboundId).stream()
.filter(item -> Long.valueOf(evidenceId).equals(item.getId()) && isActive(item))
.findFirst().orElseThrow(() -> new BizException(ResultCode.NOT_FOUND, "装柜证据不存在"));
try {
ResponseEntity<byte[]> response = files.download(internalToken, tenantId, inboundId, evidence.getFileId());
if (response == null || response.getBody() == null) {
throw new BizException(ResultCode.NOT_FOUND, "装柜证据文件不存在");
}
return response;
} catch (FeignException ex) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用");
}
}
@Transactional
public LoadingActionResponse voidEvidence(long tenantId, long inboundId, long evidenceId, long operatorId,
String operatorName, String key, VoidEvidenceRequest request) {
@@ -333,11 +382,16 @@ public class LoadingDeliveryService {
private EvidenceListResponse evidenceResponse(long tenantId, long inboundId) {
List<EvidenceResponse> values = repository.listEvidence(tenantId, inboundId).stream().map(e ->
new EvidenceResponse(e.getId(), e.getInboundId(), e.getTenantId(), e.getEvidenceType(), e.getFileId(),
e.getFileName(), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
e.getFileName(), evidencePreviewUrl(tenantId, inboundId, e.getId()), e.getSealNo(), e.getRemark(), e.getVersion(), activeStatus(e), e.getVoidReason(),
e.getVoidedBy(), e.getVoidedByName(), e.getVoidedAt(), e.getCreatedAt(), e.getUpdatedAt())).toList();
return new EvidenceListResponse(inboundId, values);
}
private static String evidencePreviewUrl(long tenantId, long inboundId, long evidenceId) {
return "/api/platform/v1/inbounds/" + inboundId + "/loading/evidence/" + evidenceId
+ "/content/pda?tenantId=" + tenantId;
}
private static boolean isActive(LoadingEvidenceEntity evidence) {
return evidence != null && (evidence.getStatus() == null || "ACTIVE".equals(evidence.getStatus()));
}
@@ -11,9 +11,24 @@ import org.apache.ibatis.annotations.Update;
@Mapper
public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntity> {
@Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL")
int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Select("""
<script>
SELECT b.*, p.id AS plan_id,
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
THEN TRUE ELSE FALSE END AS exception_active,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DESTINATION_CLEARANCE' LIMIT 1),'MISSING') AS destination_clearance_document_status,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='CUSTOMS_DECLARATION' LIMIT 1),'MISSING') AS customs_declaration_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
@@ -21,9 +36,21 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
) THEN 'WAIT_LOADING'
ELSE p.plan_status END AS plan_status
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
WHERE (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
WHERE b.record_status = 'ACTIVE'
AND (#{query.containerNo} IS NULL OR b.container_no = #{query.containerNo})
AND (#{query.cabinetNo} IS NULL OR b.cabinet_no = #{query.cabinetNo})
AND (#{query.billOfLadingNo} IS NULL OR b.bill_of_lading_no = #{query.billOfLadingNo})
AND (#{query.customerId} IS NULL OR b.customer_id = #{query.customerId} OR EXISTS (
SELECT 1 FROM fl_ocean_booking_container_allocation customer_allocation
JOIN fl_ocean_booking customer_booking ON customer_booking.id = customer_allocation.ocean_booking_id
WHERE customer_allocation.container_booking_id = b.id
AND customer_booking.customer_id = #{query.customerId}
) OR EXISTS (
SELECT 1 FROM fl_loading_plan_item customer_item
WHERE customer_item.loading_plan_id = p.id
AND customer_item.deleted = 0
AND customer_item.account_id = #{query.customerId}
))
AND (#{query.warehouseId} IS NULL OR b.warehouse_id = #{query.warehouseId})
AND (#{query.containerType} IS NULL OR b.container_type = #{query.containerType})
AND (#{query.shippingCompanyId} IS NULL OR b.shipping_company_id = #{query.shippingCompanyId})
@@ -49,7 +76,20 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
Page<ContainerBookingEntity> selectBookingPage(Page<ContainerBookingEntity> page, @Param("query") Query query);
@Select("""
SELECT b.*, p.id AS plan_id,
SELECT b.*, p.id AS plan_id, p.plan_no AS plan_no,
CASE WHEN (SELECT eh.action FROM fl_container_exception_history eh
WHERE eh.container_booking_id=b.id ORDER BY eh.occurred_at DESC,eh.id DESC LIMIT 1)='MARK'
THEN TRUE ELSE FALSE END AS exception_active,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DESTINATION_CLEARANCE' LIMIT 1),'MISSING') AS destination_clearance_document_status,
COALESCE((SELECT CASE WHEN d.completed_at IS NOT NULL THEN 'COMPLETED' ELSE 'UPLOADED' END
FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='CUSTOMS_DECLARATION' LIMIT 1),'MISSING') AS customs_declaration_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='DELIVERY_ORDER' LIMIT 1),'MISSING') AS delivery_order_document_status,
COALESCE((SELECT 'UPLOADED' FROM fl_container_document d WHERE d.container_booking_id=b.id
AND d.document_type='PICKUP_ORDER' LIMIT 1),'MISSING') AS pickup_order_document_status,
CASE WHEN p.id IS NULL THEN 'WAIT_LOADING'
WHEN p.plan_status != 'COMPLETED' AND NOT EXISTS (
SELECT 1 FROM fl_loading_plan_item active_item
@@ -57,13 +97,33 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
) THEN 'WAIT_LOADING'
ELSE p.plan_status END AS plan_status
FROM fl_container_booking b LEFT JOIN fl_loading_plan p ON p.container_booking_id = b.id
WHERE b.id = #{id}
WHERE b.id = #{id} AND b.record_status = 'ACTIVE'
""")
ContainerBookingEntity selectBooking(@Param("id") long id);
@Select("SELECT * FROM fl_container_booking WHERE id=#{id} FOR UPDATE")
ContainerBookingEntity selectBookingForUpdate(@Param("id") long id);
@Select("SELECT * FROM fl_container_booking WHERE id=#{id} AND record_status='ACTIVE' FOR UPDATE")
ContainerBookingEntity selectActiveBookingForUpdate(@Param("id") long id);
@Update("""
UPDATE fl_container_booking SET container_no=NULL, ocean_booking_id=NULL,
ocean_booking_requirement_id=NULL, customer_id=NULL, customer_name=NULL,
bill_of_lading_no=NULL, vessel_name=NULL, voyage_no=NULL,
shipping_company_id=NULL, shipping_company_name=NULL,
departure_port_code=NULL, departure_port_name=NULL,
destination_port_code=NULL, destination_port_name=NULL,
estimated_sailing_at=NULL, estimated_port_arrival_at=NULL,
updated_by=#{operatorId}, updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3), version=version+1
WHERE id=#{targetId} AND version=#{expectedVersion} AND source_type='SIMULATION'
AND record_status='ACTIVE' AND ocean_booking_id IS NOT NULL
""")
int unlinkReleasedBooking(@Param("targetId") long targetId, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_container_booking SET
container_no=#{value.containerNo}, cabinet_no=#{value.cabinetNo},
@@ -74,7 +134,8 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
departure_port_name=#{value.departurePortName}, destination_port_code=#{value.destinationPortCode},
destination_port_name=#{value.destinationPortName}, destination_warehouse_code=#{value.destinationWarehouseCode},
destination_warehouse_name=#{value.destinationWarehouseName}, capacity_cbm=#{value.capacityCbm},
max_weight_kg=#{value.maxWeightKg}, terminal_trucking_company=#{value.terminalTruckingCompany},
max_weight_kg=#{value.maxWeightKg}, terminal_trucking_company_id=#{value.terminalTruckingCompanyId},
terminal_trucking_company=#{value.terminalTruckingCompany},
loading_at=#{value.loadingAt}, estimated_sailing_at=#{value.estimatedSailingAt},
actual_sailing_at=#{value.actualSailingAt}, estimated_port_arrival_at=#{value.estimatedPortArrivalAt},
actual_port_arrival_at=#{value.actualPortArrivalAt},
@@ -87,6 +148,12 @@ public interface ContainerBookingMapper extends BaseMapper<ContainerBookingEntit
""")
int updateDraft(@Param("value") ContainerBookingEntity value, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Select("SELECT COUNT(*) FROM fl_loading_plan WHERE container_booking_id=#{id}") int hasLoadingPlan(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0") int hasActiveLoadingItems(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_pick_task t JOIN fl_loading_plan p ON p.id=t.loading_plan_id WHERE p.container_booking_id=#{id}") int hasPickTask(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_evidence e JOIN fl_loading_plan_item i ON i.inbound_id=e.inbound_id AND i.account_id=e.tenant_id JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND e.status='ACTIVE'") int hasEvidence(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_container_document WHERE container_booking_id=#{id}") int hasDocuments(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_plan_item i JOIN fl_loading_plan p ON p.id=i.loading_plan_id WHERE p.container_booking_id=#{id} AND i.deleted=0 AND i.account_id<>#{customerId}") int hasMismatchedCustomerItems(@Param("id") long id, @Param("customerId") long customerId);
@Update("""
UPDATE fl_container_booking SET container_no=#{containerNo}, voyage_no=#{voyageNo},
@@ -0,0 +1,9 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.ContainerFeeSourceSnapshotEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ContainerFeeSourceSnapshotMapper extends BaseMapper<ContainerFeeSourceSnapshotEntity> {
}
@@ -6,11 +6,13 @@ import java.util.List;
@Mapper public interface ContainerOverviewMapper {
@Select("SELECT * FROM fl_container_document WHERE container_booking_id=#{id} AND document_type IN ('DELIVERY_ORDER','PICKUP_ORDER') ORDER BY document_type") List<ContainerDocumentEntity> documents(@Param("id") long id);
@Select("SELECT * FROM fl_container_transport_history WHERE container_booking_id=#{id} ORDER BY created_at,id") List<ContainerTransportHistoryEntity> transport(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id,operator_name,occurred_at FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY occurred_at,id") List<ExceptionRow> exceptions(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY occurred_at,id") List<ExceptionRow> exceptions(@Param("id") long id);
@Insert("INSERT INTO fl_container_exception_history(id,container_booking_id,action,reason,operator_id,operator_name,operation_id,request_hash,occurred_at) VALUES(#{id},#{bookingId},#{action},#{reason},#{operatorId},#{operatorName},#{operationId},#{requestHash},CURRENT_TIMESTAMP(3))") int insertException(@Param("id") long id,@Param("bookingId") long bookingId,@Param("action") String action,@Param("reason") String reason,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName,@Param("operationId") String operationId,@Param("requestHash") String requestHash);
@Select("SELECT action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") ExceptionRow latestException(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") ExceptionRow latestException(@Param("id") long id);
@Select("SELECT id,action,reason,operator_id operatorId,operator_name operatorName,occurred_at occurredAt,request_hash requestHash FROM fl_container_exception_history WHERE container_booking_id=#{bookingId} AND operator_id=#{operatorId} AND operation_id=#{operationId} LIMIT 1") ExceptionRow exceptionOperation(@Param("bookingId") long bookingId,@Param("operatorId") long operatorId,@Param("operationId") String operationId);
@Select("SELECT * FROM fl_loading_pick_task WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") PickTaskRow pickTask(@Param("id") long id);
@Select("SELECT id,loading_plan_id AS loadingPlanId,container_booking_id AS containerBookingId,"
+ "task_status AS taskStatus,version FROM fl_loading_pick_task "
+ "WHERE container_booking_id=#{id} ORDER BY id DESC LIMIT 1") PickTaskRow pickTask(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_pick_item WHERE pick_task_id=#{id}") int itemCount(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_pick_item WHERE pick_task_id=#{id} AND item_status IN ('PICKED','PARTIAL','SKIPPED')") int completedItemCount(@Param("id") long id);
@Select("SELECT COUNT(*) FROM fl_loading_pick_item WHERE pick_task_id=#{id} AND item_status NOT IN ('PICKED','PARTIAL','SKIPPED')") int unresolvedItemCount(@Param("id") long id);
@@ -11,17 +11,22 @@ import org.apache.ibatis.annotations.Update;
public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
@Update("""
UPDATE fl_inbound_order
SET process_status = 'WAIT_PRICING', measure_review_status = 'APPROVED',
actual_gross_weight_kg = (SELECT COALESCE(SUM(item.total_weight_kg), 0)
FROM fl_inbound_measurement_item item
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
SET process_status = 'WAIT_SPLIT', measure_review_status = 'APPROVED',
actual_gross_weight_kg = (SELECT SUM(weight.gross_weight_kg)
FROM fl_inbound_weight_result weight
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED'),
actual_net_weight_kg = (SELECT SUM(weight.net_weight_kg)
FROM fl_inbound_weight_result weight
WHERE weight.tenant_id = #{tenantId} AND weight.inbound_id = #{inboundId}
AND weight.deleted = 0 AND weight.recognition_status = 'CONFIRMED'),
actual_volume_cbm = (SELECT COALESCE(SUM(item.total_volume_cbm), 0)
FROM fl_inbound_measurement_item item
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
volume_weight_kg = (SELECT COALESCE(SUM(item.volume_weight_kg), 0)
FROM fl_inbound_measurement_item item
WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}),
updated_by = #{operatorId}, version = version + 1
inbounded_at = CURRENT_TIMESTAMP(3), updated_by = #{operatorId}, version = version + 1
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
AND process_status = 'MEASURE_PENDING' AND measure_review_status = 'PENDING'
AND version = #{expectedVersion}
@@ -45,7 +50,8 @@ public interface InboundOrderMapper extends BaseMapper<InboundOrderEntity> {
@Update("""
UPDATE fl_inbound_order
SET process_status = 'WAIT_PRICING', updated_by = #{operatorId}, version = version + 1
SET process_status = 'WAIT_SPLIT', inbounded_at = CURRENT_TIMESTAMP(3),
updated_by = #{operatorId}, version = version + 1
WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0
AND process_status = 'MEASURE_REJECTED' AND measure_review_status = 'REJECTED'
AND version = #{expectedVersion}
@@ -0,0 +1,7 @@
package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.entity.InboundPickupSnapshotEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface InboundPickupSnapshotMapper extends BaseMapper<InboundPickupSnapshotEntity> {
}
@@ -51,8 +51,12 @@ public interface InboundSplitSettlementMapper {
company_id,company_name,warehouse_id,warehouse_name,delivery_warehouse_id,delivery_warehouse_name,business_type,
channel_code,channel_name,carrier_id,carrier_name,tax_mode,pricing_tax_mode,container_mode,customs_inspection,
destination_type,destination_warehouse_id,destination_code,destination_name,hold_flag,current_hold_reason,
#{packageCount},#{packageCount},actual_gross_weight_kg,chargeable_weight_kg,actual_volume_cbm,chargeable_volume_cbm,
pallet_count,volume_weight_kg,actual_net_weight_kg,measure_completed_at,inbounded_at,express_company_code,
#{packageCount},#{packageCount},
actual_gross_weight_kg * #{packageCount} / NULLIF(total_package_count, 0),
chargeable_weight_kg,actual_volume_cbm,chargeable_volume_cbm,
pallet_count,volume_weight_kg,
actual_net_weight_kg * #{packageCount} / NULLIF(total_package_count, 0),
measure_completed_at,inbounded_at,express_company_code,
express_company_name,express_tracking_no,public_remark,#{operatorId},#{operatorName},CURRENT_TIMESTAMP(3),
#{operatorId},CURRENT_TIMESTAMP(3),0,0,'CHILD',#{rootInboundId},#{rootInboundId},#{splitSequence},'NOT_SPLIT',#{splitPurpose}
FROM fl_inbound_order WHERE id=#{parentInboundId} AND tenant_id=#{accountId} AND deleted=0
@@ -124,14 +128,13 @@ public interface InboundSplitSettlementMapper {
INSERT INTO fl_inbound_measurement_item
(id, tenant_id, measurement_id, split_item_id, source_measurement_item_id, inbound_detail_id,
external_detail_no, box_code, item_no, box_specification, package_count, length_cm, width_cm,
height_cm, girth_cm, unit_weight_kg, unit_volume_cbm, volume_raw_cbm, volume_cbm,
total_weight_kg, total_volume_cbm, volume_weight_kg, calculation_rule_version,
height_cm, girth_cm, unit_volume_cbm, volume_raw_cbm, volume_cbm,
total_volume_cbm, volume_weight_kg, calculation_rule_version,
source_image_file_id, machine_no, device_measured_at, over_limit_warning,
outer_length_cm, outer_width_cm, outer_height_cm, remark, created_at, updated_at)
SELECT #{childMeasurementItemId}, tenant_id, #{childMeasurementId}, #{splitItemId}, id, inbound_detail_id,
NULL, NULL, CAST(#{lineNo} AS CHAR), box_specification, #{packageCount}, length_cm, width_cm,
height_cm, girth_cm, unit_weight_kg, unit_volume_cbm, volume_raw_cbm, volume_cbm,
CASE WHEN unit_weight_kg IS NULL THEN NULL ELSE unit_weight_kg * #{packageCount} END,
height_cm, girth_cm, unit_volume_cbm, volume_raw_cbm, volume_cbm,
unit_volume_cbm * #{packageCount}, volume_weight_kg / NULLIF(package_count, 0) * #{packageCount},
calculation_rule_version, source_image_file_id, machine_no, device_measured_at,
over_limit_warning, outer_length_cm, outer_width_cm, outer_height_cm, remark,
@@ -175,7 +178,6 @@ public interface InboundSplitSettlementMapper {
@Update("""
UPDATE fl_inbound_measurement_item
SET package_count=#{packageCount},
total_weight_kg=CASE WHEN unit_weight_kg IS NULL THEN NULL ELSE unit_weight_kg*#{packageCount} END,
total_volume_cbm=unit_volume_cbm*#{packageCount},
volume_weight_kg=volume_weight_kg/NULLIF(package_count,0)*#{packageCount},
updated_at=CURRENT_TIMESTAMP(3)
@@ -278,14 +280,12 @@ public interface InboundSplitSettlementMapper {
UPDATE fl_inbound_order child
JOIN fl_inbound_measurement measurement ON measurement.tenant_id=child.tenant_id
AND measurement.inbound_id=child.id AND measurement.id=#{measurementId}
SET child.actual_gross_weight_kg=(SELECT COALESCE(SUM(item.total_weight_kg),0)
FROM fl_inbound_measurement_item item WHERE item.measurement_id=measurement.id),
child.actual_volume_cbm=(SELECT COALESCE(SUM(item.total_volume_cbm),0)
SET child.actual_volume_cbm=(SELECT COALESCE(SUM(item.total_volume_cbm),0)
FROM fl_inbound_measurement_item item WHERE item.measurement_id=measurement.id),
child.chargeable_weight_kg=NULL,child.chargeable_volume_cbm=NULL,
child.volume_weight_kg=(SELECT COALESCE(SUM(item.volume_weight_kg),0)
FROM fl_inbound_measurement_item item WHERE item.measurement_id=measurement.id),
child.actual_net_weight_kg=NULL,child.pallet_count=0,child.version=child.version+1
child.pallet_count=0,child.version=child.version+1
WHERE child.id=#{childInboundId} AND child.tenant_id=#{accountId} AND child.deleted=0
""")
int normalizeChildMetrics(@Param("accountId") long accountId, @Param("childInboundId") long childInboundId,
@@ -25,12 +25,14 @@ public interface LoadingCompletedEventMapper {
i.product_name_zh, i.product_name_en, i.package_count, i.product_quantity,
i.pallet_count, i.gross_weight_kg, i.net_weight_kg, i.dimensional_weight_kg,
i.volume_cbm, i.fba_tracking_no, i.shipment_no, i.shipping_mark
,i.customer_remark, i.internal_remark, i.warehouse_remark
FROM fl_loading_plan p
JOIN fl_container_booking b ON b.id = p.container_booking_id
JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
JOIN fl_inbound_order o ON o.id = i.inbound_id AND o.tenant_id = i.account_id AND o.deleted = 0
LEFT JOIN fl_inbound_order root_order ON root_order.id = COALESCE(o.root_inbound_id, o.id)
WHERE p.id = #{planId} AND p.plan_status = 'COMPLETED'
AND o.process_status = 'LOADED'
AND i.package_count > 0
ORDER BY i.account_id, i.inbound_id, i.inbound_detail_id, i.id
""")
@@ -71,6 +73,7 @@ public interface LoadingCompletedEventMapper {
private BigDecimal netWeightKg; private BigDecimal dimensionalWeightKg; private BigDecimal volumeCbm;
private String fbaTrackingNo; private String shipmentNo; private String shippingMark;
private String businessType;
private String customerRemark; private String internalRemark; private String warehouseRemark;
}
@Getter @Setter
@@ -21,9 +21,24 @@ import java.util.List;
public interface LoadingListQueryMapper {
String AGGREGATE_COLUMNS = """
p.id AS plan_id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
p.created_by, p.created_by_name, p.created_at, p.version,
p.created_by, p.created_by_name, p.created_at, p.version, p.preload_pushed_at,
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
b.vessel_name, b.voyage_no, b.container_type,
(SELECT COUNT(*) FROM fl_preload_loading_plan_link preload_link
WHERE preload_link.loading_plan_id = p.id) AS preload_link_count,
(SELECT COUNT(*) FROM fl_preload_loading_plan_link confirmed_link
JOIN fl_preload_plan confirmed_preload ON confirmed_preload.id = confirmed_link.preload_plan_id
WHERE confirmed_link.loading_plan_id = p.id
AND confirmed_preload.status = 'CONFIRMED') AS confirmed_preload_count,
(SELECT allocation.ocean_booking_id FROM fl_ocean_booking_container_allocation allocation
WHERE allocation.container_booking_id = p.container_booking_id LIMIT 1) AS ocean_booking_id,
(SELECT ocean.status FROM fl_ocean_booking_container_allocation allocation
JOIN fl_ocean_booking ocean ON ocean.id = allocation.ocean_booking_id
WHERE allocation.container_booking_id = p.container_booking_id LIMIT 1) AS ocean_booking_status,
(SELECT pick_task.id FROM fl_loading_pick_task pick_task
WHERE pick_task.loading_plan_id = p.id LIMIT 1) AS pda_task_id,
(SELECT pick_task.task_status FROM fl_loading_pick_task pick_task
WHERE pick_task.loading_plan_id = p.id LIMIT 1) AS pda_task_status,
COUNT(DISTINCT NULLIF(i.sku_code, '')) AS sku_count,
COALESCE(SUM(i.package_count), 0) AS total_package_count,
COALESCE(SUM(i.gross_weight_kg), 0) AS total_gross_weight_kg,
@@ -34,7 +49,7 @@ public interface LoadingListQueryMapper {
String AGGREGATE_GROUP = """
p.id, p.plan_no, p.container_booking_id, p.plan_status, p.remark,
p.created_by, p.created_by_name, p.created_at, p.version,
p.created_by, p.created_by_name, p.created_at, p.version, p.preload_pushed_at,
b.container_no, b.cabinet_no, b.warehouse_id, b.warehouse_name,
b.vessel_name, b.voyage_no, b.container_type
""";
@@ -51,6 +66,7 @@ public interface LoadingListQueryMapper {
AND EXISTS (SELECT 1 FROM fl_loading_plan_item active_item
WHERE active_item.loading_plan_id = p.id AND active_item.deleted = 0)
<if test="query.status != null and query.status != ''">AND p.plan_status = #{query.status}</if>
<if test="query.customerId != null">AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId})</if>
<if test="query.keyword != null and query.keyword != ''">
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
@@ -74,6 +90,7 @@ public interface LoadingListQueryMapper {
JOIN fl_container_booking b ON b.id = p.container_booking_id
LEFT JOIN fl_loading_plan_item i ON i.loading_plan_id = p.id AND i.deleted = 0
WHERE p.plan_status = 'COMPLETED'
<if test="query.customerId != null">AND EXISTS (SELECT 1 FROM fl_loading_plan_item customer_item WHERE customer_item.loading_plan_id = p.id AND customer_item.deleted = 0 AND customer_item.account_id = #{query.customerId})</if>
<if test="query.keyword != null and query.keyword != ''">
AND (p.plan_no LIKE CONCAT('%', #{query.keyword}, '%')
OR b.container_no LIKE CONCAT('%', #{query.keyword}, '%')
@@ -154,16 +171,19 @@ public interface LoadingListQueryMapper {
@Param("operatorName") String operatorName);
@Select("""
SELECT id AS item_id, account_id, customer_name, inbound_id, inbound_detail_id,
inbound_code, sku_code, destination_warehouse_name, product_name_zh,
SELECT i.id AS item_id, i.account_id, i.customer_name, i.order_id, i.order_no,
i.inbound_id, i.inbound_detail_id, i.inbound_code, b.container_no,
i.sku_code, i.destination_warehouse_name, i.product_name_zh,
product_name_en, product_image_url, package_count, pallet_count,
gross_weight_kg, net_weight_kg, volume_cbm, dimensional_weight_kg,
product_quantity, units_per_carton, fba_tracking_no, shipment_no,
shipping_mark, internal_remark, customer_remark, warehouse_remark,
load_sequence, frozen_at
FROM fl_loading_plan_item
WHERE loading_plan_id = #{planId} AND deleted = 0
ORDER BY load_sequence, id
load_sequence, frozen_at, i.version AS item_version
FROM fl_loading_plan_item i
JOIN fl_loading_plan p ON p.id=i.loading_plan_id
JOIN fl_container_booking b ON b.id=p.container_booking_id AND b.record_status='ACTIVE'
WHERE i.loading_plan_id = #{planId} AND i.deleted = 0
ORDER BY i.load_sequence, i.id
""")
List<ItemRow> selectItems(@Param("planId") long planId);
@@ -200,7 +220,8 @@ public interface LoadingListQueryMapper {
UPDATE fl_loading_plan
SET updated_by = #{operatorId}, updated_by_name = #{operatorName},
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
WHERE id = #{planId} AND plan_status = 'LOADING'
WHERE id = #{planId} AND plan_status IN ('EDITING', 'LOADING')
AND (plan_status = 'LOADING' OR preload_pushed_at IS NOT NULL)
AND version = #{expectedVersion}
""")
int touchEditable(@Param("planId") long planId, @Param("expectedVersion") int expectedVersion,
@@ -212,7 +233,8 @@ public interface LoadingListQueryMapper {
SET b.container_no = #{containerNo}, b.voyage_no = #{voyageNo},
b.updated_by = #{operatorId}, b.updated_by_name = #{operatorName},
b.updated_at = CURRENT_TIMESTAMP(3), b.version = b.version + 1
WHERE p.id = #{planId} AND p.plan_status = 'LOADING'
WHERE p.id = #{planId} AND p.plan_status IN ('EDITING', 'LOADING')
AND (p.plan_status = 'LOADING' OR p.preload_pushed_at IS NOT NULL)
AND (#{containerNo} IS NOT NULL OR NOT EXISTS (
SELECT 1 FROM fl_loading_pick_task t WHERE t.loading_plan_id = p.id))
""")
@@ -220,6 +242,30 @@ public interface LoadingListQueryMapper {
@Param("voyageNo") String voyageNo, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_preload_plan
SET status = 'CONFIRMED', updated_by = #{operatorId}, updated_by_name = #{operatorName},
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
WHERE id = #{preloadPlanId} AND status = #{fromStatus} AND version = #{expectedVersion}
""")
int confirmLinkedPreload(@Param("preloadPlanId") long preloadPlanId,
@Param("fromStatus") String fromStatus,
@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan
SET updated_by = #{operatorId}, updated_by_name = #{operatorName},
updated_at = CURRENT_TIMESTAMP(3), version = version + 1
WHERE id = #{planId} AND plan_status = 'EDITING' AND preload_pushed_at IS NOT NULL
AND version = #{expectedVersion}
""")
int touchForPreloadConfirmation(@Param("planId") long planId,
@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan
SET plan_status = #{toStatus},
@@ -266,6 +312,7 @@ public interface LoadingListQueryMapper {
private String createdByName;
private LocalDateTime createdAt;
private Integer version;
private LocalDateTime preloadPushedAt;
private String containerNo;
private String cabinetNo;
private Long warehouseId;
@@ -273,6 +320,12 @@ public interface LoadingListQueryMapper {
private String vesselName;
private String voyageNo;
private String containerType;
private Long preloadLinkCount;
private Long confirmedPreloadCount;
private Long oceanBookingId;
private String oceanBookingStatus;
private Long pdaTaskId;
private String pdaTaskStatus;
private Long skuCount;
private Long totalPackageCount;
private BigDecimal totalGrossWeightKg;
@@ -287,9 +340,12 @@ public interface LoadingListQueryMapper {
private Long itemId;
private Long accountId;
private String customerName;
private Long orderId;
private String orderNo;
private Long inboundId;
private Long inboundDetailId;
private String inboundCode;
private String containerNo;
private String skuCode;
private String destinationWarehouseName;
private String productNameZh;
@@ -311,6 +367,7 @@ public interface LoadingListQueryMapper {
private String warehouseRemark;
private Integer loadSequence;
private LocalDateTime frozenAt;
private Integer itemVersion;
}
@Getter
@@ -2,6 +2,7 @@ package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -24,7 +25,8 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
JOIN fl_loading_plan plan ON plan.id = task.loading_plan_id
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
WHERE booking.container_no = #{containerNo} AND task.task_status = 'LOADING'
WHERE booking.container_no = #{containerNo} AND booking.record_status = 'ACTIVE'
AND task.task_status = 'LOADING'
AND booking.warehouse_id = #{warehouseId}
LIMIT 1
""")
@@ -33,21 +35,35 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
@Select("SELECT task.* FROM fl_loading_pick_task task JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id "
+ "JOIN fl_container_booking booking ON booking.id=plan.container_booking_id "
+ "WHERE task.task_status='LOADING' AND booking.cabinet_no=#{cabinetNo} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
+ "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' "
+ "AND booking.cabinet_no=#{cabinetNo} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
LoadingPickTaskEntity selectLoadingByCabinet(@Param("cabinetNo") String cabinetNo, @Param("warehouseId") long warehouseId);
@Select("SELECT task.* FROM fl_loading_pick_task task JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id "
+ "JOIN fl_container_booking booking ON booking.id=plan.container_booking_id "
+ "WHERE task.task_status='LOADING' AND task.loading_plan_id=#{loadingPlanId} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
+ "WHERE task.task_status='LOADING' AND booking.record_status='ACTIVE' "
+ "AND task.loading_plan_id=#{loadingPlanId} AND booking.warehouse_id=#{warehouseId} LIMIT 1")
LoadingPickTaskEntity selectLoadingByPlan(@Param("loadingPlanId") long loadingPlanId, @Param("warehouseId") long warehouseId);
@Select("SELECT booking.* FROM fl_loading_pick_task task "
+ "JOIN fl_loading_plan plan ON plan.id=task.loading_plan_id "
+ "JOIN fl_container_booking booking ON booking.id=plan.container_booking_id "
+ "WHERE task.id=#{taskId} AND booking.record_status='ACTIVE'")
ContainerBookingEntity selectActiveBookingByTask(@Param("taskId") long taskId);
@Select("""
<script>
SELECT task.id AS taskId, task.loading_plan_id AS loadingPlanId,
task.container_booking_id AS containerBookingId, task.cabinet_no AS cabinetNo,
booking.container_no AS containerNo, booking.warehouse_id AS warehouseId,
booking.source_type AS sourceType, booking.record_status AS recordStatus,
booking.merged_into_container_booking_id AS mergedIntoContainerBookingId,
booking.warehouse_name AS warehouseName, task.task_status AS taskStatus,
CASE WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED' ELSE 'IN_PROGRESS' END AS workflowStatus,
CASE WHEN task.task_status = 'COMPLETED' THEN 'COMPLETED'
WHEN assignment.pick_task_id IS NULL THEN 'UNASSIGNED'
WHEN SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) = 0
THEN 'AWAITING_REVIEW'
ELSE 'IN_PROGRESS' END AS workflowStatus,
task.version AS version, COALESCE(assignment.version, 0) AS assignmentVersion,
COUNT(pick_item.id) AS itemCount,
SUM(CASE WHEN pick_item.item_status IN ('PENDING', 'PICKING') THEN 1 ELSE 0 END) AS pendingItemCount
@@ -56,7 +72,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
JOIN fl_container_booking booking ON booking.id = plan.container_booking_id
JOIN fl_loading_pick_item pick_item ON pick_item.pick_task_id = task.id
LEFT JOIN fl_loading_pick_assignment assignment ON assignment.pick_task_id = task.id
WHERE task.task_status = 'LOADING'
WHERE task.task_status = 'LOADING' AND booking.record_status = 'ACTIVE'
AND (assignment.pick_task_id IS NULL OR assignment.assignee_id = #{operatorId})
<if test="keyword != null and keyword != ''">
AND (booking.container_no LIKE CONCAT('%', #{keyword}, '%')
@@ -64,6 +80,7 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
</if>
GROUP BY task.id, task.loading_plan_id, task.container_booking_id, task.cabinet_no,
booking.container_no, booking.warehouse_id,
booking.source_type, booking.record_status, booking.merged_into_container_booking_id,
booking.warehouse_name, task.task_status, task.version,
assignment.pick_task_id, assignment.version
ORDER BY task.id DESC
@@ -79,6 +96,9 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
private String containerNo;
private Long containerBookingId;
private String cabinetNo;
private String sourceType;
private String recordStatus;
private Long mergedIntoContainerBookingId;
private Long warehouseId;
private String warehouseName;
private String taskStatus;
@@ -98,6 +118,12 @@ public interface LoadingPickTaskMapper extends BaseMapper<LoadingPickTaskEntity>
public void setContainerBookingId(Long value) { this.containerBookingId = value; }
public String getCabinetNo() { return cabinetNo; }
public void setCabinetNo(String value) { this.cabinetNo = value; }
public String getSourceType() { return sourceType; }
public void setSourceType(String sourceType) { this.sourceType = sourceType; }
public String getRecordStatus() { return recordStatus; }
public void setRecordStatus(String recordStatus) { this.recordStatus = recordStatus; }
public Long getMergedIntoContainerBookingId() { return mergedIntoContainerBookingId; }
public void setMergedIntoContainerBookingId(Long value) { this.mergedIntoContainerBookingId = value; }
public Long getWarehouseId() { return warehouseId; }
public void setWarehouseId(Long warehouseId) { this.warehouseId = warehouseId; }
public String getWarehouseName() { return warehouseName; }
@@ -9,4 +9,21 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl
AND version=#{expectedVersion}
""")
int removeEditing(@Param("planId") long planId,@Param("itemId") long itemId,@Param("expectedVersion") int expectedVersion,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan_item i
SET i.warehouse_remark=#{remark}, i.updated_by=#{operatorId}, i.updated_by_name=#{operatorName},
i.updated_at=CURRENT_TIMESTAMP(3), i.version=i.version+1
WHERE i.id=#{itemId} AND i.loading_plan_id=#{planId} AND i.deleted=0 AND i.frozen_at IS NULL
AND i.version=#{expectedItemVersion}
AND EXISTS (
SELECT 1 FROM fl_loading_plan p
WHERE p.id=#{planId} AND p.version=#{touchedPlanVersion}
AND p.plan_status IN ('EDITING','LOADING')
)
""")
int updateWarehouseRemark(@Param("planId") long planId, @Param("itemId") long itemId,
@Param("touchedPlanVersion") int touchedPlanVersion,
@Param("expectedItemVersion") int expectedItemVersion,
@Param("remark") String remark, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
}
@@ -20,16 +20,4 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baiyue.firstl
WHERE id=#{planId} AND plan_status='EDITING' AND preload_pushed_at IS NOT NULL
""")
int startPicking(@Param("planId") long planId,@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_loading_plan plan SET plan_status='EDITING',updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE plan.id=#{planId} AND plan.plan_status='LOADING' AND plan.version=#{expectedVersion}
AND NOT EXISTS (
SELECT 1 FROM fl_loading_pick_item pick_item
JOIN fl_loading_pick_task pick_task ON pick_task.id=pick_item.pick_task_id
WHERE pick_task.loading_plan_id=plan.id AND pick_item.picked_package_count>0
)
""")
int recycleUnpicked(@Param("planId") long planId,@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId,@Param("operatorName") String operatorName);
}
@@ -3,6 +3,7 @@ package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.RateRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelPrefixRow;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@@ -12,6 +13,15 @@ import java.util.List;
/** Dedicated read model for merchant channel selection and reference estimates. */
@Mapper
public interface PriceBookQueryMapper {
@Select("""
SELECT channel_code AS channelCode, inbound_code_prefix AS inboundCodePrefix,
overseas_inbound_code_prefix AS overseasInboundCodePrefix
FROM fl_price_channel_catalog
WHERE channel_code = #{channelCode} AND status = 'ACTIVE'
LIMIT 1
""")
ChannelPrefixRow findChannelPrefix(@Param("channelCode") String channelCode);
@Select("""
SELECT id
FROM fl_price_book_version
@@ -12,10 +12,12 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
-COALESCE((SELECT SUM(split_item.package_count)
FROM fl_inbound_split_item split_item
WHERE o.inbound_role='MASTER'
WHERE o.inbound_role='MASTER'
AND split_item.tenant_id=o.tenant_id
AND split_item.parent_inbound_id=o.id
AND split_item.source_measurement_item_id=i.id),0) availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
AND split_item.source_measurement_item_id=i.id),0) availablePackageCount,
COALESCE(o.actual_gross_weight_kg / NULLIF(o.total_package_count, 0), 0) unitGrossWeightKg,
COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg,
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
@@ -24,6 +26,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
WHERE o.warehouse_id=#{warehouseId} AND o.process_status='WAIT_SPLIT'
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
AND i.inbound_detail_id IS NOT NULL
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
@@ -53,7 +56,9 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
WHERE o.inbound_role='MASTER'
AND split_item.tenant_id=o.tenant_id
AND split_item.parent_inbound_id=o.id
AND split_item.source_measurement_item_id=i.id),0) availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
AND split_item.source_measurement_item_id=i.id),0) availablePackageCount,
COALESCE(o.actual_gross_weight_kg / NULLIF(o.total_package_count, 0), 0) unitGrossWeightKg,
COALESCE(o.actual_net_weight_kg / NULLIF(o.total_package_count, 0), 0) unitNetWeightKg,
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
FROM fl_inbound_order o
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
@@ -62,6 +67,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
WHERE o.warehouse_id=#{warehouseId} AND o.id=#{inboundId} AND o.process_status='WAIT_SPLIT'
AND o.actual_gross_weight_kg IS NOT NULL AND o.actual_net_weight_kg IS NOT NULL
AND (o.inbound_role='MASTER' OR (o.split_purpose='REMAINDER' AND o.split_status='NOT_SPLIT'))
AND i.inbound_detail_id=#{detailId}
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
@@ -1,8 +1,11 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baiyue.firstleg.dto.TruckAppointmentDtos.Query; import com.baiyue.firstleg.entity.TruckAppointmentEntity; import org.apache.ibatis.annotations.*;
@Mapper public interface TruckAppointmentMapper extends BaseMapper<TruckAppointmentEntity>{
@Select("<script>SELECT * FROM fl_truck_appointment WHERE (#{q.keyword} IS NULL OR appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR vehicle_no LIKE CONCAT('%',#{q.keyword},'%') OR carrier_name LIKE CONCAT('%',#{q.keyword},'%')) AND (#{q.status} IS NULL OR status=#{q.status}) AND (#{q.containerBookingId} IS NULL OR container_booking_id=#{q.containerBookingId}) ORDER BY updated_at DESC,id DESC</script>") Page<TruckAppointmentEntity> page(Page<TruckAppointmentEntity> p,@Param("q") Query q);
@Select("SELECT * FROM fl_truck_appointment WHERE container_booking_id=#{containerBookingId} AND status NOT IN ('COMPLETED','CANCELLED','EXCEPTION') ORDER BY id FOR UPDATE") java.util.List<TruckAppointmentEntity> activeForUpdate(@Param("containerBookingId") long containerBookingId);
@Select("<script>SELECT a.*,o.booking_no ocean_booking_no,COALESCE(o.warehouse_name,c.warehouse_name) warehouse_name,o.shipping_company_id,o.shipping_company_name,o.booking_agent_id,o.booking_agent_name,c.cabinet_no,c.container_no,(SELECT COUNT(*) FROM fl_truck_appointment_attachment x WHERE x.appointment_id=a.id) attachment_count FROM fl_truck_appointment a LEFT JOIN fl_ocean_booking o ON o.id=a.ocean_booking_id LEFT JOIN fl_container_booking c ON c.id=a.container_booking_id AND c.record_status='ACTIVE' WHERE (#{q.keyword} IS NULL OR a.appointment_no LIKE CONCAT('%',#{q.keyword},'%') OR a.vehicle_no LIKE CONCAT('%',#{q.keyword},'%') OR a.carrier_name LIKE CONCAT('%',#{q.keyword},'%')) AND (#{q.status} IS NULL OR a.status=#{q.status}) AND (#{q.customerId} IS NULL OR a.customer_id=#{q.customerId}) AND (#{q.containerBookingId} IS NULL OR a.container_booking_id=#{q.containerBookingId}) ORDER BY a.updated_at DESC,a.id DESC</script>") Page<TruckAppointmentEntity> page(Page<TruckAppointmentEntity> p,@Param("q") Query q);
@Update("UPDATE fl_truck_appointment SET status=#{toStatus}, actual_arrived_warehouse_at=CASE WHEN #{toStatus}='ARRIVED_WAREHOUSE' THEN CURRENT_TIMESTAMP(3) ELSE actual_arrived_warehouse_at END, actual_loading_at=CASE WHEN #{toStatus}='LOADING' THEN CURRENT_TIMESTAMP(3) ELSE actual_loading_at END, actual_departed_at=CASE WHEN #{toStatus}='DEPARTED' THEN CURRENT_TIMESTAMP(3) ELSE actual_departed_at END, revision=revision+1, version=version+1, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int transition(@Param("id") long id,@Param("fromStatus") String from,@Param("toStatus") String to,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET status='DISPATCHED',bill_of_lading_no=#{billOfLadingNo},revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int dispatch(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("billOfLadingNo") String billOfLadingNo,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET status='COMPLETED',actual_loading_at=COALESCE(actual_loading_at,CURRENT_TIMESTAMP(3)),revision=revision+1,version=version+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND status=#{fromStatus} AND version=#{version}") int completeLoadingFromPda(@Param("id") long id,@Param("fromStatus") String fromStatus,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET planned_loading_at=#{at}, revision=revision+1, version=version+1, status='RESCHEDULED', remark=#{reason}, updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{id} AND version=#{version}") int reschedule(@Param("id") long id,@Param("at") java.time.LocalDateTime at,@Param("reason") String reason,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET vehicle_no=#{e.vehicleNo},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},payable_amount=#{e.payableAmount},receivable_amount=#{e.receivableAmount},planned_loading_at=#{e.plannedLoadingAt},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version);
@Update("UPDATE fl_truck_appointment SET outbound_order_id=#{e.outboundOrderId},ocean_booking_id=#{e.oceanBookingId},container_booking_id=#{e.containerBookingId},loading_plan_id=#{e.loadingPlanId},warehouse_id=#{e.warehouseId},customer_id=#{e.customerId},customer_name=#{e.customerName},carrier_id=#{e.carrierId},carrier_name=#{e.carrierName},bill_of_lading_no=#{e.billOfLadingNo},fleet_contact=#{e.fleetContact},fleet_phone=#{e.fleetPhone},driver_name=#{e.driverName},driver_phone=#{e.driverPhone},driver_id_card=#{e.driverIdCard},vehicle_no=#{e.vehicleNo},store_name=#{e.storeName},store_contact=#{e.storeContact},store_phone=#{e.storePhone},store_address=#{e.storeAddress},store_city=#{e.storeCity},store_zipcode=#{e.storeZipcode},package_count=#{e.packageCount},gross_weight_kg=#{e.grossWeightKg},volume_cbm=#{e.volumeCbm},pallet_count=#{e.palletCount},po_no=#{e.poNo},planned_loading_at=#{e.plannedLoadingAt},transit_minutes=#{e.transitMinutes},payable_amount=#{e.payableAmount},receivable_amount=#{e.receivableAmount},currency=#{e.currency},remark=#{e.remark},version=version+1,revision=revision+1,updated_at=CURRENT_TIMESTAMP(3) WHERE id=#{e.id} AND version=#{version}") int updateEdit(@Param("e") TruckAppointmentEntity e,@Param("version") int version);
}
@@ -0,0 +1,29 @@
package com.baiyue.firstleg.mapper;
import com.baiyue.firstleg.dto.CurrentContainerTransport;
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 UnifiedInboundTransportMapper {
@Select("""
<script>
SELECT DISTINCT i.inbound_id, i.account_id, p.container_booking_id,
b.container_no, b.cabinet_no, b.vessel_name, b.voyage_no,
b.estimated_sailing_at, b.actual_sailing_at,
b.estimated_port_arrival_at, b.actual_port_arrival_at
FROM fl_loading_plan_item i
JOIN fl_loading_plan p ON p.id = i.loading_plan_id
JOIN fl_container_booking b ON b.id = p.container_booking_id
WHERE i.deleted = 0
AND p.plan_status IN ('LOADING', 'COMPLETED')
AND i.inbound_id IN
<foreach collection="inboundIds" item="id" open="(" separator="," close=")">#{id}</foreach>
</script>
""")
List<CurrentContainerTransport> findCurrentByInboundIds(@Param("inboundIds") List<Long> inboundIds);
}
@@ -16,6 +16,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Validated
@RestController
@@ -26,40 +27,25 @@ public class PlatformOceanBookingController {
@GetMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<PageResult<OceanBookingEntity>> page(@AuthenticationPrincipal AuthenticatedUser user, @Valid Query query) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.page(query)); }
@GetMapping("/channels") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<List<ChannelOption>> channels(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long originWarehouseId) {
FirstlegScopeGuard.platform(user); return ApiResponse.success(service.channels(originWarehouseId));
}
@GetMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:list')")
public ApiResponse<Detail> detail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.detail(oceanBookingId)); }
@PostMapping @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:create')")
@BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="创建海运订舱单", captureBefore=false)
public ApiResponse<Detail> create(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateRequest request) { FirstlegScopeGuard.platform(user, request.data().customerId()); return ApiResponse.success(service.create(user.userId(), user.username(), key, request)); }
@BusinessAudit(event="OCEAN_BOOKING_CREATE", resourceType="OCEAN_BOOKING", description="创建海运订舱草稿", captureBefore=false)
public ApiResponse<Detail> create(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CreateRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.create(user.userId(), user.username(), key, request)); }
@PutMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:edit')")
@BusinessAudit(event="OCEAN_BOOKING_UPDATE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="编辑海运订舱草稿")
public ApiResponse<Detail> updateDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpdateDraftRequest request) { FirstlegScopeGuard.platform(user, request.data().customerId()); return ApiResponse.success(service.updateDraft(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/submit") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:submit')")
@BusinessAudit(event="OCEAN_BOOKING_SUBMIT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="提交海运订舱")
public ApiResponse<Detail> submit(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.submit(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认海运订舱")
public ApiResponse<Detail> confirm(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirm(oceanBookingId, user.userId(), user.username(), key, request)); }
@BusinessAudit(event="OCEAN_BOOKING_UPDATE_DRAFT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="编辑海运订舱草稿")
public ApiResponse<Detail> updateDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody UpdateRequest request) { validateOptionalCustomer(user, request.data().customerId()); return ApiResponse.success(service.updateDraft(oceanBookingId, user.userId(), user.username(), key, request)); }
@DeleteMapping("/{oceanBookingId}") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:delete')")
@BusinessAudit(event="OCEAN_BOOKING_DELETE_DRAFT", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="删除海运订舱草稿")
public ApiResponse<Void> deleteDraft(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); service.deleteDraft(oceanBookingId, user.userId(), user.username(), key, request); return ApiResponse.success(null); }
@PostMapping("/{oceanBookingId}/release") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:release')")
@BusinessAudit(event="OCEAN_BOOKING_RELEASE", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认放舱")
public ApiResponse<Detail> release(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ReleaseRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.release(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:confirm')")
@BusinessAudit(event="OCEAN_BOOKING_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记海运订舱失败")
public ApiResponse<Detail> fail(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.fail(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/cancel") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:cancel')")
@BusinessAudit(event="OCEAN_BOOKING_CANCEL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="取消海运订舱")
public ApiResponse<Detail> cancel(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.cancel(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-request") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_REQUEST", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="申请变更船期")
public ApiResponse<Detail> requestChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ChangeRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.requestChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-confirm") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_CONFIRM", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="确认变更船期")
public ApiResponse<Detail> confirmChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.confirmChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/change-fail") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:change')")
@BusinessAudit(event="OCEAN_BOOKING_CHANGE_FAIL", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="标记变更船期失败")
public ApiResponse<Detail> failChange(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody ActionRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.failChange(oceanBookingId, user.userId(), user.username(), key, request)); }
@PostMapping("/{oceanBookingId}/allocations") @PreAuthorize("hasAuthority('platform:firstleg:ocean-booking:allocate')")
@BusinessAudit(event="OCEAN_BOOKING_ALLOCATE_CONTAINER", resourceType="OCEAN_BOOKING", resourceId="#oceanBookingId", description="分配订舱柜次")
public ApiResponse<Detail> allocate(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Positive long oceanBookingId, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody AllocationRequest request) { FirstlegScopeGuard.platform(user); return ApiResponse.success(service.allocate(oceanBookingId, user.userId(), user.username(), key, request)); }
private static void validateOptionalCustomer(AuthenticatedUser user, Long customerId) { FirstlegScopeGuard.platform(user); if (customerId != null) FirstlegScopeGuard.platform(user, customerId); }
}
@@ -2,10 +2,5 @@ package com.baiyue.firstleg.oceanbooking.domain;
public enum OceanBookingStatus {
DRAFT,
SUBMITTED,
CONFIRMED,
RELEASED,
FAILED,
CHANGE_PENDING,
CANCELLED
RELEASED
}
@@ -1,9 +1,8 @@
package com.baiyue.firstleg.oceanbooking.dto;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingPartyEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerRequirementEntity;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingStatusHistoryEntity;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
@@ -34,17 +33,14 @@ public final class OceanBookingDtos {
@Size(max = 64) String contactPhone) {}
public record ContainerRequirementRequest(@NotBlank @Size(max = 32) String containerType,
@NotBlank @Size(max = 32) String containerKind,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg,
@NotNull @Positive Integer quantity) {}
public record DraftData(@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@NotBlank @Size(max = 64) String channelCode,
@NotBlank @Size(max = 128) String channelName,
@NotNull @Positive Long warehouseId,
@NotBlank @Size(max = 128) String warehouseName,
@NotBlank @Size(max = 32) String containerKind,
@NotNull @Min(1) @Max(100) Integer quantity,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal capacityCbm,
@NotNull @jakarta.validation.constraints.DecimalMin(value = "0", inclusive = false) BigDecimal maxWeightKg) {}
public record DraftData(@Positive Long customerId,
@Size(max = 128) String customerName,
@Size(max = 64) String channelCode,
@Positive Long warehouseId,
@Size(max = 128) String billOfLadingNo,
@Positive Long shippingCompanyId,
@Size(max = 128) String shippingCompanyName,
@@ -62,34 +58,29 @@ public final class OceanBookingDtos {
LocalDateTime estimatedSailingAt,
LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String remark,
@Valid @NotEmpty @Size(max = 3) List<PartyRequest> parties,
@Valid @NotEmpty @Size(max = 20) List<ContainerRequirementRequest> containerRequirements) {}
@Valid @Size(max = 3) List<PartyRequest> parties,
@Valid @Size(max = 20) List<ContainerRequirementRequest> containerRequirements) {}
public record CreateRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record UpdateDraftRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record UpdateRequest(@NotNull @Min(0) Integer expectedVersion,
@Valid @NotNull DraftData data) {}
public record ActionRequest(@NotNull @Min(0) Integer expectedVersion,
@Size(max = 1000) String reason) {}
public record ReleaseRequest(@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String billOfLadingNo,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
@NotNull LocalDateTime estimatedSailingAt,
@NotNull LocalDateTime estimatedArrivalAt,
@Size(max = 1000) String reason) {}
public record ChangeRequest(@NotNull @Min(0) Integer expectedVersion,
@NotBlank @Size(max = 128) String vesselName,
@NotBlank @Size(max = 64) String voyageNo,
LocalDateTime estimatedSailingAt,
LocalDateTime estimatedArrivalAt,
@NotBlank @Size(max = 1000) String reason) {}
public record AllocationRequest(@NotNull @Positive Long requirementId,
@NotNull @Positive Long containerBookingId,
@NotNull @Min(0) Integer expectedVersion) {}
public record RealContainer(Long containerBookingId, String cabinetNo, String containerNo,
Long requirementId, Long allocationId, String containerType,
String containerKind, BigDecimal capacityCbm, BigDecimal maxWeightKg,
Long warehouseId, String warehouseName, String channelCode,
String channelName, Long customerId, String customerName) {}
public record Detail(OceanBookingEntity oceanBooking,
List<OceanBookingPartyEntity> parties,
List<OceanBookingContainerRequirementEntity> containerRequirements,
List<OceanBookingContainerAllocationEntity> containerAllocations,
List<RealContainer> containers,
List<OceanBookingStatusHistoryEntity> histories) {}
public record ChannelOption(Long priceBookVersionId, String channelCode, String channelName,
String priceModel, List<String> pricingTaxModes) {}
}
@@ -40,13 +40,6 @@ public class OceanBookingEntity {
private String freightTerm;
private LocalDateTime estimatedSailingAt;
private LocalDateTime estimatedArrivalAt;
private String pendingVesselName;
private String pendingVoyageNo;
private LocalDateTime pendingEstimatedSailingAt;
private LocalDateTime pendingEstimatedArrivalAt;
private String changeReason;
private String failureReason;
private String cancellationReason;
private String remark;
private Long createdBy;
private String createdByName;
@@ -55,4 +48,7 @@ public class OceanBookingEntity {
private String updatedByName;
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt;
@Version @TableField(fill = FieldFill.INSERT) private Integer version;
@TableField(exist = false) private String shipperName;
@TableField(exist = false) private String consigneeName;
@TableField(exist = false) private Long containerCount;
}
@@ -2,4 +2,9 @@ package com.baiyue.firstleg.oceanbooking.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingContainerAllocationEntity;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper<OceanBookingContainerAllocationEntity> {}
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
@Mapper public interface OceanBookingContainerAllocationMapper extends BaseMapper<OceanBookingContainerAllocationEntity> {
@Update("UPDATE fl_ocean_booking_container_allocation SET container_booking_id=#{targetId} WHERE container_booking_id=#{sourceId}")
int reassign(@Param("sourceId") long sourceId, @Param("targetId") long targetId);
}
@@ -5,103 +5,50 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query;
import com.baiyue.firstleg.oceanbooking.entity.OceanBookingEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
@Mapper
public interface OceanBookingMapper extends BaseMapper<OceanBookingEntity> {
@Update("UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},channel_code=#{value.channelCode},channel_name=#{value.channelName},warehouse_id=#{value.warehouseId},warehouse_name=#{value.warehouseName},bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},departure_port_code=#{value.departurePortCode},departure_port_name=#{value.departurePortName},destination_port_code=#{value.destinationPortCode},destination_port_name=#{value.destinationPortName},place_of_delivery=#{value.placeOfDelivery},service_term=#{value.serviceTerm},freight_term=#{value.freightTerm},estimated_sailing_at=#{value.estimatedSailingAt},estimated_arrival_at=#{value.estimatedArrivalAt},remark=#{value.remark},updated_by=#{operatorId},updated_by_name=#{operatorName},version=version+1,updated_at=NOW() WHERE id=#{id} AND status='DRAFT' AND version=#{version}")
int updateDraft(@Param("id") long id, @Param("value") OceanBookingEntity value, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Delete("DELETE FROM fl_ocean_booking WHERE id=#{id} AND status='DRAFT' AND version=#{version}")
int deleteDraft(@Param("id") long id, @Param("version") int version);
@Select("""
<script>
SELECT * FROM fl_ocean_booking
WHERE (#{query.customerId} IS NULL OR customer_id=#{query.customerId})
AND (#{query.status} IS NULL OR status=#{query.status})
AND (#{query.keyword} IS NULL OR booking_no LIKE CONCAT('%',#{query.keyword},'%')
OR customer_name LIKE CONCAT('%',#{query.keyword},'%')
OR bill_of_lading_no LIKE CONCAT('%',#{query.keyword},'%')
OR vessel_name LIKE CONCAT('%',#{query.keyword},'%')
OR voyage_no LIKE CONCAT('%',#{query.keyword},'%'))
ORDER BY updated_at DESC,id DESC
SELECT booking.*,
(SELECT party.party_name FROM fl_ocean_booking_party party
WHERE party.ocean_booking_id=booking.id AND party.party_type='SHIPPER' LIMIT 1) shipper_name,
(SELECT party.party_name FROM fl_ocean_booking_party party
WHERE party.ocean_booking_id=booking.id AND party.party_type='CONSIGNEE' LIMIT 1) consignee_name,
(SELECT COUNT(*) FROM fl_ocean_booking_container_allocation allocation
JOIN fl_container_booking container ON container.id=allocation.container_booking_id
WHERE allocation.ocean_booking_id=booking.id AND container.record_status='ACTIVE') container_count
FROM fl_ocean_booking booking
WHERE (#{query.customerId} IS NULL OR booking.customer_id=#{query.customerId})
AND booking.status IN ('DRAFT','RELEASED')
AND (#{query.status} IS NULL OR booking.status=#{query.status})
AND (#{query.keyword} IS NULL OR booking.booking_no LIKE CONCAT('%',#{query.keyword},'%')
OR booking.customer_name LIKE CONCAT('%',#{query.keyword},'%')
OR booking.bill_of_lading_no LIKE CONCAT('%',#{query.keyword},'%')
OR booking.vessel_name LIKE CONCAT('%',#{query.keyword},'%')
OR booking.voyage_no LIKE CONCAT('%',#{query.keyword},'%'))
ORDER BY booking.updated_at DESC,booking.id DESC
</script>
""")
Page<OceanBookingEntity> selectPage(Page<OceanBookingEntity> page, @Param("query") Query query);
@Update("""
UPDATE fl_ocean_booking SET customer_id=#{value.customerId},customer_name=#{value.customerName},
channel_code=#{value.channelCode},channel_name=#{value.channelName},warehouse_id=#{value.warehouseId},warehouse_name=#{value.warehouseName},
bill_of_lading_no=#{value.billOfLadingNo},shipping_company_id=#{value.shippingCompanyId},
shipping_company_name=#{value.shippingCompanyName},booking_agent_id=#{value.bookingAgentId},
booking_agent_name=#{value.bookingAgentName},vessel_name=#{value.vesselName},voyage_no=#{value.voyageNo},
departure_port_code=#{value.departurePortCode},departure_port_name=#{value.departurePortName},
destination_port_code=#{value.destinationPortCode},destination_port_name=#{value.destinationPortName},
place_of_delivery=#{value.placeOfDelivery},service_term=#{value.serviceTerm},freight_term=#{value.freightTerm},
estimated_sailing_at=#{value.estimatedSailingAt},estimated_arrival_at=#{value.estimatedArrivalAt},
remark=#{value.remark},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{value.id} AND status='DRAFT' AND version=#{expectedVersion}
""")
int updateDraft(@Param("value") OceanBookingEntity value, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status=#{toStatus},failure_reason=#{failureReason},
cancellation_reason=#{cancellationReason},updated_by=#{operatorId},updated_by_name=#{operatorName},
UPDATE fl_ocean_booking SET status=#{toStatus},
updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status=#{fromStatus} AND version=#{expectedVersion}
""")
int transition(@Param("oceanBookingId") long oceanBookingId, @Param("fromStatus") String fromStatus,
@Param("toStatus") String toStatus, @Param("failureReason") String failureReason,
@Param("cancellationReason") String cancellationReason,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("toStatus") String toStatus, @Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='RELEASED',bill_of_lading_no=#{billOfLadingNo},
vessel_name=#{vesselName},voyage_no=#{voyageNo},estimated_sailing_at=#{estimatedSailingAt},
estimated_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CONFIRMED' AND version=#{expectedVersion}
""")
int release(@Param("oceanBookingId") long oceanBookingId, @Param("billOfLadingNo") String billOfLadingNo,
@Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo,
@Param("estimatedSailingAt") LocalDateTime estimatedSailingAt,
@Param("estimatedArrivalAt") LocalDateTime estimatedArrivalAt,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='CHANGE_PENDING',pending_vessel_name=#{vesselName},
pending_voyage_no=#{voyageNo},pending_estimated_sailing_at=#{estimatedSailingAt},
pending_estimated_arrival_at=#{estimatedArrivalAt},change_reason=#{reason},
updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CONFIRMED' AND version=#{expectedVersion}
""")
int requestChange(@Param("oceanBookingId") long oceanBookingId, @Param("vesselName") String vesselName,
@Param("voyageNo") String voyageNo, @Param("estimatedSailingAt") LocalDateTime estimatedSailingAt,
@Param("estimatedArrivalAt") LocalDateTime estimatedArrivalAt, @Param("reason") String reason,
@Param("expectedVersion") int expectedVersion, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET status='CONFIRMED',vessel_name=pending_vessel_name,voyage_no=pending_voyage_no,
estimated_sailing_at=pending_estimated_sailing_at,estimated_arrival_at=pending_estimated_arrival_at,
pending_vessel_name=NULL,pending_voyage_no=NULL,pending_estimated_sailing_at=NULL,
pending_estimated_arrival_at=NULL,updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3),version=version+1
WHERE id=#{oceanBookingId} AND status='CHANGE_PENDING' AND version=#{expectedVersion}
""")
int confirmChange(@Param("oceanBookingId") long oceanBookingId, @Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
@Update("""
UPDATE fl_ocean_booking SET version=version+1,updated_by=#{operatorId},updated_by_name=#{operatorName},
updated_at=CURRENT_TIMESTAMP(3)
WHERE id=#{oceanBookingId} AND version=#{expectedVersion}
AND status IN ('SUBMITTED','CONFIRMED','CHANGE_PENDING')
""")
int touchForAllocation(@Param("oceanBookingId") long oceanBookingId,
@Param("expectedVersion") int expectedVersion,
@Param("operatorId") long operatorId, @Param("operatorName") String operatorName);
}
@@ -19,7 +19,7 @@ public interface OceanBookingOperationMapper extends BaseMapper<OceanBookingOper
@Update("UPDATE fl_ocean_booking_operation SET ocean_booking_id=#{oceanBookingId},status='SUCCESS',"
+ "result_json=#{resultJson},completed_at=CURRENT_TIMESTAMP(3) WHERE operator_id=#{operatorId} "
+ "AND operation=#{operation} AND idempotency_key=#{key} AND status='PROCESSING'")
int complete(@Param("oceanBookingId") long oceanBookingId, @Param("operatorId") long operatorId,
int complete(@Param("oceanBookingId") Long oceanBookingId, @Param("operatorId") long operatorId,
@Param("operation") String operation, @Param("key") String key,
@Param("resultJson") String resultJson);
}
@@ -3,18 +3,19 @@ package com.baiyue.firstleg.oceanbooking.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.mapper.ContainerBookingMapper;
import com.baiyue.firstleg.mapper.LoadingPlanMapper;
import com.baiyue.firstleg.entity.LoadingPlanEntity;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.Query;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.ReleaseRequest;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.RealContainer;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.mapper.*;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.repository.ContainerRepository;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Repository
public class OceanBookingRepository {
@@ -24,55 +25,60 @@ public class OceanBookingRepository {
@Resource private OceanBookingContainerAllocationMapper allocations;
@Resource private OceanBookingStatusHistoryMapper histories;
@Resource private OceanBookingOperationMapper operations;
@Resource private ContainerBookingMapper containerBookings;
@Resource private LoadingPlanMapper loadingPlans;
@Resource private ContainerRepository containerBookings;
public PageResult<OceanBookingEntity> page(Query query) {
return PageResult.from(bookings.selectPage(new Page<>(query.page(), query.size()), query));
}
public OceanBookingEntity find(long id) { return bookings.selectById(id); }
public void insert(OceanBookingEntity value) { bookings.insert(value); }
public int transition(long id, String from, String to, int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, version, operatorId, operatorName);
}
public int updateDraft(OceanBookingEntity value, int version, long operatorId, String operatorName) {
return bookings.updateDraft(value, version, operatorId, operatorName);
return bookings.updateDraft(value.getId(), value, version, operatorId, operatorName);
}
public int transition(long id, String from, String to, String failureReason, String cancellationReason,
int version, long operatorId, String operatorName) {
return bookings.transition(id, from, to, failureReason, cancellationReason, version, operatorId, operatorName);
}
public int release(long id, ReleaseRequest request, long operatorId, String operatorName) {
return bookings.release(id, request.billOfLadingNo().trim(), request.vesselName().trim(),
request.voyageNo().trim(), request.estimatedSailingAt(), request.estimatedArrivalAt(),
request.expectedVersion(), operatorId, operatorName);
}
public int requestChange(long id, OceanBookingEntity pending, String reason, int version,
long operatorId, String operatorName) {
return bookings.requestChange(id, pending.getPendingVesselName(), pending.getPendingVoyageNo(),
pending.getPendingEstimatedSailingAt(), pending.getPendingEstimatedArrivalAt(), reason,
version, operatorId, operatorName);
}
public int confirmChange(long id, int version, long operatorId, String operatorName) {
return bookings.confirmChange(id, version, operatorId, operatorName);
}
public int touchForAllocation(long id, int version, long operatorId, String operatorName) {
return bookings.touchForAllocation(id, version, operatorId, operatorName);
public int deleteDraft(long id, int version) {
return bookings.deleteDraft(id, version);
}
public List<OceanBookingPartyEntity> parties(long id) { return parties.selectList(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id).orderByAsc(OceanBookingPartyEntity::getId)); }
public List<OceanBookingContainerRequirementEntity> requirements(long id) { return requirements.selectList(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerRequirementEntity::getSortNo)); }
public List<OceanBookingContainerAllocationEntity> allocations(long id) { return allocations.selectList(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getOceanBookingId, id).orderByAsc(OceanBookingContainerAllocationEntity::getAllocatedAt)); }
public List<RealContainer> containers(long id) {
Map<Long, OceanBookingContainerRequirementEntity> requirementById = requirements(id).stream()
.collect(Collectors.toMap(OceanBookingContainerRequirementEntity::getId, Function.identity()));
return allocations(id).stream().map(allocation -> {
OceanBookingContainerRequirementEntity requirement = requirementById.get(allocation.getRequirementId());
ContainerBookingEntity container = containerBookings.find(allocation.getContainerBookingId());
if (requirement == null || container == null) {
throw new IllegalStateException("订舱真实柜聚合关系不完整");
}
return new RealContainer(container.getId(), container.getCabinetNo(), container.getContainerNo(),
requirement.getId(), allocation.getId(), requirement.getContainerType(),
requirement.getContainerKind(), requirement.getCapacityCbm(), requirement.getMaxWeightKg(),
container.getWarehouseId(), container.getWarehouseName(), container.getChannelCode(),
container.getChannelName(), container.getCustomerId(), container.getCustomerName());
}).toList();
}
public OceanBookingContainerAllocationEntity allocationByContainer(long containerBookingId) {
return allocations.selectOne(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>()
.eq(OceanBookingContainerAllocationEntity::getContainerBookingId, containerBookingId));
}
public List<OceanBookingStatusHistoryEntity> histories(long id) { return histories.selectList(new LambdaQueryWrapper<OceanBookingStatusHistoryEntity>().eq(OceanBookingStatusHistoryEntity::getOceanBookingId, id).orderByAsc(OceanBookingStatusHistoryEntity::getCreatedAt)); }
public void replaceParties(long id, List<OceanBookingPartyEntity> values) { parties.delete(new LambdaQueryWrapper<OceanBookingPartyEntity>().eq(OceanBookingPartyEntity::getOceanBookingId, id)); values.forEach(parties::insert); }
public void replaceRequirements(long id, List<OceanBookingContainerRequirementEntity> values) { requirements.delete(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id)); values.forEach(requirements::insert); }
public OceanBookingContainerRequirementEntity requirement(long id, long requirementId) { return requirements.selectOne(new LambdaQueryWrapper<OceanBookingContainerRequirementEntity>().eq(OceanBookingContainerRequirementEntity::getOceanBookingId, id).eq(OceanBookingContainerRequirementEntity::getId, requirementId)); }
public ContainerBookingEntity containerBooking(long id) { return containerBookings.selectById(id); }
public void insert(ContainerBookingEntity value) { containerBookings.insert(value); }
public void insert(LoadingPlanEntity value) { loadingPlans.insert(value); }
public int syncReleasedTransport(long id, ReleaseRequest request, long operatorId, String operatorName) {
return containerBookings.syncReleasedTransport(id, request.billOfLadingNo().trim(), request.vesselName().trim(),
request.voyageNo().trim(), request.estimatedSailingAt(), operatorId, operatorName);
public boolean containerNoExists(String containerNo) {
return containerBookings.containerNoExists(containerNo, null);
}
public void insertContainer(ContainerBookingEntity value) { containerBookings.insert(value); }
public void insert(OceanBookingContainerAllocationEntity value) { allocations.insert(value); }
public int reassignAllocation(long sourceId, long targetId) {
return allocations.reassign(sourceId, targetId);
}
public void deleteAllocation(long containerBookingId) { allocations.delete(new LambdaQueryWrapper<OceanBookingContainerAllocationEntity>().eq(OceanBookingContainerAllocationEntity::getContainerBookingId, containerBookingId)); }
public void insert(OceanBookingStatusHistoryEntity value) { histories.insert(value); }
public boolean claimOperation(Long id, long operatorId, String action, String key, String hash) { return operations.claim(com.baomidou.mybatisplus.core.toolkit.IdWorker.getId(), id, operatorId, action, key, hash) == 1; }
public OceanBookingOperationEntity operation(long operatorId, String action, String key) { return operations.selectOne(new LambdaQueryWrapper<OceanBookingOperationEntity>().eq(OceanBookingOperationEntity::getOperatorId, operatorId).eq(OceanBookingOperationEntity::getOperation, action).eq(OceanBookingOperationEntity::getIdempotencyKey, key)); }
public int completeOperation(long id, long operatorId, String action, String key, String json) { return operations.complete(id, operatorId, action, key, json); }
public int completeOperation(Long id, long operatorId, String action, String key, String json) { return operations.complete(id, operatorId, action, key, json); }
}
@@ -2,19 +2,25 @@ package com.baiyue.firstleg.oceanbooking.service;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.entity.ContainerBookingEntity;
import com.baiyue.firstleg.entity.LoadingPlanEntity;
import com.baiyue.firstleg.client.WarehouseMasterDataClient;
import com.baiyue.firstleg.oceanbooking.domain.OceanBookingStatus;
import com.baiyue.firstleg.domain.ContainerSpecificationCatalog;
import com.baiyue.firstleg.oceanbooking.dto.OceanBookingDtos.*;
import com.baiyue.firstleg.oceanbooking.entity.*;
import com.baiyue.firstleg.oceanbooking.repository.OceanBookingRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRepository;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelRow;
import com.baiyue.firstleg.repository.PriceBookQueryRows.ChannelTaxModeRow;
import com.baiyue.firstleg.service.WarehouseSupplierSnapshotService;
import com.baiyue.firstleg.service.FirstlegDocumentNumberGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
@@ -24,207 +30,117 @@ import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Service
public class OceanBookingService {
private static final Set<String> PARTY_TYPES = Set.of("SHIPPER", "CONSIGNEE", "NOTIFY_PARTY");
private static final Set<OceanBookingStatus> OPERATIONAL_LIST_STATUSES = Set.of(
OceanBookingStatus.DRAFT, OceanBookingStatus.RELEASED);
@Resource private OceanBookingRepository repository;
@Resource private PriceBookQueryRepository priceBooks;
@Resource private ObjectMapper objectMapper;
@Resource private WarehouseSupplierSnapshotService suppliers;
@Resource private WarehouseMasterDataClient warehouses;
@Resource private FirstlegDocumentNumberGenerator documentNumbers;
@Value("${baiyue.security.internal-service-token:}") private String internalToken;
@Value("${baiyue.warehouse.public-tenant-id:0}") private long publicWarehouseTenantId;
public PageResult<OceanBookingEntity> page(Query query) {
String status = trim(query.status());
if (status != null) parseStatus(status);
if (status != null && !OPERATIONAL_LIST_STATUSES.contains(parseStatus(status))) {
throw badRequest("订舱列表仅支持草稿或已放舱状态");
}
return repository.page(new Query(query.customerId(), status, trim(query.keyword()), query.page(), query.size()));
}
public List<ChannelOption> channels(long originWarehouseId) {
if (originWarehouseId <= 0) throw badRequest("请选择有效起运仓");
Long versionId = priceBooks.activeVersionId();
if (versionId == null) return List.of();
List<ChannelRow> rows = priceBooks.channels(versionId, originWarehouseId);
Map<String, List<String>> taxModes = priceBooks.channelTaxModes(versionId, originWarehouseId).stream()
.filter(row -> row != null && StringUtils.hasText(row.getChannelCode())
&& StringUtils.hasText(row.getPricingTaxMode()))
.collect(java.util.stream.Collectors.groupingBy(row -> row.getChannelCode().trim(),
java.util.stream.Collectors.mapping(ChannelTaxModeRow::getPricingTaxMode,
java.util.stream.Collectors.toList())));
return rows.stream().map(row -> channelOption(row, taxModes)).toList();
}
public Detail detail(long oceanBookingId) { return detail(required(oceanBookingId)); }
@Transactional
public Detail create(long operatorId, String operatorName, String key, CreateRequest request) {
if (request.expectedVersion() != 0) throw concurrent();
validateDraft(request.data());
validateDraftStructure(request.data());
String action = "CREATE";
String hash = hash(action, null, request);
Detail replay = claimOrReplay(null, operatorId, action, key, hash);
if (replay != null) return replay;
ResolvedDraft resolved = resolveDraft(request.data());
OceanBookingEntity value = new OceanBookingEntity();
value.setId(IdWorker.getId());
value.setBookingNo("OB" + value.getId());
value.setBookingNo(documentNumbers.next("OB"));
value.setStatus(OceanBookingStatus.DRAFT.name());
copy(value, request.data());
copy(value, request.data(), resolved);
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName); value.setVersion(0);
repository.insert(value);
repository.replaceParties(value.getId(), parties(value.getId(), request.data().parties()));
repository.replaceRequirements(value.getId(), requirements(value.getId(), request.data().containerRequirements()));
addHistory(value.getId(), null, OceanBookingStatus.DRAFT.name(), action, null, null, value, operatorId, operatorName);
return complete(value.getId(), operatorId, action, key, detail(value.getId()));
List<OceanBookingContainerRequirementEntity> requirements = requirements(
value.getId(), request.data().containerRequirements());
repository.replaceRequirements(value.getId(), requirements);
OceanBookingEntity draft = required(value.getId());
addHistory(value.getId(), null, OceanBookingStatus.DRAFT.name(), action, null, null, draft, operatorId, operatorName);
return complete(value.getId(), operatorId, action, key, detail(draft));
}
@Transactional
public Detail updateDraft(long oceanBookingId, long operatorId, String operatorName, String key,
UpdateDraftRequest request) {
validateDraft(request.data());
public Detail updateDraft(long id, long operatorId, String operatorName, String key, UpdateRequest request) {
String action = "UPDATE_DRAFT";
Detail replay = claimOrReplay(oceanBookingId, operatorId, action, key, hash(action, oceanBookingId, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(oceanBookingId, request.expectedVersion());
requireStatus(before, OceanBookingStatus.DRAFT);
OceanBookingEntity value = new OceanBookingEntity(); value.setId(oceanBookingId); copy(value, request.data());
if (repository.updateDraft(value, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
repository.replaceParties(oceanBookingId, parties(oceanBookingId, request.data().parties()));
repository.replaceRequirements(oceanBookingId, requirements(oceanBookingId, request.data().containerRequirements()));
return complete(oceanBookingId, operatorId, action, key, detail(oceanBookingId));
}
@Transactional public Detail submit(long id, long operatorId, String name, String key, ActionRequest request) {
return transition(id, operatorId, name, key, "SUBMIT", OceanBookingStatus.DRAFT,
OceanBookingStatus.SUBMITTED, request, false);
}
@Transactional public Detail confirm(long id, long operatorId, String name, String key, ActionRequest request) {
String action = "CONFIRM";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
validateConfirmation(before, repository.requirements(id));
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
null, null, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
requireStatus(before, OceanBookingStatus.DRAFT);
validateDraftStructure(request.data());
ResolvedDraft resolved = resolveDraft(request.data());
copy(before, request.data(), resolved);
if (repository.updateDraft(before, request.expectedVersion(), operatorId, operatorName) != 1) throw concurrent();
repository.replaceParties(id, parties(id, request.data().parties()));
repository.replaceRequirements(id, requirements(id, request.data().containerRequirements()));
OceanBookingEntity after = required(id);
generateContainers(after, repository.requirements(id), operatorId, name);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
operatorId, name);
addHistory(id, OceanBookingStatus.DRAFT.name(), OceanBookingStatus.DRAFT.name(), action, null, before, after, operatorId, operatorName);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public void deleteDraft(long id, long operatorId, String operatorName, String key, ActionRequest request) {
String action = "DELETE_DRAFT";
Detail replay = claimOrReplay(null, operatorId, action, key, hash(action, id, request));
if (replay != null) return;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.DRAFT);
if (repository.deleteDraft(id, request.expectedVersion()) != 1) throw concurrent();
complete(null, operatorId, action, key, new Detail(before, List.of(), List.of(), List.of(), List.of()));
}
@Transactional public Detail release(long id, long operatorId, String name, String key, ReleaseRequest request) {
validateRange(request.estimatedSailingAt(), request.estimatedArrivalAt());
String action = "RELEASE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CONFIRMED);
if (repository.release(id, request, operatorId, name) != 1) throw concurrent();
repository.syncReleasedTransport(id, request, operatorId, name);
requireStatus(before, OceanBookingStatus.DRAFT);
validateRelease(before, repository.parties(id), repository.requirements(id));
if (repository.transition(id, OceanBookingStatus.DRAFT.name(), OceanBookingStatus.RELEASED.name(),
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after,
operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail fail(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("失败原因不能为空");
String action = "FAIL";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.SUBMITTED);
if (repository.transition(id, OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.FAILED.name(), trim(request.reason()), null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, OceanBookingStatus.SUBMITTED.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail failChange(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("改船失败原因不能为空");
return transition(id, operatorId, name, key, "FAIL_CHANGE", OceanBookingStatus.CHANGE_PENDING,
OceanBookingStatus.FAILED, request, true);
}
@Transactional
public Detail cancel(long id, long operatorId, String name, String key, ActionRequest request) {
if (!StringUtils.hasText(request.reason())) throw badRequest("取消原因不能为空");
String action = "CANCEL";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
OceanBookingStatus from = parseStatus(before.getStatus());
if (from == OceanBookingStatus.RELEASED || from == OceanBookingStatus.CANCELLED) {
throw conflict("订舱已越过不可逆节点或已经取消");
}
if (repository.transition(id, from.name(), OceanBookingStatus.CANCELLED.name(), null,
trim(request.reason()), request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail requestChange(long id, long operatorId, String name, String key, ChangeRequest request) {
validateRange(request.estimatedSailingAt(), request.estimatedArrivalAt());
String action = "REQUEST_CHANGE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CONFIRMED);
OceanBookingEntity pending = new OceanBookingEntity();
pending.setPendingVesselName(request.vesselName().trim());
pending.setPendingVoyageNo(request.voyageNo().trim());
pending.setPendingEstimatedSailingAt(request.estimatedSailingAt());
pending.setPendingEstimatedArrivalAt(request.estimatedArrivalAt());
if (repository.requestChange(id, pending, request.reason().trim(), request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail confirmChange(long id, long operatorId, String name, String key, ActionRequest request) {
String action = "CONFIRM_CHANGE";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, OceanBookingStatus.CHANGE_PENDING);
if (repository.confirmChange(id, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, before.getStatus(), after.getStatus(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
@Transactional
public Detail allocate(long id, long operatorId, String name, String key, AllocationRequest request) {
String action = "ALLOCATE_CONTAINER";
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity booking = requiredVersion(id, request.expectedVersion());
if (!Set.of(OceanBookingStatus.SUBMITTED.name(), OceanBookingStatus.CONFIRMED.name(),
OceanBookingStatus.CHANGE_PENDING.name()).contains(booking.getStatus())) {
throw conflict("当前订舱状态不可分配柜次");
}
OceanBookingContainerRequirementEntity requirement = repository.requirement(id, request.requirementId());
if (requirement == null) throw new BizException(ResultCode.NOT_FOUND, "箱型需求不存在");
ContainerBookingEntity container = repository.containerBooking(request.containerBookingId());
if (container == null) throw new BizException(ResultCode.NOT_FOUND, "柜次不存在");
if (!requirement.getContainerType().equals(container.getContainerType())) throw conflict("柜次箱型与订舱需求不一致");
requireSameChannel(booking, container);
long used = repository.allocations(id).stream().filter(v -> request.requirementId().equals(v.getRequirementId())).count();
if (used >= requirement.getQuantity()) throw conflict("该箱型需求已分配完毕");
if (repository.touchForAllocation(id, request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(id);
allocation.setRequirementId(request.requirementId()); allocation.setContainerBookingId(request.containerBookingId());
allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(name); allocation.setAllocatedAt(LocalDateTime.now());
try { repository.insert(allocation); } catch (DuplicateKeyException ex) { throw conflict("柜次已被其他订舱占用"); }
return complete(id, operatorId, action, key, detail(id));
}
private Detail transition(long id, long operatorId, String name, String key, String action,
OceanBookingStatus from, OceanBookingStatus to, ActionRequest request,
boolean failure) {
Detail replay = claimOrReplay(id, operatorId, action, key, hash(action, id, request));
if (replay != null) return replay;
OceanBookingEntity before = requiredVersion(id, request.expectedVersion());
requireStatus(before, from);
if (from == OceanBookingStatus.DRAFT && to == OceanBookingStatus.SUBMITTED) validateSubmission(id);
if (repository.transition(id, from.name(), to.name(), failure ? trim(request.reason()) : null, null,
request.expectedVersion(), operatorId, name) != 1) throw concurrent();
OceanBookingEntity after = required(id);
addHistory(id, from.name(), to.name(), action, request.reason(), before, after, operatorId, name);
return complete(id, operatorId, action, key, detail(after));
}
private Detail claimOrReplay(Long id, long operatorId, String action, String key, String hash) {
validateKey(key);
@@ -236,7 +152,7 @@ public class OceanBookingService {
catch (JsonProcessingException ex) { throw new IllegalStateException("无法读取订舱幂等结果", ex); }
}
private Detail complete(long id, long operatorId, String action, String key, Detail detail) {
private Detail complete(Long id, long operatorId, String action, String key, Detail detail) {
try {
if (repository.completeOperation(id, operatorId, action, key, objectMapper.writeValueAsString(detail)) != 1) {
throw conflict("订舱幂等请求完成状态冲突");
@@ -254,105 +170,116 @@ public class OceanBookingService {
history.setOperatorId(operatorId); history.setOperatorName(operatorName); repository.insert(history);
}
private Detail detail(OceanBookingEntity value) { return new Detail(value, repository.parties(value.getId()), repository.requirements(value.getId()), repository.allocations(value.getId()), repository.histories(value.getId())); }
private Detail detail(OceanBookingEntity value) { return new Detail(value, repository.parties(value.getId()), repository.requirements(value.getId()), repository.containers(value.getId()), repository.histories(value.getId())); }
private OceanBookingEntity required(long id) { OceanBookingEntity value = repository.find(id); if (value == null) throw new BizException(ResultCode.NOT_FOUND, "订舱单不存在"); return value; }
private OceanBookingEntity requiredVersion(long id, int version) { OceanBookingEntity value = required(id); if (!Integer.valueOf(version).equals(value.getVersion())) throw concurrent(); return value; }
private static void requireStatus(OceanBookingEntity value, OceanBookingStatus status) { if (!status.name().equals(value.getStatus())) throw conflict("订舱状态不允许当前操作"); }
private void validateDraft(DraftData data) {
if (data.estimatedSailingAt() != null || data.estimatedArrivalAt() != null
|| StringUtils.hasText(data.billOfLadingNo()) || StringUtils.hasText(data.vesselName())
|| StringUtils.hasText(data.voyageNo())) {
throw badRequest("真实运输数据请在确认放舱时录入");
}
Set<String> types = new HashSet<>();
for (PartyRequest party : data.parties()) {
private void validateDraftStructure(DraftData data) {
boolean hasCustomerId = data.customerId() != null;
boolean hasCustomerName = StringUtils.hasText(data.customerName());
if (hasCustomerId != hasCustomerName) throw badRequest("客户ID和客户名称必须同时填写或同时为空");
if (data.parties() != null) {
Set<String> types = new HashSet<>();
for (PartyRequest party : data.parties()) {
String type = party.partyType().trim().toUpperCase();
if (!PARTY_TYPES.contains(type) || !types.add(type)) throw badRequest("订舱方必须各包含一条SHIPPER、CONSIGNEE和NOTIFY_PARTY");
}
}
if (!types.equals(PARTY_TYPES)) throw badRequest("订舱方必须各包含一条SHIPPER、CONSIGNEE和NOTIFY_PARTY");
}
private void validateSubmission(long id) {
Set<String> types = repository.parties(id).stream().map(OceanBookingPartyEntity::getPartyType)
.collect(java.util.stream.Collectors.toSet());
if (!types.equals(PARTY_TYPES) || repository.requirements(id).isEmpty()) {
throw conflict("订舱方或箱型需求不完整,不能提交");
}
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void copy(OceanBookingEntity v, DraftData d) {
v.setCustomerId(d.customerId()); v.setCustomerName(d.customerName().trim());
v.setChannelCode(d.channelCode().trim()); v.setChannelName(d.channelName().trim());
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(d.warehouseName().trim());
v.setBillOfLadingNo(null);
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(trim(d.bookingAgentName()));
v.setVesselName(null); v.setVoyageNo(null);
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
v.setDestinationPortCode(trim(d.destinationPortCode())); v.setDestinationPortName(trim(d.destinationPortName()));
v.setPlaceOfDelivery(trim(d.placeOfDelivery())); v.setServiceTerm(trim(d.serviceTerm())); v.setFreightTerm(trim(d.freightTerm()));
v.setEstimatedSailingAt(null); v.setEstimatedArrivalAt(null); v.setRemark(trim(d.remark()));
}
private void generateContainers(OceanBookingEntity booking,
List<OceanBookingContainerRequirementEntity> requirements,
long operatorId, String operatorName) {
for (OceanBookingContainerRequirementEntity requirement : requirements) {
for (int sequence = 1; sequence <= requirement.getQuantity(); sequence++) {
ContainerBookingEntity container = new ContainerBookingEntity();
container.setId(IdWorker.getId()); container.setBookingNo("CB" + container.getId());
container.setCabinetNo("CAB-" + booking.getBookingNo() + '-' + requirement.getSortNo() + '-' + sequence);
container.setContainerNo(null); container.setChannelCode(booking.getChannelCode());
container.setChannelName(booking.getChannelName()); container.setOceanBookingId(booking.getId());
container.setOceanBookingRequirementId(requirement.getId());
container.setWarehouseId(booking.getWarehouseId()); container.setWarehouseName(booking.getWarehouseName());
container.setContainerType(requirement.getContainerType());
container.setCapacityCbm(requirement.getCapacityCbm());
container.setMaxWeightKg(requirement.getMaxWeightKg());
container.setShippingCompanyId(booking.getShippingCompanyId());
container.setShippingCompanyName(booking.getShippingCompanyName());
container.setDeparturePortCode(booking.getDeparturePortCode());
container.setDeparturePortName(booking.getDeparturePortName());
container.setDestinationPortCode(booking.getDestinationPortCode());
container.setDestinationPortName(booking.getDestinationPortName());
container.setCreatedBy(operatorId); container.setCreatedByName(operatorName);
container.setUpdatedBy(operatorId); container.setUpdatedByName(operatorName); container.setVersion(0);
repository.insert(container);
LoadingPlanEntity plan = new LoadingPlanEntity();
plan.setPlanNo("LP-" + container.getId()); plan.setContainerBookingId(container.getId());
plan.setPlanStatus("EDITING"); plan.setCreatedBy(operatorId); plan.setCreatedByName(operatorName);
plan.setUpdatedBy(operatorId); plan.setUpdatedByName(operatorName); plan.setVersion(0);
repository.insert(plan);
OceanBookingContainerAllocationEntity allocation = new OceanBookingContainerAllocationEntity();
allocation.setId(IdWorker.getId()); allocation.setOceanBookingId(booking.getId());
allocation.setRequirementId(requirement.getId()); allocation.setContainerBookingId(container.getId());
allocation.setAllocatedBy(operatorId); allocation.setAllocatedByName(operatorName);
allocation.setAllocatedAt(LocalDateTime.now()); repository.insert(allocation);
if (data.containerRequirements() != null) {
for (ContainerRequirementRequest container : data.containerRequirements()) {
ContainerSpecificationCatalog.requireMatch(container.containerType(), container.capacityCbm(), container.maxWeightKg());
}
}
}
private static void validateConfirmation(OceanBookingEntity booking,
List<OceanBookingContainerRequirementEntity> requirements) {
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(booking.getChannelName())
|| booking.getWarehouseId() == null || !StringUtils.hasText(booking.getWarehouseName())) {
throw conflict("订舱缺少渠道或国内中转仓快照,不能确认");
}
if (requirements.isEmpty() || requirements.stream().anyMatch(requirement ->
requirement.getCapacityCbm() == null || requirement.getMaxWeightKg() == null)) {
throw conflict("箱型需求缺少容量或最大承重快照,不能确认");
}
private static void validateRange(LocalDateTime from, LocalDateTime to) { if (from != null && to != null && to.isBefore(from)) throw badRequest("预计到港时间不能早于预计开船时间"); }
private static void validateRelease(OceanBookingEntity booking, List<OceanBookingPartyEntity> parties,
List<OceanBookingContainerRequirementEntity> requirements) {
if (booking.getCustomerId() == null || !StringUtils.hasText(booking.getCustomerName())
|| !StringUtils.hasText(booking.getChannelCode()) || booking.getWarehouseId() == null
|| !StringUtils.hasText(booking.getBillOfLadingNo()) || !StringUtils.hasText(booking.getVesselName())
|| !StringUtils.hasText(booking.getVoyageNo()) || !StringUtils.hasText(booking.getServiceTerm())
|| !StringUtils.hasText(booking.getFreightTerm()) || booking.getEstimatedSailingAt() == null
|| booking.getEstimatedArrivalAt() == null || booking.getShippingCompanyId() == null
|| booking.getBookingAgentId() == null || parties == null || parties.size() != 3
|| requirements == null || requirements.isEmpty()) throw badRequest("订舱资料尚未填写完整,无法发布");
validateRange(booking.getEstimatedSailingAt(), booking.getEstimatedArrivalAt());
}
private ChannelOption requireActiveChannel(Long originWarehouseId, String channelCode) {
if (originWarehouseId == null || originWarehouseId <= 0) throw badRequest("请选择有效起运仓");
String normalized = normalizeChannel(channelCode);
Long versionId = priceBooks.activeVersionId();
if (versionId == null) throw conflict("当前没有 ACTIVE 价格版本");
ChannelRow row = priceBooks.channels(versionId, originWarehouseId).stream()
.filter(value -> normalized.equals(normalizeChannel(value.getChannelCode())))
.findFirst().orElseThrow(() -> conflict("所选物流渠道已失效,请重新选择"));
if (!StringUtils.hasText(row.getChannelName())) throw conflict("所选物流渠道名称配置无效");
return channelOption(row, java.util.Map.of());
}
private static ChannelOption channelOption(ChannelRow row, java.util.Map<String, List<String>> taxModes) {
return new ChannelOption(row.getPriceBookVersionId(), row.getChannelCode(), row.getChannelName(),
row.getPriceModel(), List.copyOf(taxModes.getOrDefault(row.getChannelCode().trim(), List.of())));
}
private static void requireSameChannel(OceanBookingEntity booking, ContainerBookingEntity container) {
if (!StringUtils.hasText(booking.getChannelCode()) || !StringUtils.hasText(container.getChannelCode())) {
throw conflict("订舱或柜次缺少渠道快照,请先完成存量数据回填");
private static String normalizeChannel(String value) {
if (!StringUtils.hasText(value)) throw badRequest("channelCode不能为空");
return value.trim();
}
private record ResolvedDraft(String warehouseName, String channelName, String bookingAgentName) {}
private ResolvedDraft resolveDraft(DraftData d) {
String warehouseName = null; String channelName = null; String bookingAgentName = trim(d.bookingAgentName());
if (d.warehouseId() != null && StringUtils.hasText(d.channelCode())) {
WarehouseMasterDataClient.Warehouse warehouse = requireDomesticTransitWarehouse(d.warehouseId());
warehouseName = warehouse.warehouseName();
channelName = requireActiveChannel(warehouse.id(), d.channelCode()).channelName();
}
if (d.bookingAgentId() != null) {
var snapshot = suppliers.booking(d.bookingAgentId(), d.bookingAgentName());
bookingAgentName = snapshot == null ? null : snapshot.name();
} else if (StringUtils.hasText(d.bookingAgentName())) throw badRequest("订舱代理名称不能脱离供应商ID提交");
return new ResolvedDraft(warehouseName, channelName, bookingAgentName);
}
private static void copy(OceanBookingEntity v, DraftData d, ResolvedDraft resolved) {
v.setCustomerId(d.customerId()); v.setCustomerName(trim(d.customerName()));
v.setChannelCode(trim(d.channelCode())); v.setChannelName(resolved.channelName());
v.setWarehouseId(d.warehouseId()); v.setWarehouseName(resolved.warehouseName());
v.setBillOfLadingNo(trim(d.billOfLadingNo()));
v.setShippingCompanyId(d.shippingCompanyId()); v.setShippingCompanyName(trim(d.shippingCompanyName()));
v.setBookingAgentId(d.bookingAgentId()); v.setBookingAgentName(resolved.bookingAgentName());
v.setVesselName(trim(d.vesselName())); v.setVoyageNo(trim(d.voyageNo()));
v.setDeparturePortCode(trim(d.departurePortCode())); v.setDeparturePortName(trim(d.departurePortName()));
v.setDestinationPortCode(trim(d.destinationPortCode())); v.setDestinationPortName(trim(d.destinationPortName()));
v.setPlaceOfDelivery(trim(d.placeOfDelivery())); v.setServiceTerm(trim(d.serviceTerm())); v.setFreightTerm(trim(d.freightTerm()));
v.setEstimatedSailingAt(d.estimatedSailingAt()); v.setEstimatedArrivalAt(d.estimatedArrivalAt());
v.setRemark(trim(d.remark()));
}
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { if (requests == null) return List.of(); return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { if (requests == null) return List.of(); int[] sort = {0}; return requests.stream().map(r -> { var specification = ContainerSpecificationCatalog.requireMatch(r.containerType(), r.capacityCbm(), r.maxWeightKg()); OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(specification.containerType()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(specification.capacityCbm()); v.setMaxWeightKg(specification.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
private WarehouseMasterDataClient.Warehouse requireDomesticTransitWarehouse(long warehouseId) {
try {
ApiResponse<WarehouseMasterDataClient.Detail> response = warehouses.active(
internalToken, publicWarehouseTenantId, warehouseId);
if (response == null || !response.successful() || response.data() == null
|| response.data().warehouse() == null) {
throw new BizException(ResultCode.NOT_FOUND, "国内中转仓不存在或已停用");
}
WarehouseMasterDataClient.Warehouse warehouse = response.data().warehouse();
if (!Long.valueOf(warehouseId).equals(warehouse.id())
|| !Long.valueOf(publicWarehouseTenantId).equals(warehouse.tenantId())
|| !"ACTIVE".equals(warehouse.status()) || !"DOMESTIC".equals(warehouse.warehouseType())
|| !StringUtils.hasText(warehouse.warehouseName())) {
throw new BizException(ResultCode.VALIDATION_FAILED, "订舱只能选择启用中的国内中转仓");
}
return warehouse;
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "国内中转仓服务暂时不可用");
}
if (!booking.getChannelCode().equals(container.getChannelCode())) throw conflict("柜次渠道与订舱渠道不一致");
}
private static List<OceanBookingPartyEntity> parties(long id, List<PartyRequest> requests) { return requests.stream().map(r -> { OceanBookingPartyEntity v = new OceanBookingPartyEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setPartyType(r.partyType().trim().toUpperCase()); v.setPartyName(r.partyName().trim()); v.setAddress(trim(r.address())); v.setContactName(trim(r.contactName())); v.setContactPhone(trim(r.contactPhone())); return v; }).toList(); }
private static List<OceanBookingContainerRequirementEntity> requirements(long id, List<ContainerRequirementRequest> requests) { int[] sort = {0}; return requests.stream().map(r -> { OceanBookingContainerRequirementEntity v = new OceanBookingContainerRequirementEntity(); v.setId(IdWorker.getId()); v.setOceanBookingId(id); v.setContainerType(r.containerType().trim().toUpperCase()); v.setContainerKind(r.containerKind().trim().toUpperCase()); v.setCapacityCbm(r.capacityCbm()); v.setMaxWeightKg(r.maxWeightKg()); v.setQuantity(r.quantity()); v.setSortNo(sort[0]++); return v; }).toList(); }
private String hash(String action, Long id, Object request) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest((action + '|' + id + '|' + objectMapper.writeValueAsString(request)).getBytes(StandardCharsets.UTF_8))); } catch (Exception ex) { throw new IllegalStateException("无法计算订舱请求摘要", ex); } }
private String json(Object value) { if (value == null) return null; try { return objectMapper.writeValueAsString(value); } catch (JsonProcessingException ex) { throw new IllegalStateException("无法保存订舱历史快照", ex); } }
private static OceanBookingStatus parseStatus(String status) { try { return OceanBookingStatus.valueOf(status); } catch (RuntimeException ex) { throw badRequest("订舱状态无效"); } }
@@ -20,7 +20,8 @@ public final class OutboundDispatchDtos {
public record Aggregate(Long orderId, String orderNo, Long inboundId, String inboundCode, Long tenantId,
Long customerId, String customerName, String inboundStatus,
Long containerBookingId, String containerNo, Long loadingPlanId, String loadingPlanStatus,
Long containerBookingId, String cabinetNo, String containerNo,
Long loadingPlanId, String loadingPlanStatus,
Long pickTaskId, String pickTaskStatus, Long oceanBookingId, String oceanBookingStatus,
Long truckAppointmentId, String truckAppointmentStatus,
String dispatchStatus, Integer version, GateResult gate) {}
@@ -15,7 +15,7 @@ public interface OutboundDispatchReadMapper {
SELECT DISTINCT inbound.order_id orderId,inbound.order_no orderNo,inbound.id inboundId,
inbound.inbound_code inboundCode,inbound.tenant_id tenantId,inbound.customer_id customerId,
inbound.customer_name customerName,inbound.process_status inboundStatus,
booking.id containerBookingId,booking.container_no containerNo,
booking.id containerBookingId,booking.cabinet_no cabinetNo,booking.container_no containerNo,
plan.id loadingPlanId,plan.plan_status loadingPlanStatus
FROM fl_loading_plan_item item
JOIN fl_loading_plan plan ON plan.id=item.loading_plan_id
@@ -100,7 +100,7 @@ public interface OutboundDispatchReadMapper {
record Binding(Long orderId, String orderNo, Long inboundId, String inboundCode, Long tenantId,
Long customerId, String customerName, String inboundStatus, Long containerBookingId,
String containerNo, Long loadingPlanId, String loadingPlanStatus) {}
String cabinetNo, String containerNo, Long loadingPlanId, String loadingPlanStatus) {}
record IdStatus(Long id, String status) {}
record Evidence(Integer evidenceCount, Integer validSeal) {}
}
@@ -109,7 +109,8 @@ public class OutboundDispatchService {
GateResult gate = gate(binding, pick, ocean, truck, evidence);
return new Aggregate(binding.orderId(), binding.orderNo(), binding.inboundId(), binding.inboundCode(),
binding.tenantId(), binding.customerId(), binding.customerName(), binding.inboundStatus(),
binding.containerBookingId(), binding.containerNo(), binding.loadingPlanId(), binding.loadingPlanStatus(),
binding.containerBookingId(), binding.cabinetNo(), binding.containerNo(),
binding.loadingPlanId(), binding.loadingPlanStatus(),
id(pick), status(pick), id(ocean), status(ocean), id(truck), status(truck),
dispatch == null ? OutboundDispatchStatus.READY_FOR_DISPATCH.name() : dispatch.getStatus(),
dispatch == null ? 0 : dispatch.getVersion(), gate);
@@ -73,8 +73,9 @@ public class ActivePriceBookEstimator {
}
private Estimate estimateRegion(long versionId, String channelCode, EstimateRequest request) {
if (request.destinationType() != PricingDestinationType.FBA) {
throw validation("海派渠道仅支持FBA目的仓参考估价");
if (request.destinationType() != PricingDestinationType.FBA
&& request.destinationType() != PricingDestinationType.OVERSEAS) {
throw validation("海派渠道仅支持平台FBA仓或海外中转仓参考估价");
}
if (request.pricingTaxMode() == null) {
throw validation("请选择包税或不包税报价");
@@ -95,7 +96,7 @@ public class ActivePriceBookEstimator {
private PriceRegion resolveWarehouseRegion(Long warehouseId) {
if (warehouseId == null || warehouseId <= 0) {
throw validation("请提供目的仓区域或平台FBA仓,以确定海派区域");
throw validation("请提供目的仓区域、平台FBA仓或海外中转仓,以确定海派区域");
}
return warehouseRegionResolver.resolve(warehouseId);
}

Some files were not shown because too many files have changed in this diff Show More