2026-08-19 16:24:02 +08:00
package com.baiyue.lastleg.controller ;
import com.baiyue.common.core.api.ApiResponse ;
import com.baiyue.common.core.api.PageResult ;
import com.baiyue.common.event.EventEnvelope ;
import com.baiyue.common.security.AuthenticatedUser ;
import com.baiyue.lastleg.dto.LastlegOrderDetailResponse ;
import com.baiyue.lastleg.dto.LastlegOrderRequests ;
import com.baiyue.lastleg.dto.LastlegOrderSummary ;
2026-08-28 13:54:32 +08:00
import com.baiyue.lastleg.dto.ContainerPlanProgressDetail ;
import com.baiyue.lastleg.dto.ContainerPlanProgressSummary ;
2026-08-26 11:05:26 +08:00
import com.baiyue.lastleg.entity.LastlegOrderFee ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.security.InternalServiceTokenGuard ;
2026-08-20 18:02:38 +08:00
import com.baiyue.lastleg.security.OverseasPermissions ;
2026-08-19 16:24:02 +08:00
import com.baiyue.lastleg.service.LastlegOrderService ;
import com.baiyue.lastleg.service.LoadingCompletedConsumer ;
2026-08-25 10:31:04 +08:00
import com.baiyue.lastleg.shipping.service.ContainerShippingNodeConsumer ;
import com.baiyue.common.event.EventTypes ;
2026-08-19 16:24:02 +08:00
import io.swagger.v3.oas.annotations.Operation ;
import io.swagger.v3.oas.annotations.tags.Tag ;
import jakarta.validation.Valid ;
import jakarta.annotation.Resource ;
import org.springframework.http.HttpHeaders ;
import org.springframework.http.HttpStatus ;
import org.springframework.http.MediaType ;
import org.springframework.http.ResponseEntity ;
import org.springframework.security.core.annotation.AuthenticationPrincipal ;
2026-08-20 18:02:38 +08:00
import org.springframework.security.access.prepost.PreAuthorize ;
2026-08-19 16:24:02 +08:00
import org.springframework.web.bind.annotation.GetMapping ;
import org.springframework.web.bind.annotation.PathVariable ;
import org.springframework.web.bind.annotation.PostMapping ;
import org.springframework.web.bind.annotation.PutMapping ;
import org.springframework.web.bind.annotation.RequestBody ;
import org.springframework.web.bind.annotation.RequestHeader ;
import org.springframework.web.bind.annotation.RequestParam ;
import org.springframework.web.bind.annotation.RestController ;
import org.springframework.web.server.ResponseStatusException ;
import java.nio.charset.StandardCharsets ;
@RestController
@Tag ( name = "Tail-leg orders" )
public class LastlegOrderController {
@Resource private LoadingCompletedConsumer consumer ;
2026-08-25 10:31:04 +08:00
@Resource private ContainerShippingNodeConsumer shippingNodeConsumer ;
2026-08-19 16:24:02 +08:00
@Resource private LastlegOrderService orderService ;
@Resource private InternalServiceTokenGuard tokenGuard ;
@PostMapping ( "/internal/lastleg/events" )
2026-08-25 10:31:04 +08:00
@Operation ( summary = "Receive a first-leg loading or actual shipping-node event" ,
description = "Accepts loading-completed V1/V2 and FirstLegContainerShippingNodeRecorded V1 through the internal token boundary; only monotonic actual PICKUP_READY-or-later facts open overseas PDA receiving." )
2026-08-19 16:24:02 +08:00
public ApiResponse < Integer > receive (
@RequestHeader ( value = "X-Internal-Service-Token" , required = false ) String token ,
@RequestBody EventEnvelope event ) {
tokenGuard . verify ( token );
2026-08-25 14:46:06 +08:00
int consumed ;
if ( EventTypes . FIRST_LEG_CONTAINER_SHIPPING_NODE_RECORDED . equals ( event . eventType ())) {
consumed = shippingNodeConsumer . consume ( event );
} else if ( EventTypes . FIRST_LEG_LOADING_PLAN_COMPLETED . equals ( event . eventType ())) {
consumed = consumer . consume ( event );
} else {
throw new ResponseStatusException ( HttpStatus . BAD_REQUEST , "Lastleg 不消费该事件类型: " + event . eventType ());
}
2026-08-25 10:31:04 +08:00
return ApiResponse . success ( consumed );
2026-08-19 16:24:02 +08:00
}
@GetMapping ( "/api/platform/v1/last-leg/orders" )
2026-08-20 18:02:38 +08:00
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_LIST + "')" )
@Operation ( summary = "Page tail-leg order rows" , description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_LIST )
2026-08-19 16:24:02 +08:00
public ApiResponse < PageResult < LastlegOrderSummary >> list (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@Valid LastlegOrderRequests . Query query ) {
2026-08-21 10:43:25 +08:00
return ApiResponse . success ( orderService . page ( platformScope ( user , customerId ), query ));
2026-08-19 16:24:02 +08:00
}
2026-08-27 17:16:33 +08:00
@GetMapping ( "/api/platform/v1/last-leg/orders/container-summaries" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_LIST + "')" )
@Operation ( summary = "分页查询待分配柜级摘要" ,
description = "按稳定 sourceContainerBookingId 聚合待分配尾程订单;平台跨客户查询。Required authority: "
+ OverseasPermissions . LAST_MILE_ORDER_LIST )
2026-08-28 13:54:32 +08:00
public ApiResponse < PageResult < ContainerPlanProgressSummary >> containerSummaries (
2026-08-27 17:16:33 +08:00
@AuthenticationPrincipal AuthenticatedUser user ,
2026-08-28 13:54:32 +08:00
@Valid LastlegOrderRequests . ContainerProgressQuery query ) {
2026-08-27 17:16:33 +08:00
platformScope ( user , null );
2026-08-28 13:54:32 +08:00
return ApiResponse . success ( orderService . containerProgressPage ( query ));
}
@GetMapping ( "/api/platform/v1/last-leg/orders/container-summaries/{sourceContainerBookingId}" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_LIST + "')" )
@Operation ( summary = "查询柜级入库计划进度详情" ,
description = "按稳定 sourceContainerBookingId 返回柜内全部尾程订单、入仓码及各自计划状态,不受订单分页影响。Required authority: "
+ OverseasPermissions . LAST_MILE_ORDER_LIST )
public ApiResponse < ContainerPlanProgressDetail > containerSummaryDetail (
@AuthenticationPrincipal AuthenticatedUser user ,
@PathVariable String sourceContainerBookingId ) {
platformScope ( user , null );
return ApiResponse . success ( orderService . containerProgressDetail ( sourceContainerBookingId ));
2026-08-27 17:16:33 +08:00
}
2026-08-19 16:24:02 +08:00
@GetMapping ( "/api/platform/v1/last-leg/orders/{id}" )
2026-08-20 18:02:38 +08:00
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_LIST + "')" )
@Operation ( summary = "Get one inbound-code-level tail-leg order" , description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_LIST )
2026-08-19 16:24:02 +08:00
public ApiResponse < LastlegOrderDetailResponse > detail (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@PathVariable long id ) {
2026-08-21 10:43:25 +08:00
return ApiResponse . success ( orderService . detail ( platformScope ( user , customerId ), id ));
2026-08-19 16:24:02 +08:00
}
2026-08-26 11:05:26 +08:00
@PostMapping ( "/api/platform/v1/last-leg/orders/{id}/fees" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
@Operation ( summary = "Add a fee to one tail-leg order" , description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_EDIT )
public ApiResponse < LastlegOrderFee > createFee (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@PathVariable long id ,
@Valid @RequestBody LastlegOrderRequests . CreateFee request ) {
return ApiResponse . success ( orderService . createFee ( platformScope ( user , customerId ), id , request , operatorName ( user )));
}
2026-08-28 14:20:48 +08:00
@PutMapping ( "/api/platform/v1/last-leg/orders/{id}/detained" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
@Operation ( summary = "Set tail-leg order hold status" , description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_EDIT )
public ApiResponse < Void > setDetained (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@PathVariable long id ,
@Valid @RequestBody LastlegOrderRequests . SetDetained request ) {
2026-08-31 15:22:32 +08:00
orderService . setDetained ( platformScope ( user , customerId ), id , request ,
String . valueOf ( user . userId ()), operatorName ( user ));
2026-08-28 14:20:48 +08:00
return ApiResponse . success ( null );
}
@PutMapping ( "/api/platform/v1/last-leg/orders/{id}/pallet-merge-required" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
@Operation ( summary = "Set the PDA pallet-merge reminder for one tail-leg order" ,
description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_EDIT )
public ApiResponse < Void > setPalletMergeRequired (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@PathVariable long id ,
@Valid @RequestBody LastlegOrderRequests . SetPalletMergeRequired request ) {
orderService . setPalletMergeRequired ( platformScope ( user , customerId ), id , request , operatorName ( user ));
return ApiResponse . success ( null );
}
2026-08-31 14:39:40 +08:00
@PutMapping ( "/api/platform/v1/last-leg/orders/pallet-merge-required/batch" )
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
@Operation ( summary = "Mark a selected tail-leg order group for PDA pallet merge" )
public ApiResponse < Void > setPalletMergeBatch (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@Valid @RequestBody LastlegOrderRequests . SetPalletMergeBatch request ) {
orderService . setPalletMergeBatch ( platformScope ( user , customerId ), request , operatorName ( user ));
return ApiResponse . success ( null );
}
2026-08-19 16:24:02 +08:00
@GetMapping ( value = "/api/platform/v1/last-leg/orders/export" , produces = "text/csv" )
2026-08-20 18:02:38 +08:00
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EXPORT + "')" )
@Operation ( summary = "Export filtered tail-leg orders as UTF-8 CSV" , description = "Required authority: " + OverseasPermissions . LAST_MILE_ORDER_EXPORT )
2026-08-19 16:24:02 +08:00
public ResponseEntity < byte []> export (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@Valid LastlegOrderRequests . Query query ) {
2026-08-21 10:43:25 +08:00
byte [] body = orderService . exportCsv ( platformScope ( user , customerId ), query );
2026-08-19 16:24:02 +08:00
return ResponseEntity . ok ()
. contentType ( new MediaType ( "text" , "csv" , StandardCharsets . UTF_8 ))
. header ( HttpHeaders . CONTENT_DISPOSITION , "attachment; filename=lastleg-orders.csv" )
. body ( body );
}
@PutMapping ( "/api/platform/v1/last-leg/orders/{id}/destination" )
2026-08-20 18:02:38 +08:00
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
2026-08-28 13:54:32 +08:00
@Operation ( summary = "将单票尾程订单转入 FBA 目的仓" ,
description = "目标仓必须来自国内 Warehouse Service 的平台公共 ACTIVE FBA 主档,ID/编码/名称必须一致。Required authority: "
+ OverseasPermissions . LAST_MILE_ORDER_EDIT )
2026-08-19 16:24:02 +08:00
public ApiResponse < Integer > transferOne (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@PathVariable long id ,
@Valid @RequestBody LastlegOrderRequests . SingleTransferDestination request ) {
2026-08-21 10:43:25 +08:00
return ApiResponse . success ( orderService . transferDestination ( platformScope ( user , customerId ), request . withOrderId ( id )));
2026-08-19 16:24:02 +08:00
}
@PutMapping ( "/api/platform/v1/last-leg/orders/destination" )
2026-08-20 18:02:38 +08:00
@PreAuthorize ( "hasAuthority('" + OverseasPermissions . LAST_MILE_ORDER_EDIT + "')" )
2026-08-28 13:54:32 +08:00
@Operation ( summary = "批量将尾程订单转入 FBA 目的仓" ,
description = "目标仓必须来自国内 Warehouse Service 的平台公共 ACTIVE FBA 主档,ID/编码/名称必须一致。Required authority: "
+ OverseasPermissions . LAST_MILE_ORDER_EDIT )
2026-08-19 16:24:02 +08:00
public ApiResponse < Integer > transferBatch (
@AuthenticationPrincipal AuthenticatedUser user ,
@RequestParam ( required = false ) String customerId ,
@Valid @RequestBody LastlegOrderRequests . TransferDestination request ) {
2026-08-21 10:43:25 +08:00
return ApiResponse . success ( orderService . transferDestination ( platformScope ( user , customerId ), request ));
2026-08-19 16:24:02 +08:00
}
2026-08-21 10:43:25 +08:00
private String platformScope ( AuthenticatedUser user , String requestedCustomer ) {
2026-08-19 16:24:02 +08:00
if ( user == null || user . tenantId () == null ) {
throw new ResponseStatusException ( HttpStatus . UNAUTHORIZED );
}
2026-08-21 10:43:25 +08:00
if ( ! "PLATFORM" . equals ( user . appCode ()) || user . tenantId () != 0L ) {
throw new ResponseStatusException ( HttpStatus . FORBIDDEN , "Platform identity is required" );
2026-08-19 16:24:02 +08:00
}
2026-08-21 10:43:25 +08:00
return requestedCustomer == null || requestedCustomer . isBlank () || "0" . equals ( requestedCustomer )
? null : requestedCustomer . trim ();
2026-08-19 16:24:02 +08:00
}
2026-08-26 11:05:26 +08:00
private String operatorName ( AuthenticatedUser user ) {
return user . username () == null || user . username (). isBlank () ? String . valueOf ( user . userId ()) : user . username ();
}
2026-08-19 16:24:02 +08:00
}