提交
This commit is contained in:
@@ -21,6 +21,7 @@ import org.springframework.web.bind.MissingServletRequestParameterException;
|
|||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
@@ -89,6 +90,12 @@ public class GlobalExceptionHandler {
|
|||||||
return failure(code, message, request);
|
return failure(code, message, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(NoResourceFoundException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleNoResourceFound(
|
||||||
|
NoResourceFoundException exception, HttpServletRequest request) {
|
||||||
|
return failure(ResultCode.NOT_FOUND, ResultCode.NOT_FOUND.message(), request);
|
||||||
|
}
|
||||||
|
|
||||||
@ExceptionHandler(Exception.class)
|
@ExceptionHandler(Exception.class)
|
||||||
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception, HttpServletRequest request) {
|
public ResponseEntity<ApiResponse<Void>> handleUnexpected(Exception exception, HttpServletRequest request) {
|
||||||
log.error("Unhandled request failure requestId={} traceId={}",
|
log.error("Unhandled request failure requestId={} traceId={}",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.springframework.validation.BindException;
|
|||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
import org.springframework.web.bind.MissingRequestHeaderException;
|
import org.springframework.web.bind.MissingRequestHeaderException;
|
||||||
|
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
@@ -103,6 +104,18 @@ class GlobalExceptionHandlerTest {
|
|||||||
assertThat(response.getBody().requestId()).isEqualTo("stale-version");
|
assertThat(response.getBody().requestId()).isEqualTo("stale-version");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingControllerPathMapsToNotFoundInsteadOfInternalError() {
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||||
|
|
||||||
|
var response = handler.handleNoResourceFound(
|
||||||
|
new NoResourceFoundException(org.springframework.http.HttpMethod.GET,
|
||||||
|
"api/platform/v1/last-leg/dispatches"), request);
|
||||||
|
|
||||||
|
assertThat(response.getStatusCode().value()).isEqualTo(200);
|
||||||
|
assertThat(response.getBody().code()).isEqualTo(ResultCode.NOT_FOUND.code());
|
||||||
|
}
|
||||||
|
|
||||||
private void paging(long page) {
|
private void paging(long page) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ spring:
|
|||||||
lastleg:
|
lastleg:
|
||||||
dispatch-query-api:
|
dispatch-query-api:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
smart-dispatch-api:
|
||||||
|
enabled: true
|
||||||
baiyue:
|
baiyue:
|
||||||
clients:
|
clients:
|
||||||
domestic-warehouse:
|
domestic-warehouse:
|
||||||
|
|||||||
@@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
- 四期在本服务内只进入尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、海外采购和本土卡车;本土散板新增实现也归本服务的独立稳定子域。
|
- 四期在本服务内只进入尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、海外采购和本土卡车;本土散板新增实现也归本服务的独立稳定子域。
|
||||||
- `dispatch/`、`delivery/`、出库单、PDA 出库拣货、派送和其他库存离仓能力虽然已存在于服务代码或表结构中,但不属于当前四期开发与验收范围,不得因代码已存在而自动纳入。
|
- `dispatch/`、`delivery/`、出库单、PDA 出库拣货、派送和其他库存离仓能力虽然已存在于服务代码或表结构中,但不属于当前四期开发与验收范围,不得因代码已存在而自动纳入。
|
||||||
- 历史 `dispatch/` 写操作、PDA 出库和 `delivery/` Controller 默认不注册;只有后续阶段明确启用 `lastleg.future-outbound-api.enabled=true` 才可恢复。平台排车列表与详情是独立只读入口,仅由海外配置 `lastleg.dispatch-query-api.enabled=true` 启用,不得借此开放预排、编辑、取消、PDA 出库或派送。四期平台库存 Controller 不映射 allocate、unallocate、pick、ship。
|
- 历史排车编辑/取消/出库、PDA 出库和 `delivery/` Controller 默认不注册;只有后续阶段明确启用 `lastleg.future-outbound-api.enabled=true` 才可恢复。平台排车列表与详情由 `lastleg.dispatch-query-api.enabled=true` 独立启用;入库单智能排车候选和预排创建由 `lastleg.smart-dispatch-api.enabled=true` 独立启用。四个排车平台入口均不接收 `customerId`,服务端从尾程库排车/入仓数据解析唯一客户范围,检测到多客户时拒绝请求;预排只能创建待出库排车并锁定托盘,不得借此开放编辑、取消、depart、PDA 出库或派送。四期平台库存 Controller 不映射 allocate、unallocate、pick、ship。
|
||||||
- 海外仓库、库位、实物托盘和资源锁归 `warehouse-overseas-service/`;浏览器 API 统一经 `gateway-overseas/`,本服务不得复制仓储资源主数据或另建公网认证入口。
|
- 海外仓库、库位、实物托盘和资源锁归 `warehouse-overseas-service/`;浏览器 API 统一经 `gateway-overseas/`,本服务不得复制仓储资源主数据或另建公网认证入口。
|
||||||
|
|
||||||
## 数据库表索引与注释约束
|
## 数据库表索引与注释约束
|
||||||
|
|||||||
-26
@@ -4,7 +4,6 @@ import com.baiyue.common.core.api.ApiResponse;
|
|||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
|
||||||
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
|
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests;
|
||||||
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
|
import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses;
|
||||||
import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService;
|
import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService;
|
||||||
@@ -18,7 +17,6 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.PutMapping;
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
@@ -28,8 +26,6 @@ import org.springframework.web.bind.annotation.RequestParam;
|
|||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@ConditionalOnProperty(name = "lastleg.future-outbound-api.enabled", havingValue = "true")
|
@ConditionalOnProperty(name = "lastleg.future-outbound-api.enabled", havingValue = "true")
|
||||||
@RequestMapping("/api/platform/v1/last-leg/dispatches")
|
@RequestMapping("/api/platform/v1/last-leg/dispatches")
|
||||||
@@ -38,28 +34,6 @@ public class DispatchController {
|
|||||||
@Resource private DispatchService service;
|
@Resource private DispatchService service;
|
||||||
@Resource private DispatchPdaWorkflowService pdaWorkflow;
|
@Resource private DispatchPdaWorkflowService pdaWorkflow;
|
||||||
|
|
||||||
@GetMapping("/candidates")
|
|
||||||
@Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch")
|
|
||||||
public ApiResponse<List<DispatchPalletCandidate>> candidates(
|
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@RequestParam(required = false) String customerId,
|
|
||||||
@RequestParam(required = false) String orderNo,
|
|
||||||
@RequestParam(required = false) String fbaWarehouseCode,
|
|
||||||
@RequestParam(required = false) String sourceStage) {
|
|
||||||
return ApiResponse.success(service.candidates(scope(user, customerId), orderNo, fbaWarehouseCode, sourceStage));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PostMapping("/pre-arrange")
|
|
||||||
@Operation(summary = "Create a pending-departure dispatch and lock all selected pallets")
|
|
||||||
public ApiResponse<DispatchResponse> preArrange(
|
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
|
||||||
@RequestParam(required = false) String customerId,
|
|
||||||
@Valid @RequestBody DispatchRequests.PreArrange request) {
|
|
||||||
String scopedTenant = scope(user, customerId);
|
|
||||||
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
|
||||||
return ApiResponse.success(service.preArrange(scopedTenant, operatorId, request));
|
|
||||||
}
|
|
||||||
|
|
||||||
@PutMapping("/{dispatchNo}")
|
@PutMapping("/{dispatchNo}")
|
||||||
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
|
@Operation(summary = "Edit vehicle, driver, appointment, route and departure data")
|
||||||
public ApiResponse<DispatchResponse> update(
|
public ApiResponse<DispatchResponse> update(
|
||||||
|
|||||||
+6
-2
@@ -6,12 +6,14 @@ import com.baiyue.common.security.AuthenticatedUser;
|
|||||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||||
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||||
|
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
@@ -27,21 +29,23 @@ public class DispatchQueryController {
|
|||||||
@Resource private DispatchService service;
|
@Resource private DispatchService service;
|
||||||
|
|
||||||
@GetMapping
|
@GetMapping
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||||
@Operation(summary = "Page dispatch orders")
|
@Operation(summary = "Page dispatch orders")
|
||||||
public ApiResponse<PageResult<DispatchResponse>> page(
|
public ApiResponse<PageResult<DispatchResponse>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@Valid DispatchRequests.Query query) {
|
@Valid DispatchRequests.Query query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(null, query));
|
return ApiResponse.success(service.page(service.resolveSingleCustomerScope(), query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{dispatchNo}")
|
@GetMapping("/{dispatchNo}")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||||
@Operation(summary = "Get dispatch details and all pallet destinations")
|
@Operation(summary = "Get dispatch details and all pallet destinations")
|
||||||
public ApiResponse<DispatchResponse> detail(
|
public ApiResponse<DispatchResponse> detail(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable String dispatchNo) {
|
@PathVariable String dispatchNo) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.detail(null, dispatchNo));
|
return ApiResponse.success(service.detail(service.resolveSingleCustomerScope(), dispatchNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requirePlatform(AuthenticatedUser user) {
|
private void requirePlatform(AuthenticatedUser user) {
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
|
import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
||||||
|
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||||
|
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||||
|
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@ConditionalOnProperty(name = "lastleg.smart-dispatch-api.enabled", havingValue = "true")
|
||||||
|
@RequestMapping("/api/platform/v1/last-leg/dispatches")
|
||||||
|
@Tag(name = "Tail-leg smart dispatch")
|
||||||
|
public class SmartDispatchController {
|
||||||
|
@Resource private DispatchService service;
|
||||||
|
|
||||||
|
@GetMapping("/candidates")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_LIST + "')")
|
||||||
|
@Operation(summary = "List unlocked planned or received pallets for intelligent pre-dispatch",
|
||||||
|
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||||
|
public ApiResponse<List<DispatchPalletCandidate>> candidates(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) String orderNo,
|
||||||
|
@RequestParam(required = false) String fbaWarehouseCode,
|
||||||
|
@RequestParam(required = false) String sourceStage) {
|
||||||
|
requirePlatform(user);
|
||||||
|
return ApiResponse.success(service.candidates(service.resolveSingleCustomerScope(), orderNo,
|
||||||
|
fbaWarehouseCode, sourceStage));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/pre-arrange")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_EDIT + "')")
|
||||||
|
@Operation(summary = "Create a pending-departure dispatch and lock all selected pallets",
|
||||||
|
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_EDIT)
|
||||||
|
public ApiResponse<DispatchResponse> preArrange(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody DispatchRequests.PreArrange request) {
|
||||||
|
requirePlatform(user);
|
||||||
|
String scopedCustomer = service.resolveSingleCustomerScope();
|
||||||
|
String operatorId = user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||||
|
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requirePlatform(AuthenticatedUser user) {
|
||||||
|
if (user == null || user.tenantId() == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
if (!"PLATFORM".equals(user.appCode()) || user.tenantId() != 0L) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Platform application required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
@@ -9,6 +9,12 @@ import java.util.List;
|
|||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface DispatchCandidateMapper {
|
public interface DispatchCandidateMapper {
|
||||||
|
@Select("SELECT customer_id FROM (" +
|
||||||
|
"SELECT customer_id FROM ll_inbound_plan UNION SELECT customer_id FROM ll_dispatch_order" +
|
||||||
|
") dispatch_customers WHERE customer_id IS NOT NULL AND customer_id != '' " +
|
||||||
|
"ORDER BY customer_id LIMIT 2")
|
||||||
|
List<String> findCustomerScopes();
|
||||||
|
|
||||||
@Select("<script>" +
|
@Select("<script>" +
|
||||||
"SELECT p.plan_id AS planId,p.id AS planPalletId,p.warehouse_pallet_id AS warehousePalletId," +
|
"SELECT p.plan_id AS planId,p.id AS planPalletId,p.warehouse_pallet_id AS warehousePalletId," +
|
||||||
"MIN(l.order_id) AS orderId,MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
"MIN(l.order_id) AS orderId,MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||||
|
|||||||
-3
@@ -11,9 +11,6 @@ public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
|
|||||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1")
|
||||||
DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
|
DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo);
|
||||||
|
|
||||||
@Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1")
|
|
||||||
DispatchOrder findAnyByDispatchNo(@Param("dispatchNo") String dispatchNo);
|
|
||||||
|
|
||||||
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
@Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE")
|
||||||
DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id);
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-4
@@ -41,16 +41,30 @@ public class DispatchService {
|
|||||||
@Resource private DispatchPalletLockMapper locks;
|
@Resource private DispatchPalletLockMapper locks;
|
||||||
@Resource private DispatchCandidateMapper candidates;
|
@Resource private DispatchCandidateMapper candidates;
|
||||||
|
|
||||||
|
public String resolveSingleCustomerScope() {
|
||||||
|
List<String> customerIds = candidates.findCustomerScopes();
|
||||||
|
if (customerIds.size() > 1) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Dispatch APIs require exactly one customer in the current deployment");
|
||||||
|
}
|
||||||
|
return customerIds.isEmpty() ? null : customerIds.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
public List<DispatchPalletCandidate> candidates(String customerId, String orderNo,
|
public List<DispatchPalletCandidate> candidates(String customerId, String orderNo,
|
||||||
String fbaWarehouseCode, String sourceStage) {
|
String fbaWarehouseCode, String sourceStage) {
|
||||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
||||||
}
|
}
|
||||||
|
if (!StringUtils.hasText(customerId)) return List.of();
|
||||||
return candidates.available(customerId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
|
return candidates.available(customerId, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
|
||||||
|
if (!StringUtils.hasText(customerId)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"No customer business data is available for dispatch");
|
||||||
|
}
|
||||||
List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList();
|
List<Long> palletIds = request.planPalletIds().stream().filter(Objects::nonNull).distinct().sorted().toList();
|
||||||
if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) {
|
if (palletIds.isEmpty() || palletIds.size() != request.planPalletIds().size()) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Plan pallet IDs must be unique and non-empty");
|
||||||
@@ -149,8 +163,11 @@ public class DispatchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query) {
|
public PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query) {
|
||||||
|
if (!StringUtils.hasText(customerId)) {
|
||||||
|
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
||||||
|
}
|
||||||
Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()),
|
Page<DispatchOrder> page = orders.selectPage(new Page<>(query.page(), query.size()),
|
||||||
new LambdaQueryWrapper<DispatchOrder>().eq(StringUtils.hasText(customerId), DispatchOrder::getCustomerId, customerId)
|
new LambdaQueryWrapper<DispatchOrder>().eq(DispatchOrder::getCustomerId, customerId)
|
||||||
.like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
|
.like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo())
|
||||||
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
.like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate())
|
||||||
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
|
.eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status())
|
||||||
@@ -216,9 +233,10 @@ public class DispatchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private DispatchOrder find(String customerId, String dispatchNo) {
|
private DispatchOrder find(String customerId, String dispatchNo) {
|
||||||
DispatchOrder dispatch = StringUtils.hasText(customerId)
|
if (!StringUtils.hasText(customerId)) {
|
||||||
? orders.findByDispatchNo(customerId, dispatchNo)
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||||
: orders.findAnyByDispatchNo(dispatchNo);
|
}
|
||||||
|
DispatchOrder dispatch = orders.findByDispatchNo(customerId, dispatchNo);
|
||||||
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||||
return dispatch;
|
return dispatch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ spring:
|
|||||||
password: ${LASTLEG_DB_PASSWORD:123456}
|
password: ${LASTLEG_DB_PASSWORD:123456}
|
||||||
management.endpoints.web.exposure.include: health,info,prometheus
|
management.endpoints.web.exposure.include: health,info,prometheus
|
||||||
|
|
||||||
|
lastleg:
|
||||||
|
dispatch-query-api:
|
||||||
|
enabled: ${LASTLEG_DISPATCH_QUERY_API_ENABLED:true}
|
||||||
|
smart-dispatch-api:
|
||||||
|
enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}
|
||||||
|
|
||||||
baiyue:
|
baiyue:
|
||||||
clients:
|
clients:
|
||||||
domestic-warehouse:
|
domestic-warehouse:
|
||||||
|
|||||||
@@ -3,6 +3,55 @@ info:
|
|||||||
title: Lastleg Overseas Business API
|
title: Lastleg Overseas Business API
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
paths:
|
paths:
|
||||||
|
/api/platform/v1/last-leg/dispatches:
|
||||||
|
get:
|
||||||
|
summary: 分页查询排车单
|
||||||
|
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端仅在当前部署恰好存在一个客户范围时查询,前端不传 customerId。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
parameters:
|
||||||
|
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||||
|
- { name: size, in: query, schema: { type: integer, minimum: 1, maximum: 200, default: 20 } }
|
||||||
|
- { name: dispatchNo, in: query, schema: { type: string } }
|
||||||
|
- { name: vehiclePlate, in: query, schema: { type: string } }
|
||||||
|
- { name: status, in: query, schema: { type: string } }
|
||||||
|
- { name: fbaAppointmentNo, in: query, schema: { type: string } }
|
||||||
|
responses:
|
||||||
|
'200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse }
|
||||||
|
'409': { description: 当前部署检测到多个客户范围,拒绝跨客户查询 }
|
||||||
|
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
|
||||||
|
get:
|
||||||
|
summary: 查询排车单详情
|
||||||
|
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端解析唯一客户范围,前端不传 customerId。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
parameters:
|
||||||
|
- { name: dispatchNo, in: path, required: true, schema: { type: string } }
|
||||||
|
responses:
|
||||||
|
'200': { description: 统一 ApiResponse 包装的 DispatchResponse }
|
||||||
|
'404': { description: 排车单不存在 }
|
||||||
|
'409': { description: 当前部署检测到多个客户范围 }
|
||||||
|
/api/platform/v1/last-leg/dispatches/candidates:
|
||||||
|
get:
|
||||||
|
summary: 查询智能排车候选托盘
|
||||||
|
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端解析唯一客户范围,仅返回尚未被有效排车锁定的计划或已实收托盘,前端不传 customerId。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
parameters:
|
||||||
|
- { name: orderNo, in: query, schema: { type: string } }
|
||||||
|
- { name: fbaWarehouseCode, in: query, schema: { type: string } }
|
||||||
|
- { name: sourceStage, in: query, schema: { type: string, enum: [PLAN, RECEIVED] } }
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 统一 ApiResponse 包装的候选托盘数组
|
||||||
|
content: { application/json: { schema: { type: array, items: { $ref: '#/components/schemas/SmartDispatchCandidate' } } } }
|
||||||
|
/api/platform/v1/last-leg/dispatches/pre-arrange:
|
||||||
|
post:
|
||||||
|
summary: 创建智能预排车单
|
||||||
|
description: 权限 platform:overseas-warehouse:inbound-order:edit;服务端解析唯一客户范围,锁定同一海外仓的所选托盘并创建 PENDING_DEPARTURE 排车单,前端不传 customerId;不开放编辑、取消、depart、PDA 出库或派送。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
|
content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchPreArrange' } } }
|
||||||
|
responses:
|
||||||
|
'200': { description: 统一 ApiResponse 包装的预排车详情, content: { application/json: { schema: { $ref: '#/components/schemas/DispatchResponse' } } } }
|
||||||
/api/platform/v1/last-leg/overseas-procurements:
|
/api/platform/v1/last-leg/overseas-procurements:
|
||||||
get:
|
get:
|
||||||
summary: 分页查询海外采购订单
|
summary: 分页查询海外采购订单
|
||||||
@@ -212,6 +261,81 @@ paths:
|
|||||||
responses: { '200': { description: 统一 ApiResponse 包装的换标单详情 }, '409': { description: 非法状态迁移或关联尾程单缺失 } }
|
responses: { '200': { description: 统一 ApiResponse 包装的换标单详情 }, '409': { description: 非法状态迁移或关联尾程单缺失 } }
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
|
SmartDispatchCandidate:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
planId: { type: string }
|
||||||
|
planPalletId: { type: string }
|
||||||
|
warehousePalletId: { type: string, nullable: true }
|
||||||
|
orderId: { type: string }
|
||||||
|
overseasWarehouseId: { type: string }
|
||||||
|
overseasWarehouseName: { type: string }
|
||||||
|
orderNo: { type: string }
|
||||||
|
orderRefs: { type: string }
|
||||||
|
inboundCode: { type: string }
|
||||||
|
palletNo: { type: string }
|
||||||
|
destinationWarehouseId: { type: string }
|
||||||
|
destinationWarehouseCode: { type: string }
|
||||||
|
destinationWarehouseName: { type: string }
|
||||||
|
fbaWarehouseCode: { type: string }
|
||||||
|
packageCount: { type: integer }
|
||||||
|
volumeCbm: { type: number }
|
||||||
|
sourceStage: { type: string, enum: [PLAN, RECEIVED] }
|
||||||
|
SmartDispatchPreArrange:
|
||||||
|
type: object
|
||||||
|
required: [planPalletIds]
|
||||||
|
properties:
|
||||||
|
planPalletIds: { type: array, minItems: 1, maxItems: 500, uniqueItems: true, items: { type: string } }
|
||||||
|
route: { type: string, nullable: true, maxLength: 255 }
|
||||||
|
fbaAppointmentNo: { type: string, nullable: true, maxLength: 64 }
|
||||||
|
fbaAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
||||||
|
vehicleAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
||||||
|
DispatchResponse:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
dispatchNo: { type: string }
|
||||||
|
vehiclePlate: { type: string, nullable: true }
|
||||||
|
driverName: { type: string, nullable: true }
|
||||||
|
driverPhone: { type: string, nullable: true }
|
||||||
|
carrierName: { type: string, nullable: true }
|
||||||
|
orderNos: { type: string }
|
||||||
|
route: { type: string, nullable: true }
|
||||||
|
overseasWarehouseId: { type: string }
|
||||||
|
overseasWarehouseName: { type: string }
|
||||||
|
departAt: { type: string, format: date-time, nullable: true }
|
||||||
|
fbaAppointmentNo: { type: string, nullable: true }
|
||||||
|
fbaAppointmentStatus: { type: string }
|
||||||
|
vehicleAppointmentStatus: { type: string }
|
||||||
|
estimatedArrivalAt: { type: string, format: date-time, nullable: true }
|
||||||
|
status: { type: string, description: 预排新建固定为 PENDING_DEPARTURE;查询可返回历史排车状态 }
|
||||||
|
itemCount: { type: integer }
|
||||||
|
palletCount: { type: integer }
|
||||||
|
packageCount: { type: integer }
|
||||||
|
pickedPackageCount: { type: integer }
|
||||||
|
shippedPackageCount: { type: integer }
|
||||||
|
remainingPackageCount: { type: integer }
|
||||||
|
partialCompletionReason: { type: string, nullable: true }
|
||||||
|
createdAt: { type: string, format: date-time }
|
||||||
|
items: { type: array, items: { $ref: '#/components/schemas/DispatchItem' } }
|
||||||
|
DispatchItem:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id: { type: string }
|
||||||
|
planPalletId: { type: string }
|
||||||
|
warehousePalletId: { type: string, nullable: true }
|
||||||
|
palletNo: { type: string }
|
||||||
|
overseasWarehouseId: { type: string }
|
||||||
|
overseasWarehouseName: { type: string }
|
||||||
|
orderNo: { type: string }
|
||||||
|
orderRefs: { type: string }
|
||||||
|
inboundCode: { type: string }
|
||||||
|
destinationWarehouseCode: { type: string }
|
||||||
|
destinationWarehouseName: { type: string }
|
||||||
|
fbaWarehouseCode: { type: string }
|
||||||
|
packageCount: { type: integer }
|
||||||
|
volumeCbm: { type: number }
|
||||||
|
sourceStage: { type: string, enum: [PLAN, RECEIVED] }
|
||||||
RelabelStatus: { type: string, enum: [PENDING_PROCESSING, PROCESSING, COMPLETED, REJECTED, CANCELLED] }
|
RelabelStatus: { type: string, enum: [PENDING_PROCESSING, PROCESSING, COMPLETED, REJECTED, CANCELLED] }
|
||||||
RelabelCreate:
|
RelabelCreate:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
+28
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.controller;
|
|||||||
import com.baiyue.lastleg.delivery.controller.DeliveryTransferController;
|
import com.baiyue.lastleg.delivery.controller.DeliveryTransferController;
|
||||||
import com.baiyue.lastleg.dispatch.controller.DispatchController;
|
import com.baiyue.lastleg.dispatch.controller.DispatchController;
|
||||||
import com.baiyue.lastleg.dispatch.controller.DispatchQueryController;
|
import com.baiyue.lastleg.dispatch.controller.DispatchQueryController;
|
||||||
|
import com.baiyue.lastleg.dispatch.controller.SmartDispatchController;
|
||||||
import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController;
|
import com.baiyue.lastleg.dispatch.pda.controller.DispatchPdaController;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPalletAssignmentController;
|
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPalletAssignmentController;
|
||||||
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPdaController;
|
import com.baiyue.lastleg.inboundplan.pda.controller.InboundPdaController;
|
||||||
@@ -11,10 +12,13 @@ import org.junit.jupiter.api.Test;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@@ -53,6 +57,30 @@ class Phase4BrowserApiBoundaryTest {
|
|||||||
.contains(annotation.annotationType().getSimpleName())));
|
.contains(annotation.annotationType().getSimpleName())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void dispatchQueryIsEnabledByDefaultWithoutEnablingOutboundCommands() throws Exception {
|
||||||
|
String yaml = Files.readString(Path.of("src/main/resources/application.yml"));
|
||||||
|
assertTrue(yaml.contains("enabled: ${LASTLEG_DISPATCH_QUERY_API_ENABLED:true}"));
|
||||||
|
assertTrue(yaml.contains("enabled: ${LASTLEG_SMART_DISPATCH_API_ENABLED:true}"));
|
||||||
|
assertFalse(yaml.contains("future-outbound-api:"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void smartDispatchIsIndependentlyEnabledWithOnlyCandidateAndPreArrangeEndpoints() {
|
||||||
|
ConditionalOnProperty condition = SmartDispatchController.class.getAnnotation(ConditionalOnProperty.class);
|
||||||
|
assertTrue(condition != null);
|
||||||
|
assertTrue(Arrays.asList(condition.name()).contains("lastleg.smart-dispatch-api.enabled"));
|
||||||
|
assertFalse(condition.matchIfMissing());
|
||||||
|
assertTrue(Arrays.stream(SmartDispatchController.class.getDeclaredMethods())
|
||||||
|
.filter(method -> method.isAnnotationPresent(GetMapping.class)).count() == 1);
|
||||||
|
assertTrue(Arrays.stream(SmartDispatchController.class.getDeclaredMethods())
|
||||||
|
.filter(method -> method.isAnnotationPresent(PostMapping.class)).count() == 1);
|
||||||
|
assertFalse(Arrays.stream(SmartDispatchController.class.getDeclaredMethods())
|
||||||
|
.flatMap(method -> Arrays.stream(method.getAnnotations()))
|
||||||
|
.anyMatch(annotation -> Set.of("PutMapping", "DeleteMapping", "PatchMapping")
|
||||||
|
.contains(annotation.annotationType().getSimpleName())));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformInventoryCommandsDoNotMapOutboundAllocationPickingOrShipping() {
|
void platformInventoryCommandsDoNotMapOutboundAllocationPickingOrShipping() {
|
||||||
Set<String> paths = Arrays.stream(InventoryCommandController.class.getDeclaredMethods())
|
Set<String> paths = Arrays.stream(InventoryCommandController.class.getDeclaredMethods())
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class SmartDispatchOpenApiContractTest {
|
||||||
|
@Test
|
||||||
|
void contractDocumentsOnlyCandidateAndPreArrangeSmartDispatchWrites() throws Exception {
|
||||||
|
String yaml;
|
||||||
|
try (var input = getClass().getResourceAsStream("/openapi/overseas-business-v1.yaml")) {
|
||||||
|
yaml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/candidates:"));
|
||||||
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/pre-arrange:"));
|
||||||
|
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:list"));
|
||||||
|
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:edit"));
|
||||||
|
assertTrue(yaml.contains("SmartDispatchPreArrange"));
|
||||||
|
assertTrue(yaml.contains("SmartDispatchCandidate"));
|
||||||
|
String dispatchContract = yaml.substring(0,
|
||||||
|
yaml.indexOf("/api/platform/v1/last-leg/overseas-procurements:"));
|
||||||
|
assertFalse(dispatchContract.contains("name: customerId"));
|
||||||
|
assertFalse(yaml.contains("/api/platform/v1/last-leg/dispatches/{dispatchNo}/depart:"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-3
@@ -4,12 +4,14 @@ import com.baiyue.common.core.api.PageResult;
|
|||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||||
|
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -32,13 +34,14 @@ class DispatchQueryControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformPageQueriesAllCustomersWithoutCustomerId() {
|
void platformPageUsesServerResolvedSingleCustomerWithoutCustomerId() {
|
||||||
DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null);
|
DispatchRequests.Query query = new DispatchRequests.Query(1, 20, null, null, null, null);
|
||||||
when(service.page(null, query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
when(service.resolveSingleCustomerScope()).thenReturn("100");
|
||||||
|
when(service.page("100", query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||||
|
|
||||||
var response = controller.page(platform(), query);
|
var response = controller.page(platform(), query);
|
||||||
|
|
||||||
verify(service).page(null, query);
|
verify(service).page("100", query);
|
||||||
assertThat(response.code()).isEqualTo("200");
|
assertThat(response.code()).isEqualTo("200");
|
||||||
assertThat(response.data().records()).isEmpty();
|
assertThat(response.data().records()).isEmpty();
|
||||||
assertThatThrownBy(() -> controller.page(null, query))
|
assertThatThrownBy(() -> controller.page(null, query))
|
||||||
@@ -47,6 +50,19 @@ class DispatchQueryControllerTest {
|
|||||||
((ResponseStatusException) error).getStatusCode().value()).isEqualTo(401));
|
((ResponseStatusException) error).getStatusCode().value()).isEqualTo(401));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listAndDetailReuseInboundOrderListAuthority() throws Exception {
|
||||||
|
PreAuthorize page = DispatchQueryController.class
|
||||||
|
.getMethod("page", AuthenticatedUser.class, DispatchRequests.Query.class)
|
||||||
|
.getAnnotation(PreAuthorize.class);
|
||||||
|
PreAuthorize detail = DispatchQueryController.class
|
||||||
|
.getMethod("detail", AuthenticatedUser.class, String.class)
|
||||||
|
.getAnnotation(PreAuthorize.class);
|
||||||
|
|
||||||
|
assertThat(page.value()).contains(OverseasPermissions.INBOUND_ORDER_LIST);
|
||||||
|
assertThat(detail.value()).contains(OverseasPermissions.INBOUND_ORDER_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
private static AuthenticatedUser platform() {
|
private static AuthenticatedUser platform() {
|
||||||
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB", Set.of());
|
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB", Set.of());
|
||||||
}
|
}
|
||||||
|
|||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
|
import com.baiyue.lastleg.dispatch.service.DispatchService;
|
||||||
|
import com.baiyue.lastleg.security.OverseasPermissions;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SmartDispatchControllerTest {
|
||||||
|
@Mock DispatchService service;
|
||||||
|
private SmartDispatchController controller;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
controller = new SmartDispatchController();
|
||||||
|
ReflectionTestUtils.setField(controller, "service", service);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void candidatesResolveSingleCustomerScopeWithoutRequestParameter() {
|
||||||
|
when(service.resolveSingleCustomerScope()).thenReturn("100");
|
||||||
|
when(service.candidates("100", "LMO-1", "ONT8", "PLAN")).thenReturn(List.of());
|
||||||
|
|
||||||
|
controller.candidates(platform(), "LMO-1", "ONT8", "PLAN");
|
||||||
|
|
||||||
|
verify(service).candidates("100", "LMO-1", "ONT8", "PLAN");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preArrangeResolvesSingleCustomerAndAuthenticatedOperator() {
|
||||||
|
DispatchRequests.PreArrange request = new DispatchRequests.PreArrange(
|
||||||
|
List.of(10L, 11L), "ONT8 -> LAX9", "APT-1", "SUCCESS", "PENDING");
|
||||||
|
when(service.resolveSingleCustomerScope()).thenReturn("100");
|
||||||
|
|
||||||
|
controller.preArrange(platform(), request);
|
||||||
|
|
||||||
|
verify(service).preArrange("100", "7", request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void endpointsReuseInboundOrderListAndEditAuthorities() throws Exception {
|
||||||
|
PreAuthorize candidates = SmartDispatchController.class
|
||||||
|
.getMethod("candidates", AuthenticatedUser.class, String.class, String.class, String.class)
|
||||||
|
.getAnnotation(PreAuthorize.class);
|
||||||
|
PreAuthorize preArrange = SmartDispatchController.class
|
||||||
|
.getMethod("preArrange", AuthenticatedUser.class, DispatchRequests.PreArrange.class)
|
||||||
|
.getAnnotation(PreAuthorize.class);
|
||||||
|
|
||||||
|
assertThat(candidates.value()).contains(OverseasPermissions.INBOUND_ORDER_LIST);
|
||||||
|
assertThat(preArrange.value()).contains(OverseasPermissions.INBOUND_ORDER_EDIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthenticatedUser platform() {
|
||||||
|
return new AuthenticatedUser(7L, 0L, 0L, 8L, "operator", "PLATFORM", "WEB",
|
||||||
|
Set.of(OverseasPermissions.INBOUND_ORDER_LIST, OverseasPermissions.INBOUND_ORDER_EDIT));
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-3
@@ -86,6 +86,28 @@ class DispatchServiceTest {
|
|||||||
verify(candidates, never()).available(any(), any(), any(), any());
|
verify(candidates, never()).available(any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolvesOnlyCustomerScopeAndRejectsMultipleCustomers() {
|
||||||
|
when(candidates.findCustomerScopes()).thenReturn(List.of("100"));
|
||||||
|
assertEquals("100", service.resolveSingleCustomerScope());
|
||||||
|
|
||||||
|
when(candidates.findCustomerScopes()).thenReturn(List.of("100", "200"));
|
||||||
|
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||||
|
() -> service.resolveSingleCustomerScope());
|
||||||
|
assertEquals(409, exception.getStatusCode().value());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyCustomerScopeProducesEmptyCandidatesAndRejectsPreArrange() {
|
||||||
|
assertEquals(List.of(), service.candidates(null, null, null, null));
|
||||||
|
verify(candidates, never()).available(any(), any(), any(), any());
|
||||||
|
|
||||||
|
ResponseStatusException exception = assertThrows(ResponseStatusException.class,
|
||||||
|
() -> service.preArrange(null, "7",
|
||||||
|
new DispatchRequests.PreArrange(List.of(10L), null, null, null, null)));
|
||||||
|
assertEquals(409, exception.getStatusCode().value());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void editCannotBypassPdaManagedDispatchStatus() {
|
void editCannotBypassPdaManagedDispatchStatus() {
|
||||||
DispatchOrder dispatch = new DispatchOrder();
|
DispatchOrder dispatch = new DispatchOrder();
|
||||||
@@ -105,14 +127,14 @@ class DispatchServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformDetailFindsDispatchGloballyAndLoadsItemsWithRecordCustomer() {
|
void detailUsesResolvedCustomerScopeAndLoadsItems() {
|
||||||
DispatchOrder dispatch = new DispatchOrder();
|
DispatchOrder dispatch = new DispatchOrder();
|
||||||
dispatch.setId(1L);
|
dispatch.setId(1L);
|
||||||
dispatch.setCustomerId("200");
|
dispatch.setCustomerId("200");
|
||||||
dispatch.setDispatchNo("PC1");
|
dispatch.setDispatchNo("PC1");
|
||||||
when(orders.findAnyByDispatchNo("PC1")).thenReturn(dispatch);
|
when(orders.findByDispatchNo("200", "PC1")).thenReturn(dispatch);
|
||||||
|
|
||||||
service.detail(null, "PC1");
|
service.detail("200", "PC1");
|
||||||
|
|
||||||
verify(items).findByDispatchOrder("200", 1L);
|
verify(items).findByDispatchOrder("200", 1L);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -759,6 +759,12 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab
|
|||||||
'ALTER TABLE wh_pallet MODIFY COLUMN pallet_code VARCHAR(128) NOT NULL COMMENT ''Warehouse pallet code''', 'SELECT 1'));
|
'ALTER TABLE wh_pallet MODIFY COLUMN pallet_code VARCHAR(128) NOT NULL COMMENT ''Warehouse pallet code''', 'SELECT 1'));
|
||||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||||
|
AND table_name='wh_pallet' AND column_name='fba_warehouse_code'),
|
||||||
|
'ALTER TABLE wh_pallet MODIFY COLUMN fba_warehouse_code VARCHAR(128) NULL COMMENT ''所属FBA仓编码''',
|
||||||
|
'ALTER TABLE wh_pallet ADD COLUMN fba_warehouse_code VARCHAR(128) NULL COMMENT ''所属FBA仓编码'' AFTER remark'));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
INSERT INTO wh_location (id, tenant_id, warehouse_id, zone_code, location_code, location_type,
|
INSERT INTO wh_location (id, tenant_id, warehouse_id, zone_code, location_code, location_type,
|
||||||
max_load_kg, status, remark)
|
max_load_kg, status, remark)
|
||||||
SELECT UUID_SHORT(), w.tenant_id, w.id, 'DEFAULT', CONCAT('LOC-', w.warehouse_code, '-DEFAULT'),
|
SELECT UUID_SHORT(), w.tenant_id, w.id, 'DEFAULT', CONCAT('LOC-', w.warehouse_code, '-DEFAULT'),
|
||||||
|
|||||||
+2
-1
@@ -99,7 +99,8 @@ public class PlatformWarehouseController {
|
|||||||
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform access required / 需要平台端权限")
|
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "403", description = "Platform access required / 需要平台端权限")
|
||||||
})
|
})
|
||||||
@GetMapping
|
@GetMapping
|
||||||
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.WAREHOUSE_LIST + "')")
|
@PreAuthorize("hasAnyAuthority('" + OverseasWarehousePermissions.WAREHOUSE_LIST + "', '"
|
||||||
|
+ OverseasWarehousePermissions.PALLET_LIST + "', '" + OverseasWarehousePermissions.LOCATION_LIST + "')")
|
||||||
public ApiResponse<PageResult<WarehouseEntity>> page(
|
public ApiResponse<PageResult<WarehouseEntity>> page(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@Valid WarehouseRequests.QueryRequest request) {
|
@Valid WarehouseRequests.QueryRequest request) {
|
||||||
|
|||||||
+7
-1
@@ -22,8 +22,14 @@ public final class PalletRequests {
|
|||||||
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
@NotNull @DecimalMin("0.00") BigDecimal unitPrice,
|
||||||
@NotBlank String status,
|
@NotBlank String status,
|
||||||
Long locationId,
|
Long locationId,
|
||||||
@Size(max = 500) String remark
|
@Size(max = 500) String remark,
|
||||||
|
@Size(max = 128) String fbaWarehouseCode
|
||||||
) {
|
) {
|
||||||
|
public SaveRequest(String palletCode, String palletType, BigDecimal capacityCbm,
|
||||||
|
BigDecimal unitPrice, String status, Long locationId, String remark) {
|
||||||
|
this(palletCode, palletType, capacityCbm, unitPrice, status, locationId, remark, null);
|
||||||
|
}
|
||||||
|
|
||||||
public SaveRequest {
|
public SaveRequest {
|
||||||
if (capacityCbm == null) capacityCbm = DEFAULT_CAPACITY;
|
if (capacityCbm == null) capacityCbm = DEFAULT_CAPACITY;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -24,4 +24,5 @@ public class WarehousePalletEntity extends BaseEntity {
|
|||||||
private BigDecimal unitPrice;
|
private BigDecimal unitPrice;
|
||||||
private String status;
|
private String status;
|
||||||
private String remark;
|
private String remark;
|
||||||
|
private String fbaWarehouseCode;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -276,6 +276,7 @@ public class WarehouseResourceService {
|
|||||||
entity.setUnitPrice(request.unitPrice());
|
entity.setUnitPrice(request.unitPrice());
|
||||||
entity.setStatus(request.status());
|
entity.setStatus(request.status());
|
||||||
entity.setRemark(request.remark());
|
entity.setRemark(request.remark());
|
||||||
|
entity.setFbaWarehouseCode(request.fbaWarehouseCode());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
|
|||||||
unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'IDLE' COMMENT '托盘状态:IDLE闲置、OCCUPIED占用、SCRAPPED废弃',
|
status VARCHAR(20) NOT NULL DEFAULT 'IDLE' COMMENT '托盘状态:IDLE闲置、OCCUPIED占用、SCRAPPED废弃',
|
||||||
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
remark VARCHAR(500) DEFAULT NULL COMMENT '备注',
|
||||||
|
fba_warehouse_code VARCHAR(128) DEFAULT NULL COMMENT '所属FBA仓编码',
|
||||||
created_by BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
created_by BIGINT UNSIGNED DEFAULT NULL COMMENT '创建人ID',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
updated_by BIGINT UNSIGNED DEFAULT NULL COMMENT '最后修改人ID',
|
updated_by BIGINT UNSIGNED DEFAULT NULL COMMENT '最后修改人ID',
|
||||||
@@ -49,6 +50,14 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
|
|||||||
KEY idx_pallet_location (customer_id, warehouse_id, location_id, deleted)
|
KEY idx_pallet_location (customer_id, warehouse_id, location_id, deleted)
|
||||||
) ENGINE=InnoDB COMMENT='仓库托盘表:按租户和仓库维护托盘、容量、单价及状态';
|
) ENGINE=InnoDB COMMENT='仓库托盘表:按租户和仓库维护托盘、容量、单价及状态';
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE()
|
||||||
|
AND table_name='wh_pallet' AND column_name='fba_warehouse_code'),
|
||||||
|
'SELECT 1',
|
||||||
|
'ALTER TABLE wh_pallet ADD COLUMN fba_warehouse_code VARCHAR(128) NULL COMMENT ''所属FBA仓编码'' AFTER remark'));
|
||||||
|
PREPARE pallet_fba_warehouse_stmt FROM @ddl;
|
||||||
|
EXECUTE pallet_fba_warehouse_stmt;
|
||||||
|
DEALLOCATE PREPARE pallet_fba_warehouse_stmt;
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS wh_resource_lock (
|
CREATE TABLE IF NOT EXISTS wh_resource_lock (
|
||||||
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
id BIGINT UNSIGNED NOT NULL COMMENT '主键ID',
|
||||||
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户租户ID',
|
customer_id BIGINT UNSIGNED NOT NULL COMMENT '客户租户ID',
|
||||||
@@ -193,6 +202,7 @@ ALTER TABLE wh_pallet
|
|||||||
MODIFY COLUMN unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
MODIFY COLUMN unit_price DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT '托盘单价',
|
||||||
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'IDLE' COMMENT '托盘状态:IDLE闲置、OCCUPIED占用、SCRAPPED废弃',
|
MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'IDLE' COMMENT '托盘状态:IDLE闲置、OCCUPIED占用、SCRAPPED废弃',
|
||||||
MODIFY COLUMN remark VARCHAR(500) NULL COMMENT '备注',
|
MODIFY COLUMN remark VARCHAR(500) NULL COMMENT '备注',
|
||||||
|
MODIFY COLUMN fba_warehouse_code VARCHAR(128) NULL COMMENT '所属FBA仓编码',
|
||||||
MODIFY COLUMN created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
|
MODIFY COLUMN created_by BIGINT UNSIGNED NULL COMMENT '创建人ID',
|
||||||
MODIFY COLUMN created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
MODIFY COLUMN created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
MODIFY COLUMN updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID',
|
MODIFY COLUMN updated_by BIGINT UNSIGNED NULL COMMENT '最后修改人ID',
|
||||||
|
|||||||
+14
@@ -3,8 +3,10 @@ package com.baiyue.warehouse.controller;
|
|||||||
import com.baiyue.common.core.api.PageResult;
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.warehouse.dto.WarehouseRequests;
|
import com.baiyue.warehouse.dto.WarehouseRequests;
|
||||||
|
import com.baiyue.warehouse.security.OverseasWarehousePermissions;
|
||||||
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
import com.baiyue.warehouse.service.WarehouseApplicationService;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -13,8 +15,20 @@ import java.util.Set;
|
|||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
class PlatformWarehouseControllerTest {
|
class PlatformWarehouseControllerTest {
|
||||||
|
@Test
|
||||||
|
void platformPageAllowsWarehousePalletAndLocationListAuthorities() throws Exception {
|
||||||
|
PreAuthorize preAuthorize = PlatformWarehouseController.class
|
||||||
|
.getMethod("page", AuthenticatedUser.class, WarehouseRequests.QueryRequest.class)
|
||||||
|
.getAnnotation(PreAuthorize.class);
|
||||||
|
|
||||||
|
assertEquals("hasAnyAuthority('" + OverseasWarehousePermissions.WAREHOUSE_LIST + "', '"
|
||||||
|
+ OverseasWarehousePermissions.PALLET_LIST + "', '" + OverseasWarehousePermissions.LOCATION_LIST + "')",
|
||||||
|
preAuthorize.value());
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformPageQueriesAllWarehousesWithoutCustomerId() {
|
void platformPageQueriesAllWarehousesWithoutCustomerId() {
|
||||||
WarehouseApplicationService service = mock(WarehouseApplicationService.class);
|
WarehouseApplicationService service = mock(WarehouseApplicationService.class);
|
||||||
|
|||||||
+38
@@ -267,6 +267,44 @@ class WarehouseResourceServiceTest {
|
|||||||
.hasMessageContaining("cannot be reused");
|
.hasMessageContaining("cannot be reused");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatePalletChangesCodeAndFbaWarehouseCode() {
|
||||||
|
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||||
|
WarehousePalletEntity existing = new WarehousePalletEntity();
|
||||||
|
existing.setId(40003L);
|
||||||
|
existing.setPalletCode("PLT-OLD");
|
||||||
|
existing.setStatus("IDLE");
|
||||||
|
when(repository.findPallet(TENANT, WAREHOUSE, 40003L)).thenReturn(existing);
|
||||||
|
|
||||||
|
service.updatePallet(TENANT, WAREHOUSE, 40003L,
|
||||||
|
new PalletRequests.SaveRequest("PLT-NEW", "WOOD", new BigDecimal("2.5000"),
|
||||||
|
new BigDecimal("25.00"), "IDLE", null, "updated", "ONT8"));
|
||||||
|
|
||||||
|
ArgumentCaptor<WarehousePalletEntity> captor = ArgumentCaptor.forClass(WarehousePalletEntity.class);
|
||||||
|
verify(repository).palletCodeExists(TENANT, WAREHOUSE, "PLT-NEW", 40003L);
|
||||||
|
verify(repository).updatePallet(captor.capture());
|
||||||
|
assertThat(captor.getValue().getPalletCode()).isEqualTo("PLT-NEW");
|
||||||
|
assertThat(captor.getValue().getFbaWarehouseCode()).isEqualTo("ONT8");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatePalletRejectsDuplicateCodeInSameWarehouse() {
|
||||||
|
when(repository.warehouseOwned(TENANT, WAREHOUSE)).thenReturn(true);
|
||||||
|
WarehousePalletEntity existing = new WarehousePalletEntity();
|
||||||
|
existing.setId(40004L);
|
||||||
|
existing.setPalletCode("PLT-OLD");
|
||||||
|
existing.setStatus("IDLE");
|
||||||
|
when(repository.findPallet(TENANT, WAREHOUSE, 40004L)).thenReturn(existing);
|
||||||
|
when(repository.palletCodeExists(TENANT, WAREHOUSE, "PLT-DUP", 40004L)).thenReturn(true);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.updatePallet(TENANT, WAREHOUSE, 40004L,
|
||||||
|
new PalletRequests.SaveRequest("PLT-DUP", "WOOD", BigDecimal.ONE,
|
||||||
|
BigDecimal.ONE, "IDLE", null, null, "ONT8")))
|
||||||
|
.isInstanceOf(BizException.class)
|
||||||
|
.hasMessageContaining("托盘编码已存在");
|
||||||
|
verify(repository, never()).updatePallet(org.mockito.ArgumentMatchers.any());
|
||||||
|
}
|
||||||
|
|
||||||
private static WarehouseEntity warehouse() {
|
private static WarehouseEntity warehouse() {
|
||||||
WarehouseEntity warehouse = new WarehouseEntity();
|
WarehouseEntity warehouse = new WarehouseEntity();
|
||||||
warehouse.setId(WAREHOUSE);
|
warehouse.setId(WAREHOUSE);
|
||||||
|
|||||||
Reference in New Issue
Block a user