修改提交
This commit is contained in:
@@ -16194,6 +16194,41 @@ WHERE role.tenant_id=0 AND role.app_code='PLATFORM'
|
|||||||
AND UPPER(role.role_code) IN ('PLATFORM_ADMIN','PLATFORM_CUSTOM_ADMIN')
|
AND UPPER(role.role_code) IN ('PLATFORM_ADMIN','PLATFORM_CUSTOM_ADMIN')
|
||||||
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
AND role.status='ACTIVE' AND role.deleted=0 AND permission.status='ACTIVE' AND permission.deleted=0;
|
||||||
|
|
||||||
|
-- 海外智能排车草稿锁:拖入即锁定,移除或关闭草稿即释放。
|
||||||
|
CREATE DATABASE IF NOT EXISTS baiyue_lastleg
|
||||||
|
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||||
|
USE baiyue_lastleg;
|
||||||
|
CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
|
||||||
|
id BIGINT NOT NULL COMMENT '主键ID',
|
||||||
|
customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
|
||||||
|
source_pallet_key VARCHAR(160) NOT NULL COMMENT '来源托盘业务键',
|
||||||
|
active_key VARCHAR(160) NULL COMMENT '生效中的资源排他键',
|
||||||
|
plan_id BIGINT NULL COMMENT '入仓计划ID',
|
||||||
|
plan_pallet_id BIGINT NULL COMMENT '计划托盘ID',
|
||||||
|
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID',
|
||||||
|
dispatch_order_id BIGINT NULL COMMENT '正式派车单ID,草稿锁阶段为空',
|
||||||
|
draft_token VARCHAR(64) NULL COMMENT '智能排车草稿随机标识',
|
||||||
|
operator_id VARCHAR(128) NULL COMMENT '草稿锁操作人ID',
|
||||||
|
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态',
|
||||||
|
locked_at DATETIME(3) NOT NULL COMMENT '锁定时间',
|
||||||
|
expires_at DATETIME(3) NULL COMMENT '草稿锁失效时间,正式派车锁为空',
|
||||||
|
released_at DATETIME(3) NULL COMMENT '释放时间',
|
||||||
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY uk_ll_dispatch_active_pallet (customer_id,active_key),
|
||||||
|
KEY idx_ll_dispatch_pallet_lock_order (customer_id,dispatch_order_id,status),
|
||||||
|
KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status)
|
||||||
|
) ENGINE=InnoDB COMMENT='派车托盘排他锁';
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='draft_token'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN draft_token VARCHAR(64) NULL COMMENT ''智能排车草稿随机标识'' AFTER dispatch_order_id'));
|
||||||
|
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='ll_dispatch_pallet_lock' AND column_name='operator_id'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN operator_id VARCHAR(128) NULL COMMENT ''草稿锁操作人ID'' AFTER draft_token'));
|
||||||
|
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='ll_dispatch_pallet_lock' AND column_name='expires_at'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN expires_at DATETIME(3) NULL COMMENT ''草稿锁失效时间,正式派车锁为空'' AFTER locked_at'));
|
||||||
|
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='ll_dispatch_pallet_lock' AND column_name='dispatch_order_id' AND is_nullable='YES'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock MODIFY COLUMN dispatch_order_id BIGINT NULL COMMENT ''正式派车单ID,草稿锁阶段为空'''));
|
||||||
|
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
CALL baiyue_firstleg.assert_unified_final_schema();
|
CALL baiyue_firstleg.assert_unified_final_schema();
|
||||||
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
DROP PROCEDURE IF EXISTS baiyue_firstleg.assert_unified_final_schema;
|
||||||
SELECT 'PASS' AS migration_status,
|
SELECT 'PASS' AS migration_status,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
- 四期在本服务内只进入尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、海外采购和本土卡车;本土散板新增实现也归本服务的独立稳定子域。
|
- 四期在本服务内只进入尾程订单、入仓计划/PDA 入仓、换标、产品与箱唛快照、库存、海外采购和本土卡车;本土散板新增实现也归本服务的独立稳定子域。
|
||||||
- `dispatch/`、`delivery/`、出库单、PDA 出库拣货、派送和其他库存离仓能力虽然已存在于服务代码或表结构中,但不属于当前四期开发与验收范围,不得因代码已存在而自动纳入。
|
- `dispatch/`、`delivery/`、出库单、PDA 出库拣货、派送和其他库存离仓能力虽然已存在于服务代码或表结构中,但不属于当前四期开发与验收范围,不得因代码已存在而自动纳入。
|
||||||
- 历史排车编辑/取消/出库、PDA 出库和 `delivery/` Controller 默认不注册;只有后续阶段明确启用 `lastleg.future-outbound-api.enabled=true` 才可恢复。平台排车列表与详情由 `lastleg.dispatch-query-api.enabled=true` 独立启用;入库单智能排车候选和预排创建由 `lastleg.smart-dispatch-api.enabled=true` 独立启用。四个排车平台入口均不接收 `customerId`:排车列表/详情从既有排车数据解析范围,智能排车候选根据已选入库计划 ID、预排创建根据所选托盘 ID 在服务端解析并校验唯一客户范围;预排只能创建待出库排车并锁定托盘,不得借此开放编辑、取消、depart、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`:排车列表跨客户分页,详情按全局排车单号定位后使用记录自身客户范围加载明细;智能排车候选未传计划 ID 时跨客户查询,传入时根据计划 ID 解析范围;托盘移入草稿时立即以操作人和随机草稿标识锁定,移除或关闭时释放,草稿锁超时失效;预排创建根据所选托盘 ID 在服务端解析并校验唯一客户范围,并将当前操作人的草稿锁原子转换为正式排车锁。预排只能创建待出库排车并锁定托盘,不得借此开放编辑、取消、depart、PDA 出库或派送。四期平台库存 Controller 不映射 allocate、unallocate、pick、ship。
|
||||||
- 海外仓库、库位、实物托盘和资源锁归 `warehouse-overseas-service/`;浏览器 API 统一经 `gateway-overseas/`,本服务不得复制仓储资源主数据或另建公网认证入口。
|
- 海外仓库、库位、实物托盘和资源锁归 `warehouse-overseas-service/`;浏览器 API 统一经 `gateway-overseas/`,本服务不得复制仓储资源主数据或另建公网认证入口。
|
||||||
|
|
||||||
## 数据库表索引与注释约束
|
## 数据库表索引与注释约束
|
||||||
|
|||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
package com.baiyue.lastleg.discard.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||||
|
import com.baiyue.lastleg.discard.dto.CargoDiscardResponses;
|
||||||
|
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/merchant/v1/last-leg/cargo-discards")
|
||||||
|
public class MerchantCargoDiscardController {
|
||||||
|
@Resource private CargoDiscardService service;
|
||||||
|
@GetMapping("/inventory-candidates")
|
||||||
|
public ApiResponse<List<CargoDiscardResponses.Candidate>> candidates(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) String inboundCode) { return ApiResponse.success(service.candidates(customer(user), inboundCode)); }
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<CargoDiscardResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody CargoDiscardRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<CargoDiscardResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid CargoDiscardRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||||
|
@GetMapping("/{discardNo}")
|
||||||
|
public ApiResponse<CargoDiscardResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String discardNo) { return ApiResponse.success(service.detail(customer(user), discardNo)); }
|
||||||
|
private String customer(AuthenticatedUser user) {
|
||||||
|
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||||
|
return String.valueOf(user.tenantId());
|
||||||
|
}
|
||||||
|
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||||
|
}
|
||||||
+2
-2
@@ -35,7 +35,7 @@ public class DispatchQueryController {
|
|||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@Valid DispatchRequests.Query query) {
|
@Valid DispatchRequests.Query query) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.page(service.resolveSingleCustomerScope(), query));
|
return ApiResponse.success(service.platformPage(query));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{dispatchNo}")
|
@GetMapping("/{dispatchNo}")
|
||||||
@@ -45,7 +45,7 @@ public class DispatchQueryController {
|
|||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable String dispatchNo) {
|
@PathVariable String dispatchNo) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
return ApiResponse.success(service.detail(service.resolveSingleCustomerScope(), dispatchNo));
|
return ApiResponse.success(service.platformDetail(dispatchNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requirePlatform(AuthenticatedUser user) {
|
private void requirePlatform(AuthenticatedUser user) {
|
||||||
|
|||||||
+60
-3
@@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.PostMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
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;
|
||||||
|
|
||||||
@@ -38,13 +39,30 @@ public class SmartDispatchController {
|
|||||||
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
description = "Required authority: " + OverseasPermissions.INBOUND_ORDER_LIST)
|
||||||
public ApiResponse<List<DispatchPalletCandidate>> candidates(
|
public ApiResponse<List<DispatchPalletCandidate>> candidates(
|
||||||
@AuthenticationPrincipal AuthenticatedUser user,
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam List<Long> planIds,
|
@RequestParam(required = false) List<Long> planIds,
|
||||||
@RequestParam(required = false) String orderNo,
|
@RequestParam(required = false) String orderNo,
|
||||||
@RequestParam(required = false) String fbaWarehouseCode,
|
@RequestParam(required = false) String fbaWarehouseCode,
|
||||||
@RequestParam(required = false) String sourceStage) {
|
@RequestParam(required = false) String sourceStage,
|
||||||
|
@RequestParam(required = false) String draftToken) {
|
||||||
requirePlatform(user);
|
requirePlatform(user);
|
||||||
|
if (planIds == null || planIds.isEmpty()) {
|
||||||
|
return ApiResponse.success(service.platformCandidates(orderNo, fbaWarehouseCode, sourceStage, draftToken));
|
||||||
|
}
|
||||||
return ApiResponse.success(service.candidates(service.resolveCustomerScopeForPlans(planIds), planIds, orderNo,
|
return ApiResponse.success(service.candidates(service.resolveCustomerScopeForPlans(planIds), planIds, orderNo,
|
||||||
fbaWarehouseCode, sourceStage));
|
fbaWarehouseCode, sourceStage, draftToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/drafts")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
|
||||||
|
public ApiResponse<List<com.baiyue.lastleg.dispatch.dto.DispatchDraftResponse>> drafts(@AuthenticationPrincipal AuthenticatedUser user) {
|
||||||
|
requirePlatform(user); return ApiResponse.success(service.activeDrafts(operatorId(user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/drafts/snapshot")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
|
||||||
|
public ApiResponse<Void> saveDraft(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody DispatchRequests.DraftSnapshot request) {
|
||||||
|
requirePlatform(user); service.saveDraftSnapshot(operatorId(user), request); return ApiResponse.success(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/pre-arrange")
|
@PostMapping("/pre-arrange")
|
||||||
@@ -60,6 +78,45 @@ public class SmartDispatchController {
|
|||||||
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
|
return ApiResponse.success(service.preArrange(scopedCustomer, operatorId, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/draft-locks/lock")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
|
||||||
|
@Operation(summary = "Immediately lock one pallet in the current intelligent dispatch draft")
|
||||||
|
public ApiResponse<Void> lockDraftPallet(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody DispatchRequests.DraftPallet request) {
|
||||||
|
requirePlatform(user);
|
||||||
|
String customerId = service.resolveCustomerScopeForPallets(List.of(request.planPalletId()));
|
||||||
|
service.lockDraftPallet(customerId, operatorId(user), request);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/draft-locks/unlock")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
|
||||||
|
@Operation(summary = "Unlock one pallet removed from the current intelligent dispatch draft")
|
||||||
|
public ApiResponse<Void> unlockDraftPallet(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody DispatchRequests.DraftPallet request) {
|
||||||
|
requirePlatform(user);
|
||||||
|
String customerId = service.resolveCustomerScopeForPallets(List.of(request.planPalletId()));
|
||||||
|
service.unlockDraftPallet(customerId, operatorId(user), request);
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/draft-locks/release")
|
||||||
|
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
|
||||||
|
@Operation(summary = "Release every pallet lock held by the current intelligent dispatch draft")
|
||||||
|
public ApiResponse<Void> releaseDraft(
|
||||||
|
@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody DispatchRequests.DraftRelease request) {
|
||||||
|
requirePlatform(user);
|
||||||
|
service.releaseDraft(operatorId(user), request.draftToken());
|
||||||
|
return ApiResponse.success(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String operatorId(AuthenticatedUser user) {
|
||||||
|
return user.userId() == null ? user.username() : String.valueOf(user.userId());
|
||||||
|
}
|
||||||
|
|
||||||
private void requirePlatform(AuthenticatedUser user) {
|
private void requirePlatform(AuthenticatedUser user) {
|
||||||
if (user == null || user.tenantId() == null) {
|
if (user == null || user.tenantId() == null) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch.dto;
|
||||||
|
|
||||||
|
public record DispatchDraftResponse(String draftToken, String planIdsJson, String containersJson) {
|
||||||
|
}
|
||||||
+18
-1
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.dispatch.dto;
|
|||||||
import jakarta.validation.constraints.Max;
|
import jakarta.validation.constraints.Max;
|
||||||
import jakarta.validation.constraints.Min;
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
|
|
||||||
@@ -21,7 +22,23 @@ public final class DispatchRequests {
|
|||||||
public record PreArrange(@NotEmpty @Size(max = 500) List<@NotNull Long> planPalletIds,
|
public record PreArrange(@NotEmpty @Size(max = 500) List<@NotNull Long> planPalletIds,
|
||||||
@Size(max = 255) String route, @Size(max = 64) String fbaAppointmentNo,
|
@Size(max = 255) String route, @Size(max = 64) String fbaAppointmentNo,
|
||||||
@Size(max = 32) String fbaAppointmentStatus,
|
@Size(max = 32) String fbaAppointmentStatus,
|
||||||
@Size(max = 32) String vehicleAppointmentStatus) {
|
@Size(max = 32) String vehicleAppointmentStatus,
|
||||||
|
@Size(max = 64) String draftToken) {
|
||||||
|
public PreArrange(List<Long> planPalletIds, String route, String fbaAppointmentNo,
|
||||||
|
String fbaAppointmentStatus, String vehicleAppointmentStatus) {
|
||||||
|
this(planPalletIds, route, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DraftPallet(@NotNull Long planPalletId, @NotBlank @Size(max = 64) String draftToken) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DraftRelease(@NotBlank @Size(max = 64) String draftToken) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record DraftSnapshot(@NotBlank @Size(max = 64) String draftToken,
|
||||||
|
@NotEmpty @Size(max = 500) List<@NotNull Long> planIds,
|
||||||
|
@NotBlank @Size(max = 20000) String containersJson) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName,
|
public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName,
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class DispatchDraft {
|
||||||
|
@TableId(type = IdType.INPUT)
|
||||||
|
private Long id;
|
||||||
|
private String draftToken;
|
||||||
|
private String customerId;
|
||||||
|
private String operatorId;
|
||||||
|
private String planIdsJson;
|
||||||
|
private String containersJson;
|
||||||
|
private String status;
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
|
private LocalDateTime releasedAt;
|
||||||
|
private LocalDateTime createdAt;
|
||||||
|
private LocalDateTime updatedAt;
|
||||||
|
}
|
||||||
+8
@@ -1,7 +1,9 @@
|
|||||||
package com.baiyue.lastleg.dispatch.entity;
|
package com.baiyue.lastleg.dispatch.entity;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableName;
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -16,13 +18,19 @@ public class DispatchPalletLock {
|
|||||||
private String customerId;
|
private String customerId;
|
||||||
private String sourcePalletKey;
|
private String sourcePalletKey;
|
||||||
/** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */
|
/** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
private String activeKey;
|
private String activeKey;
|
||||||
private Long planId;
|
private Long planId;
|
||||||
private Long planPalletId;
|
private Long planPalletId;
|
||||||
private Long warehousePalletId;
|
private Long warehousePalletId;
|
||||||
private Long dispatchOrderId;
|
private Long dispatchOrderId;
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private String draftToken;
|
||||||
|
private String operatorId;
|
||||||
private String status;
|
private String status;
|
||||||
private LocalDateTime lockedAt;
|
private LocalDateTime lockedAt;
|
||||||
|
@TableField(updateStrategy = FieldStrategy.ALWAYS)
|
||||||
|
private LocalDateTime expiresAt;
|
||||||
private LocalDateTime releasedAt;
|
private LocalDateTime releasedAt;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|||||||
+43
-18
@@ -9,12 +9,6 @@ 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 DISTINCT customer_id FROM ll_inbound_plan WHERE id IN " +
|
"SELECT DISTINCT customer_id FROM ll_inbound_plan WHERE id IN " +
|
||||||
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||||
@@ -32,35 +26,51 @@ public interface DispatchCandidateMapper {
|
|||||||
|
|
||||||
@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(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," +
|
||||||
|
"MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," +
|
||||||
|
"MIN(ip.container_no) AS containerNo," +
|
||||||
|
"(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||||
|
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||||
|
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS appointmentAt," +
|
||||||
|
"(SELECT MAX(fa.expected_loading_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||||
|
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||||
|
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||||
|
"MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||||
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
"MIN(l.fba_warehouse_code) AS fbaWarehouseCode," +
|
||||||
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
"CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN COALESCE((SELECT SUM(pos.available_packages) " +
|
||||||
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
"FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
|
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage," +
|
||||||
|
"(SELECT GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') " +
|
||||||
|
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id) AS cartonMarks " +
|
||||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||||
"WHERE ip.customer_id=#{customerId} AND p.plan_id IN " +
|
"WHERE 1=1 " +
|
||||||
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
"<if test='customerId != null and customerId != \"\"'> AND ip.customer_id=#{customerId}</if> " +
|
||||||
|
"<if test='planIds != null and planIds.size() > 0'> AND p.plan_id IN " +
|
||||||
|
"<foreach collection='planIds' item='id' open='(' separator=',' close=')'>#{id}</foreach></if> " +
|
||||||
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
|
"<if test='orderNo != null and orderNo != \"\"'> AND o.order_no LIKE CONCAT('%',#{orderNo},'%')</if> " +
|
||||||
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
|
"<if test='fbaWarehouseCode != null and fbaWarehouseCode != \"\"'> AND l.fba_warehouse_code=#{fbaWarehouseCode}</if> " +
|
||||||
"<if test='sourceStage != null and sourceStage != \"\"'> AND " +
|
"<if test='sourceStage != null and sourceStage != \"\"'> AND " +
|
||||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " +
|
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END=#{sourceStage}</if> " +
|
||||||
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||||
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
"WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " +
|
||||||
"WHERE d.customer_id=ip.customer_id AND d.status='ACTIVE' AND (" +
|
"WHERE d.customer_id=ip.customer_id AND d.status='ACTIVE' " +
|
||||||
|
"AND (d.expires_at IS NULL OR d.expires_at > CURRENT_TIMESTAMP(3)) AND (" +
|
||||||
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
"d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " +
|
||||||
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id)))) " +
|
"d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " +
|
||||||
|
"AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
"WHERE ta.customer_id=ip.customer_id AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||||
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
"GROUP BY p.plan_id,p.id,p.warehouse_pallet_id,p.pallet_no,p.destination_warehouse_id," +
|
||||||
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
"p.destination_warehouse_code,p.destination_warehouse_name,p.allocated_volume_cbm " +
|
||||||
"ORDER BY p.plan_id,p.sequence_no" +
|
"ORDER BY p.plan_id,p.sequence_no" +
|
||||||
@@ -69,11 +79,22 @@ public interface DispatchCandidateMapper {
|
|||||||
@Param("planIds") List<Long> planIds,
|
@Param("planIds") List<Long> planIds,
|
||||||
@Param("orderNo") String orderNo,
|
@Param("orderNo") String orderNo,
|
||||||
@Param("fbaWarehouseCode") String fbaWarehouseCode,
|
@Param("fbaWarehouseCode") String fbaWarehouseCode,
|
||||||
@Param("sourceStage") String sourceStage);
|
@Param("sourceStage") String sourceStage,
|
||||||
|
@Param("draftToken") String draftToken);
|
||||||
|
|
||||||
@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(ip.customer_id) AS customerId,MIN(o.customer_name) AS customerName," +
|
||||||
|
"MIN(ip.plan_no) AS planNo,MIN(o.order_type) AS orderType,MIN(o.volume_weight_kg) AS volumeWeightKg," +
|
||||||
|
"MIN(ip.container_no) AS containerNo," +
|
||||||
|
"(SELECT MAX(fa.appointment_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||||
|
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||||
|
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS appointmentAt," +
|
||||||
|
"(SELECT MAX(fa.expected_loading_at) FROM ll_inbound_fba_appointment_item fi " +
|
||||||
|
"JOIN ll_inbound_fba_appointment fa ON fa.id=fi.appointment_id WHERE fi.line_id IN " +
|
||||||
|
"(SELECT pa.line_id FROM ll_inbound_plan_pallet_allocation pa WHERE pa.pallet_id=p.id)) AS expectedLoadingAt," +
|
||||||
|
"MIN(o.overseas_warehouse_id) AS overseasWarehouseId,MIN(o.overseas_warehouse_name) AS overseasWarehouseName,MIN(o.order_no) AS orderNo," +
|
||||||
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
"GROUP_CONCAT(DISTINCT o.order_no ORDER BY o.order_no SEPARATOR ',') AS orderRefs,MIN(l.inbound_code) AS inboundCode," +
|
||||||
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
"p.pallet_no AS palletNo,p.destination_warehouse_id AS destinationWarehouseId," +
|
||||||
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
"p.destination_warehouse_code AS destinationWarehouseCode,p.destination_warehouse_name AS destinationWarehouseName," +
|
||||||
@@ -82,13 +103,17 @@ public interface DispatchCandidateMapper {
|
|||||||
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
"FROM ll_inventory_position pos WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id),0) " +
|
||||||
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
"ELSE COALESCE(SUM(CASE WHEN a.picked_package_count > 0 THEN a.picked_package_count " +
|
||||||
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
"ELSE a.planned_package_count END),0) END AS packageCount,p.allocated_volume_cbm AS volumeCbm," +
|
||||||
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage " +
|
"CASE WHEN p.warehouse_pallet_id IS NULL THEN 'PLAN' ELSE 'RECEIVED' END AS sourceStage," +
|
||||||
|
"(SELECT GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') " +
|
||||||
|
"FROM ll_inbound_plan_carton c WHERE c.plan_pallet_id=p.id) AS cartonMarks " +
|
||||||
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
"FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id " +
|
||||||
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
"LEFT JOIN ll_inbound_plan_pallet_allocation a ON a.pallet_id=p.id " +
|
||||||
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
"LEFT JOIN ll_inbound_plan_line l ON l.id=a.line_id " +
|
||||||
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
"LEFT JOIN ll_order o ON o.id=l.order_id " +
|
||||||
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
"WHERE ip.customer_id=#{customerId} AND p.id IN " +
|
||||||
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
"<foreach collection='planPalletIds' item='id' open='(' separator=',' close=')'>#{id}</foreach> " +
|
||||||
|
"AND (p.warehouse_pallet_id IS NULL OR EXISTS (SELECT 1 FROM ll_inventory_position pos " +
|
||||||
|
"WHERE pos.customer_id=#{customerId} AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " +
|
||||||
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
"AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " +
|
||||||
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
"JOIN ll_inventory_position tp ON tp.id=ta.inventory_position_id " +
|
||||||
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
"WHERE ta.customer_id=#{customerId} AND ta.status='RESERVED' AND tp.pallet_id=p.warehouse_pallet_id) " +
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch.mapper;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
import com.baiyue.lastleg.dispatch.entity.DispatchDraft;
|
||||||
|
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 DispatchDraftMapper extends BaseMapper<DispatchDraft> {
|
||||||
|
@Select("SELECT * FROM ll_dispatch_draft WHERE operator_id=#{operatorId} AND status='ACTIVE' AND (expires_at IS NULL OR expires_at>CURRENT_TIMESTAMP(3)) ORDER BY updated_at DESC")
|
||||||
|
List<DispatchDraft> findActiveByOperator(@Param("operatorId") String operatorId);
|
||||||
|
|
||||||
|
@Select("SELECT * FROM ll_dispatch_draft WHERE draft_token=#{draftToken} AND operator_id=#{operatorId} AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
||||||
|
DispatchDraft lockActive(@Param("draftToken") String draftToken, @Param("operatorId") String operatorId);
|
||||||
|
}
|
||||||
+5
@@ -6,11 +6,16 @@ import org.apache.ibatis.annotations.Mapper;
|
|||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface DispatchOrderMapper extends BaseMapper<DispatchOrder> {
|
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} ORDER BY customer_id LIMIT 2")
|
||||||
|
List<DispatchOrder> findAcrossCustomersByDispatchNo(@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);
|
||||||
}
|
}
|
||||||
|
|||||||
+6
@@ -5,6 +5,7 @@ import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
|||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
import org.apache.ibatis.annotations.Select;
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
|
||||||
@Mapper
|
@Mapper
|
||||||
public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock> {
|
public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock> {
|
||||||
@@ -12,4 +13,9 @@ public interface DispatchPalletLockMapper extends BaseMapper<DispatchPalletLock>
|
|||||||
"AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
"AND status='ACTIVE' LIMIT 1 FOR UPDATE")
|
||||||
DispatchPalletLock lockActive(@Param("customerId") String customerId,
|
DispatchPalletLock lockActive(@Param("customerId") String customerId,
|
||||||
@Param("sourcePalletKey") String sourcePalletKey);
|
@Param("sourcePalletKey") String sourcePalletKey);
|
||||||
|
|
||||||
|
@Update("UPDATE ll_dispatch_pallet_lock SET active_key=NULL,updated_at=CURRENT_TIMESTAMP(3) " +
|
||||||
|
"WHERE customer_id=#{customerId} AND active_key=#{activeKey} AND status!='ACTIVE'")
|
||||||
|
int clearInactiveActiveKey(@Param("customerId") String customerId,
|
||||||
|
@Param("activeKey") String activeKey);
|
||||||
}
|
}
|
||||||
|
|||||||
+25
-3
@@ -1,12 +1,21 @@
|
|||||||
package com.baiyue.lastleg.dispatch.model;
|
package com.baiyue.lastleg.dispatch.model;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
public record DispatchPalletCandidate(
|
public record DispatchPalletCandidate(
|
||||||
Long planId,
|
Long planId,
|
||||||
Long planPalletId,
|
Long planPalletId,
|
||||||
Long warehousePalletId,
|
Long warehousePalletId,
|
||||||
Long orderId,
|
Long orderId,
|
||||||
|
String customerId,
|
||||||
|
String customerName,
|
||||||
|
String planNo,
|
||||||
|
String orderType,
|
||||||
|
BigDecimal volumeWeightKg,
|
||||||
|
String containerNo,
|
||||||
|
LocalDateTime appointmentAt,
|
||||||
|
LocalDateTime expectedLoadingAt,
|
||||||
String overseasWarehouseId,
|
String overseasWarehouseId,
|
||||||
String overseasWarehouseName,
|
String overseasWarehouseName,
|
||||||
String orderNo,
|
String orderNo,
|
||||||
@@ -19,16 +28,29 @@ public record DispatchPalletCandidate(
|
|||||||
String fbaWarehouseCode,
|
String fbaWarehouseCode,
|
||||||
Integer packageCount,
|
Integer packageCount,
|
||||||
BigDecimal volumeCbm,
|
BigDecimal volumeCbm,
|
||||||
String sourceStage
|
String sourceStage,
|
||||||
|
String cartonMarks
|
||||||
) {
|
) {
|
||||||
|
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||||
|
String overseasWarehouseId, String overseasWarehouseName, String orderNo,
|
||||||
|
String orderRefs, String inboundCode, String palletNo, String destinationWarehouseId,
|
||||||
|
String destinationWarehouseCode, String destinationWarehouseName,
|
||||||
|
String fbaWarehouseCode, Integer packageCount, BigDecimal volumeCbm,
|
||||||
|
String sourceStage) {
|
||||||
|
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, overseasWarehouseId,
|
||||||
|
overseasWarehouseName, orderNo, orderRefs, inboundCode, palletNo, destinationWarehouseId,
|
||||||
|
destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode, packageCount, volumeCbm,
|
||||||
|
sourceStage, null);
|
||||||
|
}
|
||||||
|
|
||||||
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
public DispatchPalletCandidate(Long planId, Long planPalletId, Long warehousePalletId, Long orderId,
|
||||||
String orderNo, String orderRefs, String inboundCode, String palletNo,
|
String orderNo, String orderRefs, String inboundCode, String palletNo,
|
||||||
String destinationWarehouseId, String destinationWarehouseCode,
|
String destinationWarehouseId, String destinationWarehouseCode,
|
||||||
String destinationWarehouseName, String fbaWarehouseCode, Integer packageCount,
|
String destinationWarehouseName, String fbaWarehouseCode, Integer packageCount,
|
||||||
BigDecimal volumeCbm, String sourceStage) {
|
BigDecimal volumeCbm, String sourceStage) {
|
||||||
this(planId, planPalletId, warehousePalletId, orderId, null, null, orderNo, orderRefs, inboundCode,
|
this(planId, planPalletId, warehousePalletId, orderId, null, null, null, null, null, null, null, null, null, null, orderNo, orderRefs, inboundCode,
|
||||||
palletNo, destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
palletNo, destinationWarehouseId, destinationWarehouseCode, destinationWarehouseName,
|
||||||
fbaWarehouseCode, packageCount, volumeCbm, sourceStage);
|
fbaWarehouseCode, packageCount, volumeCbm, sourceStage, null);
|
||||||
}
|
}
|
||||||
public String sourcePalletKey() {
|
public String sourcePalletKey() {
|
||||||
// A plan pallet keeps the same lock identity before and after the physical pallet is received.
|
// A plan pallet keeps the same lock identity before and after the physical pallet is received.
|
||||||
|
|||||||
+194
-41
@@ -9,10 +9,13 @@ import com.baiyue.lastleg.dispatch.dto.DispatchResponse;
|
|||||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||||
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||||
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
||||||
|
import com.baiyue.lastleg.dispatch.entity.DispatchDraft;
|
||||||
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||||
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
|
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
|
||||||
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
|
||||||
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
||||||
|
import com.baiyue.lastleg.dispatch.mapper.DispatchDraftMapper;
|
||||||
|
import com.baiyue.lastleg.dispatch.dto.DispatchDraftResponse;
|
||||||
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
@@ -39,17 +42,9 @@ public class DispatchService {
|
|||||||
@Resource private DispatchOrderMapper orders;
|
@Resource private DispatchOrderMapper orders;
|
||||||
@Resource private DispatchItemMapper items;
|
@Resource private DispatchItemMapper items;
|
||||||
@Resource private DispatchPalletLockMapper locks;
|
@Resource private DispatchPalletLockMapper locks;
|
||||||
|
@Resource private DispatchDraftMapper drafts;
|
||||||
@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 String resolveCustomerScopeForPlans(List<Long> planIds) {
|
public String resolveCustomerScopeForPlans(List<Long> planIds) {
|
||||||
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")),
|
||||||
"Selected inbound plans");
|
"Selected inbound plans");
|
||||||
@@ -62,14 +57,100 @@ public class DispatchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<DispatchPalletCandidate> candidates(String customerId, List<Long> planIds, String orderNo,
|
public List<DispatchPalletCandidate> candidates(String customerId, List<Long> planIds, String orderNo,
|
||||||
String fbaWarehouseCode, String sourceStage) {
|
String fbaWarehouseCode, String sourceStage, String draftToken) {
|
||||||
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
validateSourceStage(sourceStage);
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
|
||||||
}
|
|
||||||
if (!StringUtils.hasText(customerId)) return List.of();
|
if (!StringUtils.hasText(customerId)) return List.of();
|
||||||
List<Long> scopedPlanIds = normalizeIds(planIds, "Plan IDs");
|
List<Long> scopedPlanIds = normalizeIds(planIds, "Plan IDs");
|
||||||
return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||||
trimToNull(sourceStage));
|
trimToNull(sourceStage), trimToNull(draftToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode,
|
||||||
|
String sourceStage, String draftToken) {
|
||||||
|
validateSourceStage(sourceStage);
|
||||||
|
return candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
|
||||||
|
trimToNull(sourceStage), trimToNull(draftToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<DispatchDraftResponse> activeDrafts(String operatorId) {
|
||||||
|
return drafts.findActiveByOperator(trimToNull(operatorId)).stream()
|
||||||
|
.map(item -> new DispatchDraftResponse(item.getDraftToken(), item.getPlanIdsJson(), item.getContainersJson())).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void saveDraftSnapshot(String operatorId, DispatchRequests.DraftSnapshot request) {
|
||||||
|
String customerId = resolveCustomerScopeForPlans(request.planIds());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
DispatchDraft draft = drafts.lockActive(request.draftToken(), trimToNull(operatorId));
|
||||||
|
if (draft == null) {
|
||||||
|
draft = new DispatchDraft(); draft.setId(IdWorker.getId()); draft.setDraftToken(request.draftToken());
|
||||||
|
draft.setOperatorId(trimToNull(operatorId)); draft.setCustomerId(customerId); draft.setStatus("ACTIVE");
|
||||||
|
draft.setCreatedAt(now); draft.setExpiresAt(now.plusMinutes(30));
|
||||||
|
}
|
||||||
|
draft.setPlanIdsJson(request.planIds().stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||||
|
draft.setContainersJson(request.containersJson()); draft.setUpdatedAt(now); draft.setExpiresAt(now.plusMinutes(30));
|
||||||
|
drafts.insertOrUpdate(draft);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void lockDraftPallet(String customerId, String operatorId, DispatchRequests.DraftPallet request) {
|
||||||
|
DispatchPalletCandidate candidate = requireDraftCandidate(customerId, request.planPalletId());
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||||
|
if (active != null && active.getExpiresAt() != null && !active.getExpiresAt().isAfter(now)) {
|
||||||
|
releaseLock(active, now);
|
||||||
|
active = null;
|
||||||
|
}
|
||||||
|
if (active != null) {
|
||||||
|
if (request.draftToken().equals(active.getDraftToken())
|
||||||
|
&& Objects.equals(trimToNull(operatorId), active.getOperatorId())) return;
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Pallet is already locked by another dispatch draft: " + candidate.palletNo());
|
||||||
|
}
|
||||||
|
// Older releases did not persist active_key=NULL; clear that inactive unique-key residue before relocking.
|
||||||
|
locks.clearInactiveActiveKey(customerId, candidate.sourcePalletKey());
|
||||||
|
DispatchPalletLock lock = new DispatchPalletLock();
|
||||||
|
lock.setId(IdWorker.getId());
|
||||||
|
lock.setCustomerId(customerId);
|
||||||
|
lock.setSourcePalletKey(candidate.sourcePalletKey());
|
||||||
|
lock.setActiveKey(candidate.sourcePalletKey());
|
||||||
|
lock.setPlanId(candidate.planId());
|
||||||
|
lock.setPlanPalletId(candidate.planPalletId());
|
||||||
|
lock.setWarehousePalletId(candidate.warehousePalletId());
|
||||||
|
lock.setDraftToken(request.draftToken());
|
||||||
|
lock.setOperatorId(trimToNull(operatorId));
|
||||||
|
lock.setStatus("ACTIVE");
|
||||||
|
lock.setLockedAt(now);
|
||||||
|
lock.setExpiresAt(now.plusMinutes(30));
|
||||||
|
lock.setCreatedAt(now);
|
||||||
|
lock.setUpdatedAt(now);
|
||||||
|
try {
|
||||||
|
locks.insert(lock);
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Pallet is already locked by another dispatch draft: " + candidate.palletNo(), exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void unlockDraftPallet(String customerId, String operatorId, DispatchRequests.DraftPallet request) {
|
||||||
|
DispatchPalletCandidate candidate = requireDraftCandidate(customerId, request.planPalletId());
|
||||||
|
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||||
|
if (active == null) return;
|
||||||
|
requireDraftOwner(active, operatorId, request.draftToken());
|
||||||
|
releaseLock(active, LocalDateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void releaseDraft(String operatorId, String draftToken) {
|
||||||
|
List<DispatchPalletLock> activeLocks = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>()
|
||||||
|
.eq(DispatchPalletLock::getDraftToken, draftToken)
|
||||||
|
.eq(DispatchPalletLock::getOperatorId, trimToNull(operatorId))
|
||||||
|
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
activeLocks.forEach(lock -> releaseLock(lock, now));
|
||||||
|
DispatchDraft draft = drafts.lockActive(draftToken, trimToNull(operatorId));
|
||||||
|
if (draft != null) { draft.setStatus("RELEASED"); draft.setReleasedAt(now); draft.setUpdatedAt(now); drafts.updateById(draft); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -86,20 +167,21 @@ public class DispatchService {
|
|||||||
if (selected.size() != palletIds.size()) {
|
if (selected.size() != palletIds.size()) {
|
||||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "One or more selected pallets are not in this tenant");
|
||||||
}
|
}
|
||||||
if (selected.stream().anyMatch(candidate -> candidate.packageCount() == null || candidate.packageCount() <= 0)) {
|
|
||||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "Selected pallet has no remaining packages to dispatch");
|
|
||||||
}
|
|
||||||
Set<String> warehouseIds = selected.stream().map(DispatchPalletCandidate::overseasWarehouseId)
|
Set<String> warehouseIds = selected.stream().map(DispatchPalletCandidate::overseasWarehouseId)
|
||||||
.filter(Objects::nonNull).map(String::trim).filter(value -> !value.isEmpty()).collect(Collectors.toSet());
|
.filter(Objects::nonNull).map(String::trim).filter(value -> !value.isEmpty()).collect(Collectors.toSet());
|
||||||
if (warehouseIds.size() != 1 || selected.stream().anyMatch(candidate -> candidate.overseasWarehouseId() == null)) {
|
if (warehouseIds.size() != 1 || selected.stream().anyMatch(candidate -> candidate.overseasWarehouseId() == null)) {
|
||||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "A dispatch must belong to exactly one overseas warehouse");
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "A dispatch must belong to exactly one overseas warehouse");
|
||||||
}
|
}
|
||||||
|
String draftToken = trimToNull(request.draftToken());
|
||||||
for (DispatchPalletCandidate candidate : selected) {
|
for (DispatchPalletCandidate candidate : selected) {
|
||||||
boolean planLocked = candidate.planPalletId() != null
|
DispatchPalletLock active = activeLock(customerId, candidate);
|
||||||
&& locks.lockActive(customerId, "PLAN:" + candidate.planPalletId()) != null;
|
if (draftToken != null) {
|
||||||
boolean receivedLocked = candidate.warehousePalletId() != null
|
if (active == null) {
|
||||||
&& locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId()) != null;
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
if (planLocked || receivedLocked) {
|
"Pallet draft lock has expired: " + candidate.palletNo());
|
||||||
|
}
|
||||||
|
requireDraftOwner(active, operatorId, draftToken);
|
||||||
|
} else if (active != null) {
|
||||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
"Pallet is already locked by another active dispatch: " + candidate.palletNo());
|
"Pallet is already locked by another active dispatch: " + candidate.palletNo());
|
||||||
}
|
}
|
||||||
@@ -129,24 +211,35 @@ public class DispatchService {
|
|||||||
orders.insert(dispatch);
|
orders.insert(dispatch);
|
||||||
|
|
||||||
for (DispatchPalletCandidate candidate : selected) {
|
for (DispatchPalletCandidate candidate : selected) {
|
||||||
DispatchPalletLock lock = new DispatchPalletLock();
|
DispatchPalletLock lock = activeLock(customerId, candidate);
|
||||||
lock.setId(IdWorker.getId());
|
if (draftToken != null) {
|
||||||
lock.setCustomerId(customerId);
|
requireDraftOwner(lock, operatorId, draftToken);
|
||||||
lock.setSourcePalletKey(candidate.sourcePalletKey());
|
lock.setDispatchOrderId(dispatch.getId());
|
||||||
lock.setActiveKey(candidate.sourcePalletKey());
|
lock.setDraftToken(null);
|
||||||
lock.setPlanId(candidate.planId());
|
lock.setExpiresAt(null);
|
||||||
lock.setPlanPalletId(candidate.planPalletId());
|
lock.setUpdatedAt(now);
|
||||||
lock.setWarehousePalletId(candidate.warehousePalletId());
|
locks.updateById(lock);
|
||||||
lock.setDispatchOrderId(dispatch.getId());
|
} else {
|
||||||
lock.setStatus("ACTIVE");
|
lock = new DispatchPalletLock();
|
||||||
lock.setLockedAt(now);
|
lock.setId(IdWorker.getId());
|
||||||
lock.setCreatedAt(now);
|
lock.setCustomerId(customerId);
|
||||||
lock.setUpdatedAt(now);
|
lock.setSourcePalletKey(candidate.sourcePalletKey());
|
||||||
try {
|
lock.setActiveKey(candidate.sourcePalletKey());
|
||||||
locks.insert(lock);
|
lock.setPlanId(candidate.planId());
|
||||||
} catch (DuplicateKeyException exception) {
|
lock.setPlanPalletId(candidate.planPalletId());
|
||||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
lock.setWarehousePalletId(candidate.warehousePalletId());
|
||||||
"Pallet is already locked by another active dispatch: " + candidate.palletNo(), exception);
|
lock.setDispatchOrderId(dispatch.getId());
|
||||||
|
lock.setOperatorId(trimToNull(operatorId));
|
||||||
|
lock.setStatus("ACTIVE");
|
||||||
|
lock.setLockedAt(now);
|
||||||
|
lock.setCreatedAt(now);
|
||||||
|
lock.setUpdatedAt(now);
|
||||||
|
try {
|
||||||
|
locks.insert(lock);
|
||||||
|
} catch (DuplicateKeyException exception) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Pallet is already locked by another active dispatch: " + candidate.palletNo(), exception);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
DispatchItem item = new DispatchItem();
|
DispatchItem item = new DispatchItem();
|
||||||
item.setId(IdWorker.getId());
|
item.setId(IdWorker.getId());
|
||||||
@@ -179,8 +272,18 @@ public class DispatchService {
|
|||||||
if (!StringUtils.hasText(customerId)) {
|
if (!StringUtils.hasText(customerId)) {
|
||||||
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
return new PageResult<>(List.of(), query.page(), query.size(), 0, 0);
|
||||||
}
|
}
|
||||||
|
return page(customerId, query, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public PageResult<DispatchResponse> platformPage(DispatchRequests.Query query) {
|
||||||
|
return page(null, query, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PageResult<DispatchResponse> page(String customerId, DispatchRequests.Query query,
|
||||||
|
boolean acrossCustomers) {
|
||||||
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(DispatchOrder::getCustomerId, customerId)
|
new LambdaQueryWrapper<DispatchOrder>()
|
||||||
|
.eq(!acrossCustomers, 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())
|
||||||
@@ -197,6 +300,19 @@ public class DispatchService {
|
|||||||
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DispatchResponse platformDetail(String dispatchNo) {
|
||||||
|
List<DispatchOrder> matches = orders.findAcrossCustomersByDispatchNo(dispatchNo);
|
||||||
|
if (matches == null || matches.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found");
|
||||||
|
}
|
||||||
|
if (matches.size() > 1) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||||
|
"Dispatch number is not unique across customers");
|
||||||
|
}
|
||||||
|
DispatchOrder dispatch = matches.get(0);
|
||||||
|
return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()));
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DispatchResponse update(String customerId, String dispatchNo, DispatchRequests.Update request) {
|
public DispatchResponse update(String customerId, String dispatchNo, DispatchRequests.Update request) {
|
||||||
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
DispatchOrder dispatch = orders.lockById(customerId, find(customerId, dispatchNo).getId());
|
||||||
@@ -279,6 +395,37 @@ public class DispatchService {
|
|||||||
.distinct().collect(Collectors.joining(" -> "));
|
.distinct().collect(Collectors.joining(" -> "));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DispatchPalletCandidate requireDraftCandidate(String customerId, Long planPalletId) {
|
||||||
|
List<DispatchPalletCandidate> found = candidates.lockSelected(customerId, List.of(planPalletId));
|
||||||
|
if (found.size() != 1) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Pallet is not available for dispatch");
|
||||||
|
}
|
||||||
|
return found.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) {
|
||||||
|
DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey());
|
||||||
|
if (active == null && candidate.warehousePalletId() != null) {
|
||||||
|
active = locks.lockActive(customerId, "RECEIVED:" + candidate.warehousePalletId());
|
||||||
|
}
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireDraftOwner(DispatchPalletLock lock, String operatorId, String draftToken) {
|
||||||
|
if (lock == null || !draftToken.equals(lock.getDraftToken())
|
||||||
|
|| !Objects.equals(trimToNull(operatorId), lock.getOperatorId())) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet is locked by another dispatch draft");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseLock(DispatchPalletLock lock, LocalDateTime now) {
|
||||||
|
lock.setStatus("RELEASED");
|
||||||
|
lock.setActiveKey(null);
|
||||||
|
lock.setReleasedAt(now);
|
||||||
|
lock.setUpdatedAt(now);
|
||||||
|
locks.updateById(lock);
|
||||||
|
}
|
||||||
|
|
||||||
private static List<String> splitOrderRefs(String refs, String fallback) {
|
private static List<String> splitOrderRefs(String refs, String fallback) {
|
||||||
String value = StringUtils.hasText(refs) ? refs : fallback;
|
String value = StringUtils.hasText(refs) ? refs : fallback;
|
||||||
return StringUtils.hasText(value) ? List.of(value.split(",")) : List.of();
|
return StringUtils.hasText(value) ? List.of(value.split(",")) : List.of();
|
||||||
@@ -292,6 +439,12 @@ public class DispatchService {
|
|||||||
return StringUtils.hasText(value) ? value.trim() : null;
|
return StringUtils.hasText(value) ? value.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void validateSourceStage(String sourceStage) {
|
||||||
|
if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED").contains(sourceStage)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN or RECEIVED");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<Long> normalizeIds(List<Long> ids, String label) {
|
private static List<Long> normalizeIds(List<Long> ids, String label) {
|
||||||
if (ids == null || ids.isEmpty() || ids.stream().anyMatch(Objects::isNull)) {
|
if (ids == null || ids.isEmpty() || ids.stream().anyMatch(Objects::isNull)) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " must be non-empty");
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " must be non-empty");
|
||||||
|
|||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
package com.baiyue.lastleg.relabel.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||||
|
import com.baiyue.lastleg.relabel.dto.RelabelResponses;
|
||||||
|
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/merchant/v1/last-leg/relabel-applications")
|
||||||
|
public class MerchantRelabelApplicationController {
|
||||||
|
@Resource private RelabelApplicationService service;
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<RelabelResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody RelabelRequests.Create request) { return ApiResponse.success(service.create(customer(user), operator(user), request)); }
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<RelabelResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid RelabelRequests.Query query) { return ApiResponse.success(service.page(customer(user), query)); }
|
||||||
|
@GetMapping("/{relabelNo}")
|
||||||
|
public ApiResponse<RelabelResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String relabelNo) { return ApiResponse.success(service.detail(customer(user), relabelNo)); }
|
||||||
|
private String customer(AuthenticatedUser user) {
|
||||||
|
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||||
|
return String.valueOf(user.tenantId());
|
||||||
|
}
|
||||||
|
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||||
|
}
|
||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
package com.baiyue.lastleg.transfer.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||||
|
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||||
|
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/merchant/v1/last-leg/warehouse-transfers")
|
||||||
|
public class MerchantWarehouseTransferController {
|
||||||
|
@Resource private WarehouseTransferService service;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ApiResponse<WarehouseTransferResponses.Detail> create(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid @RequestBody WarehouseTransferRequests.Create request) {
|
||||||
|
return ApiResponse.success(service.create(customer(user), operator(user), request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@Valid WarehouseTransferRequests.Query query) {
|
||||||
|
return ApiResponse.success(service.page(customer(user), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{transferNo}")
|
||||||
|
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@PathVariable String transferNo) {
|
||||||
|
return ApiResponse.success(service.detail(customer(user), transferNo));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String customer(AuthenticatedUser user) {
|
||||||
|
if (user == null || user.tenantId() == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
if ("PLATFORM".equals(user.appCode()) || user.tenantId() == 0L) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Merchant identity is required");
|
||||||
|
}
|
||||||
|
return String.valueOf(user.tenantId());
|
||||||
|
}
|
||||||
|
private String operator(AuthenticatedUser user) { return user.userId() == null ? user.username() : String.valueOf(user.userId()); }
|
||||||
|
}
|
||||||
+7
@@ -1,6 +1,7 @@
|
|||||||
package com.baiyue.lastleg.transfer.controller;
|
package com.baiyue.lastleg.transfer.controller;
|
||||||
|
|
||||||
import com.baiyue.common.core.api.ApiResponse;
|
import com.baiyue.common.core.api.ApiResponse;
|
||||||
|
import com.baiyue.common.core.api.PageResult;
|
||||||
import com.baiyue.common.security.AuthenticatedUser;
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||||
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
import com.baiyue.lastleg.transfer.dto.WarehouseTransferResponses;
|
||||||
@@ -24,6 +25,12 @@ public class WarehouseTransferController {
|
|||||||
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
return ApiResponse.success(service.create(scope(user, customerId), operator(user), request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ApiResponse<PageResult<WarehouseTransferResponses.Summary>> page(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
|
@RequestParam(required = false) String customerId, @Valid WarehouseTransferRequests.Query query) {
|
||||||
|
return ApiResponse.success(service.page(scope(user, customerId), query));
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{transferNo}")
|
@GetMapping("/{transferNo}")
|
||||||
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<WarehouseTransferResponses.Detail> detail(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
|
@RequestParam(required = false) String customerId, @PathVariable String transferNo) {
|
||||||
|
|||||||
+9
@@ -1,6 +1,8 @@
|
|||||||
package com.baiyue.lastleg.transfer.dto;
|
package com.baiyue.lastleg.transfer.dto;
|
||||||
|
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
import jakarta.validation.constraints.NotBlank;
|
||||||
import jakarta.validation.constraints.NotEmpty;
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
@@ -73,4 +75,11 @@ public final class WarehouseTransferRequests {
|
|||||||
@Positive int packageCount,
|
@Positive int packageCount,
|
||||||
@Size(max = 255) String shippingMark
|
@Size(max = 255) String shippingMark
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public record Query(@Min(1) long page, @Min(1) @Max(200) long size,
|
||||||
|
String transferNo, String inboundCode, String operatingWarehouseId,
|
||||||
|
String fbaWarehouseCode, String status, Boolean needRelabel,
|
||||||
|
LocalDateTime createdFrom, LocalDateTime createdTo) {
|
||||||
|
public Query { if (page == 0) page = 1; if (size == 0) size = 20; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
@@ -10,6 +10,10 @@ public final class WarehouseTransferResponses {
|
|||||||
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
|
public record Allocation(Long inventoryPositionId, Long inboundOrderId, String inboundCode,
|
||||||
Long palletId, String palletNo, int packageCount, String status,
|
Long palletId, String palletNo, int packageCount, String status,
|
||||||
Long destinationInventoryPositionId) {}
|
Long destinationInventoryPositionId) {}
|
||||||
|
public record Summary(Long id, String transferNo, String inboundCode, Long lastlegOrderId,
|
||||||
|
String lastlegOrderNo, String operatingWarehouseId, String fbaWarehouseCode,
|
||||||
|
int packageCount, String deliveryMethod, boolean needRelabel, String relabelNo,
|
||||||
|
String status, LocalDateTime estimatedArrivalAt, LocalDateTime createdAt) {}
|
||||||
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
public record Detail(Long id, String transferNo, Long inboundOrderId, String inboundCode,
|
||||||
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
Long lastlegOrderId, String lastlegOrderNo, String orderType, String orderStatus,
|
||||||
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
|
boolean needRelabel, Long relabelApplicationId, String relabelNo, String relabelStatus,
|
||||||
|
|||||||
+28
@@ -1,6 +1,8 @@
|
|||||||
package com.baiyue.lastleg.transfer.service;
|
package com.baiyue.lastleg.transfer.service;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
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.baomidou.mybatisplus.core.toolkit.IdWorker;
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrder;
|
import com.baiyue.lastleg.entity.LastlegOrder;
|
||||||
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
import com.baiyue.lastleg.entity.LastlegOrderDetail;
|
||||||
@@ -158,6 +160,32 @@ public class WarehouseTransferService {
|
|||||||
return detail(customerId, transferNo);
|
return detail(customerId, transferNo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public PageResult<WarehouseTransferResponses.Summary> page(String customerId, WarehouseTransferRequests.Query query) {
|
||||||
|
if (query.createdFrom() != null && query.createdTo() != null && query.createdFrom().isAfter(query.createdTo())) {
|
||||||
|
throw badRequest("createdFrom must not be after createdTo");
|
||||||
|
}
|
||||||
|
LambdaQueryWrapper<WarehouseTransferOrder> wrapper = new LambdaQueryWrapper<WarehouseTransferOrder>()
|
||||||
|
.eq(StringUtils.hasText(customerId), WarehouseTransferOrder::getCustomerId, customerId)
|
||||||
|
.like(StringUtils.hasText(query.transferNo()), WarehouseTransferOrder::getTransferNo, query.transferNo())
|
||||||
|
.like(StringUtils.hasText(query.inboundCode()), WarehouseTransferOrder::getInboundCode, query.inboundCode())
|
||||||
|
.eq(StringUtils.hasText(query.operatingWarehouseId()), WarehouseTransferOrder::getOperatingWarehouseId, query.operatingWarehouseId())
|
||||||
|
.like(StringUtils.hasText(query.fbaWarehouseCode()), WarehouseTransferOrder::getFbaWarehouseCode, query.fbaWarehouseCode())
|
||||||
|
.eq(StringUtils.hasText(query.status()), WarehouseTransferOrder::getStatus, query.status())
|
||||||
|
.eq(query.needRelabel() != null, WarehouseTransferOrder::getNeedRelabel, query.needRelabel())
|
||||||
|
.ge(query.createdFrom() != null, WarehouseTransferOrder::getCreatedAt, query.createdFrom())
|
||||||
|
.le(query.createdTo() != null, WarehouseTransferOrder::getCreatedAt, query.createdTo())
|
||||||
|
.orderByDesc(WarehouseTransferOrder::getCreatedAt).orderByDesc(WarehouseTransferOrder::getId);
|
||||||
|
Page<WarehouseTransferOrder> result = transfers.selectPage(Page.of(query.page(), query.size()), wrapper);
|
||||||
|
List<WarehouseTransferResponses.Summary> records = result.getRecords().stream().map(value ->
|
||||||
|
new WarehouseTransferResponses.Summary(value.getId(), value.getTransferNo(), value.getInboundCode(),
|
||||||
|
value.getLastlegOrderId(), value.getLastlegOrderNo(), value.getOperatingWarehouseId(),
|
||||||
|
value.getFbaWarehouseCode(), value.getPackageCount(), value.getDeliveryMethod(),
|
||||||
|
Boolean.TRUE.equals(value.getNeedRelabel()), value.getRelabelNo(), value.getStatus(),
|
||||||
|
value.getEstimatedArrivalAt(), value.getCreatedAt())).toList();
|
||||||
|
return new PageResult<>(records, result.getCurrent(), result.getSize(), result.getTotal(), result.getPages());
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public WarehouseTransferResponses.Detail detail(String customerId, String transferNo) {
|
public WarehouseTransferResponses.Detail detail(String customerId, String transferNo) {
|
||||||
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
|
WarehouseTransferOrder transfer = transfers.findNo(customerId, transferNo);
|
||||||
|
|||||||
@@ -828,9 +828,12 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
|
|||||||
plan_id BIGINT NULL COMMENT '入仓计划ID',
|
plan_id BIGINT NULL COMMENT '入仓计划ID',
|
||||||
plan_pallet_id BIGINT NULL COMMENT '计划托盘ID',
|
plan_pallet_id BIGINT NULL COMMENT '计划托盘ID',
|
||||||
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID',
|
warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID',
|
||||||
dispatch_order_id BIGINT NOT NULL COMMENT '派车单ID',
|
dispatch_order_id BIGINT NULL COMMENT '正式派车单ID,草稿锁阶段为空',
|
||||||
|
draft_token VARCHAR(64) NULL COMMENT '智能排车草稿随机标识',
|
||||||
|
operator_id VARCHAR(128) NULL COMMENT '草稿锁操作人ID',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态',
|
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态',
|
||||||
locked_at DATETIME(3) NOT NULL COMMENT '锁定时间',
|
locked_at DATETIME(3) NOT NULL COMMENT '锁定时间',
|
||||||
|
expires_at DATETIME(3) NULL COMMENT '草稿锁失效时间,正式派车锁为空',
|
||||||
released_at DATETIME(3) NULL COMMENT '释放时间',
|
released_at DATETIME(3) NULL COMMENT '释放时间',
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||||
@@ -840,6 +843,15 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
|
|||||||
KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status)
|
KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status)
|
||||||
) ENGINE=InnoDB COMMENT='派车托盘排他锁';
|
) ENGINE=InnoDB COMMENT='派车托盘排他锁';
|
||||||
|
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='draft_token'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN draft_token VARCHAR(64) NULL COMMENT ''智能排车草稿随机标识'' AFTER dispatch_order_id'));
|
||||||
|
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='operator_id'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN operator_id VARCHAR(128) NULL COMMENT ''草稿锁操作人ID'' AFTER draft_token'));
|
||||||
|
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='expires_at'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN expires_at DATETIME(3) NULL COMMENT ''草稿锁失效时间,正式派车锁为空'' AFTER locked_at'));
|
||||||
|
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
||||||
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_pallet_lock' AND column_name='dispatch_order_id' AND is_nullable='YES'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock MODIFY COLUMN dispatch_order_id BIGINT NULL COMMENT ''正式派车单ID,草稿锁阶段为空'''));
|
||||||
|
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
||||||
|
|
||||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='order_refs'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN order_refs VARCHAR(2000) NULL AFTER order_no'));
|
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='order_refs'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN order_refs VARCHAR(2000) NULL AFTER order_no'));
|
||||||
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
PREPARE dispatch_schema_stmt FROM @ddl; EXECUTE dispatch_schema_stmt; DEALLOCATE PREPARE dispatch_schema_stmt;
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ paths:
|
|||||||
/api/platform/v1/last-leg/dispatches:
|
/api/platform/v1/last-leg/dispatches:
|
||||||
get:
|
get:
|
||||||
summary: 分页查询排车单
|
summary: 分页查询排车单
|
||||||
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端仅在当前部署恰好存在一个客户范围时查询,前端不传 customerId。
|
description: 权限 platform:overseas-warehouse:inbound-order:list;平台跨客户分页查询,前端不传 customerId。
|
||||||
security: [{ bearerAuth: [] }]
|
security: [{ bearerAuth: [] }]
|
||||||
parameters:
|
parameters:
|
||||||
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
- { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } }
|
||||||
@@ -38,28 +38,27 @@ paths:
|
|||||||
- { name: fbaAppointmentNo, in: query, schema: { type: string } }
|
- { name: fbaAppointmentNo, in: query, schema: { type: string } }
|
||||||
responses:
|
responses:
|
||||||
'200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse }
|
'200': { description: 统一 ApiResponse 包装的排车单分页,records 元素为 DispatchResponse }
|
||||||
'409': { description: 当前部署检测到多个客户范围,拒绝跨客户查询 }
|
|
||||||
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
|
/api/platform/v1/last-leg/dispatches/{dispatchNo}:
|
||||||
get:
|
get:
|
||||||
summary: 查询排车单详情
|
summary: 查询排车单详情
|
||||||
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端解析唯一客户范围,前端不传 customerId。
|
description: 权限 platform:overseas-warehouse:inbound-order:list;平台按全局排车单号定位记录,并使用记录自身客户范围查询明细,前端不传 customerId。
|
||||||
security: [{ bearerAuth: [] }]
|
security: [{ bearerAuth: [] }]
|
||||||
parameters:
|
parameters:
|
||||||
- { name: dispatchNo, in: path, required: true, schema: { type: string } }
|
- { name: dispatchNo, in: path, required: true, schema: { type: string } }
|
||||||
responses:
|
responses:
|
||||||
'200': { description: 统一 ApiResponse 包装的 DispatchResponse }
|
'200': { description: 统一 ApiResponse 包装的 DispatchResponse }
|
||||||
'404': { description: 排车单不存在 }
|
'404': { description: 排车单不存在 }
|
||||||
'409': { description: 当前部署检测到多个客户范围 }
|
'409': { description: 排车单号在不同客户下重复,无法唯一定位 }
|
||||||
/api/platform/v1/last-leg/dispatches/candidates:
|
/api/platform/v1/last-leg/dispatches/candidates:
|
||||||
get:
|
get:
|
||||||
summary: 查询智能排车候选托盘
|
summary: 查询智能排车候选托盘
|
||||||
description: 权限 platform:overseas-warehouse:inbound-order:list;服务端解析唯一客户范围,仅返回尚未被有效排车锁定的计划或已实收托盘,前端不传 customerId。
|
description: 权限 platform:overseas-warehouse:inbound-order:list;planIds 为空时平台跨客户查询全部可排托盘,传入时按所选入库计划解析客户范围;仅返回尚未被有效排车锁定的计划或已实收托盘,前端不传 customerId。
|
||||||
security: [{ bearerAuth: [] }]
|
security: [{ bearerAuth: [] }]
|
||||||
parameters:
|
parameters:
|
||||||
- name: planIds
|
- name: planIds
|
||||||
in: query
|
in: query
|
||||||
required: true
|
required: false
|
||||||
description: 已选入库计划 ID,逗号分隔;服务端据此解析并校验唯一客户范围。
|
description: 可选的入库计划 ID,逗号分隔;省略时平台查询全部客户候选。
|
||||||
schema: { type: array, minItems: 1, maxItems: 500, items: { type: integer, format: int64 } }
|
schema: { type: array, minItems: 1, maxItems: 500, items: { type: integer, format: int64 } }
|
||||||
style: form
|
style: form
|
||||||
explode: false
|
explode: false
|
||||||
@@ -80,6 +79,25 @@ paths:
|
|||||||
content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchPreArrange' } } }
|
content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchPreArrange' } } }
|
||||||
responses:
|
responses:
|
||||||
'200': { description: 统一 ApiResponse 包装的预排车详情, content: { application/json: { schema: { $ref: '#/components/schemas/DispatchResponse' } } } }
|
'200': { description: 统一 ApiResponse 包装的预排车详情, content: { application/json: { schema: { $ref: '#/components/schemas/DispatchResponse' } } } }
|
||||||
|
/api/platform/v1/last-leg/dispatches/draft-locks/lock:
|
||||||
|
post:
|
||||||
|
summary: 将托盘移入智能排车草稿并立即锁定
|
||||||
|
description: 权限 platform:overseas-warehouse:inbound-order:smart-dispatch;草稿锁 30 分钟失效,与正式排车锁共享托盘排他约束。
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchDraftPallet' } } } }
|
||||||
|
responses: { '200': { description: 锁定成功 }, '409': { description: 托盘已被其他草稿或正式排车锁定 } }
|
||||||
|
/api/platform/v1/last-leg/dispatches/draft-locks/unlock:
|
||||||
|
post:
|
||||||
|
summary: 从智能排车草稿移除托盘并立即解锁
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchDraftPallet' } } } }
|
||||||
|
responses: { '200': { description: 解锁成功 }, '409': { description: 草稿锁不属于当前操作人 } }
|
||||||
|
/api/platform/v1/last-leg/dispatches/draft-locks/release:
|
||||||
|
post:
|
||||||
|
summary: 关闭智能排车草稿并释放草稿内全部托盘
|
||||||
|
security: [{ bearerAuth: [] }]
|
||||||
|
requestBody: { required: true, content: { application/json: { schema: { $ref: '#/components/schemas/SmartDispatchDraftRelease' } } } }
|
||||||
|
responses: { '200': { description: 草稿锁已全部释放 } }
|
||||||
/api/platform/v1/last-leg/overseas-procurements:
|
/api/platform/v1/last-leg/overseas-procurements:
|
||||||
get:
|
get:
|
||||||
summary: 分页查询海外采购订单
|
summary: 分页查询海外采购订单
|
||||||
@@ -296,6 +314,14 @@ components:
|
|||||||
planPalletId: { type: string }
|
planPalletId: { type: string }
|
||||||
warehousePalletId: { type: string, nullable: true }
|
warehousePalletId: { type: string, nullable: true }
|
||||||
orderId: { type: string }
|
orderId: { type: string }
|
||||||
|
customerId: { type: string }
|
||||||
|
customerName: { type: string, nullable: true }
|
||||||
|
planNo: { type: string, nullable: true }
|
||||||
|
orderType: { type: string, nullable: true }
|
||||||
|
volumeWeightKg: { type: number, nullable: true }
|
||||||
|
containerNo: { type: string, nullable: true }
|
||||||
|
appointmentAt: { type: string, format: date-time, nullable: true }
|
||||||
|
expectedLoadingAt: { type: string, format: date-time, nullable: true }
|
||||||
overseasWarehouseId: { type: string }
|
overseasWarehouseId: { type: string }
|
||||||
overseasWarehouseName: { type: string }
|
overseasWarehouseName: { type: string }
|
||||||
orderNo: { type: string }
|
orderNo: { type: string }
|
||||||
@@ -309,6 +335,7 @@ components:
|
|||||||
packageCount: { type: integer }
|
packageCount: { type: integer }
|
||||||
volumeCbm: { type: number }
|
volumeCbm: { type: number }
|
||||||
sourceStage: { type: string, enum: [PLAN, RECEIVED] }
|
sourceStage: { type: string, enum: [PLAN, RECEIVED] }
|
||||||
|
cartonMarks: { type: string, nullable: true, description: 该托盘箱号,逗号分隔 }
|
||||||
SmartDispatchPreArrange:
|
SmartDispatchPreArrange:
|
||||||
type: object
|
type: object
|
||||||
required: [planPalletIds]
|
required: [planPalletIds]
|
||||||
@@ -318,6 +345,18 @@ components:
|
|||||||
fbaAppointmentNo: { type: string, nullable: true, maxLength: 64 }
|
fbaAppointmentNo: { type: string, nullable: true, maxLength: 64 }
|
||||||
fbaAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
fbaAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
||||||
vehicleAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
vehicleAppointmentStatus: { type: string, nullable: true, maxLength: 32, default: PENDING }
|
||||||
|
draftToken: { type: string, nullable: true, maxLength: 64, description: 拖入托盘时使用的智能排车草稿标识 }
|
||||||
|
SmartDispatchDraftPallet:
|
||||||
|
type: object
|
||||||
|
required: [planPalletId, draftToken]
|
||||||
|
properties:
|
||||||
|
planPalletId: { type: string }
|
||||||
|
draftToken: { type: string, minLength: 1, maxLength: 64 }
|
||||||
|
SmartDispatchDraftRelease:
|
||||||
|
type: object
|
||||||
|
required: [draftToken]
|
||||||
|
properties:
|
||||||
|
draftToken: { type: string, minLength: 1, maxLength: 64 }
|
||||||
DispatchResponse:
|
DispatchResponse:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package com.baiyue.lastleg.discard.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.discard.dto.CargoDiscardRequests;
|
||||||
|
import com.baiyue.lastleg.discard.service.CargoDiscardService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class MerchantCargoDiscardControllerTest {
|
||||||
|
private final CargoDiscardService service = mock(CargoDiscardService.class);
|
||||||
|
private final MerchantCargoDiscardController controller = controller();
|
||||||
|
private final CargoDiscardRequests.Query query = new CargoDiscardRequests.Query(
|
||||||
|
1, 20, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantQueriesUseOnlyAuthenticatedTenant() {
|
||||||
|
AuthenticatedUser user = user(100L, "MERCHANT");
|
||||||
|
controller.page(user, query);
|
||||||
|
controller.candidates(user, "IN001");
|
||||||
|
controller.detail(user, "DS001");
|
||||||
|
verify(service).page("100", query);
|
||||||
|
verify(service).candidates("100", "IN001");
|
||||||
|
verify(service).detail("100", "DS001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantEndpointRejectsPlatformIdentity() {
|
||||||
|
assertThrows(ResponseStatusException.class,
|
||||||
|
() -> controller.page(user(0L, "PLATFORM"), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||||
|
return new AuthenticatedUser(1L, tenantId, tenantId, 9L, "merchant", appCode, "WEB", Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantCargoDiscardController controller() {
|
||||||
|
MerchantCargoDiscardController controller = new MerchantCargoDiscardController();
|
||||||
|
ReflectionTestUtils.setField(controller, "service", service);
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch;
|
||||||
|
|
||||||
|
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class DispatchCandidateMapperContractTest {
|
||||||
|
private static final List<String> CONSTRUCTOR_ALIASES = List.of(
|
||||||
|
"planId", "planPalletId", "warehousePalletId", "orderId", "customerId", "customerName",
|
||||||
|
"planNo", "orderType", "volumeWeightKg", "containerNo", "appointmentAt", "expectedLoadingAt",
|
||||||
|
"overseasWarehouseId", "overseasWarehouseName", "orderNo", "orderRefs", "inboundCode", "palletNo",
|
||||||
|
"destinationWarehouseId", "destinationWarehouseCode", "destinationWarehouseName", "fbaWarehouseCode",
|
||||||
|
"packageCount", "volumeCbm", "sourceStage", "cartonMarks"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void candidateQueriesKeepRecordConstructorColumnOrder() throws Exception {
|
||||||
|
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("available",
|
||||||
|
String.class, List.class, String.class, String.class, String.class));
|
||||||
|
assertConstructorColumnOrder(DispatchCandidateMapper.class.getMethod("lockSelected",
|
||||||
|
String.class, List.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void selectedReceivedPalletMustStillHaveAvailableInventory() throws Exception {
|
||||||
|
Method method = DispatchCandidateMapper.class.getMethod("lockSelected", String.class, List.class);
|
||||||
|
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||||
|
|
||||||
|
assertTrue(sql.contains("p.warehouse_pallet_id IS NULL OR EXISTS"));
|
||||||
|
assertTrue(sql.contains("pos.available_packages > 0"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertConstructorColumnOrder(Method method) {
|
||||||
|
String sql = String.join(" ", method.getAnnotation(Select.class).value());
|
||||||
|
int previous = -1;
|
||||||
|
for (String alias : CONSTRUCTOR_ALIASES) {
|
||||||
|
int current = sql.indexOf(" AS " + alias, previous + 1);
|
||||||
|
assertTrue(current > previous,
|
||||||
|
() -> method.getName() + " must select alias " + alias + " in record constructor order");
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
package com.baiyue.lastleg.dispatch;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
|
||||||
|
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
|
||||||
|
import org.apache.ibatis.annotations.Update;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class DispatchPalletLockPersistenceContractTest {
|
||||||
|
@Test
|
||||||
|
void nullableLockFieldsAreWrittenAsNullOnReleaseAndFinalization() throws Exception {
|
||||||
|
assertAlwaysUpdated("activeKey");
|
||||||
|
assertAlwaysUpdated("draftToken");
|
||||||
|
assertAlwaysUpdated("expiresAt");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void relockClearsInactiveUniqueKeyResidue() throws Exception {
|
||||||
|
Update update = DispatchPalletLockMapper.class
|
||||||
|
.getMethod("clearInactiveActiveKey", String.class, String.class)
|
||||||
|
.getAnnotation(Update.class);
|
||||||
|
String sql = String.join(" ", Arrays.asList(update.value()));
|
||||||
|
|
||||||
|
assertTrue(sql.contains("active_key=NULL"));
|
||||||
|
assertTrue(sql.contains("status!='ACTIVE'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertAlwaysUpdated(String fieldName) throws Exception {
|
||||||
|
TableField field = DispatchPalletLock.class.getDeclaredField(fieldName).getAnnotation(TableField.class);
|
||||||
|
assertEquals(FieldStrategy.ALWAYS, field.updateStrategy());
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -19,6 +19,9 @@ class DispatchSchemaContractTest {
|
|||||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_pick"));
|
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_pick"));
|
||||||
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_operation"));
|
assertTrue(sql.contains("CREATE TABLE IF NOT EXISTS ll_dispatch_pda_operation"));
|
||||||
assertTrue(sql.contains("active_key"));
|
assertTrue(sql.contains("active_key"));
|
||||||
|
assertTrue(sql.contains("draft_token"));
|
||||||
|
assertTrue(sql.contains("expires_at"));
|
||||||
|
assertTrue(sql.contains("dispatch_order_id BIGINT NULL"));
|
||||||
assertTrue(sql.contains("order_refs"));
|
assertTrue(sql.contains("order_refs"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -16,12 +16,17 @@ class SmartDispatchOpenApiContractTest {
|
|||||||
}
|
}
|
||||||
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/candidates:"));
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/candidates:"));
|
||||||
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/pre-arrange:"));
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/pre-arrange:"));
|
||||||
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/draft-locks/lock:"));
|
||||||
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/draft-locks/unlock:"));
|
||||||
|
assertTrue(yaml.contains("/api/platform/v1/last-leg/dispatches/draft-locks/release:"));
|
||||||
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:list"));
|
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:list"));
|
||||||
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:smart-dispatch"));
|
assertTrue(yaml.contains("platform:overseas-warehouse:inbound-order:smart-dispatch"));
|
||||||
assertTrue(yaml.contains("SmartDispatchPreArrange"));
|
assertTrue(yaml.contains("SmartDispatchPreArrange"));
|
||||||
assertTrue(yaml.contains("SmartDispatchCandidate"));
|
assertTrue(yaml.contains("SmartDispatchCandidate"));
|
||||||
assertTrue(yaml.contains("name: planIds"));
|
assertTrue(yaml.contains("name: planIds"));
|
||||||
assertTrue(yaml.contains("required: true"));
|
String planIdsParameter = yaml.substring(yaml.indexOf("name: planIds"),
|
||||||
|
yaml.indexOf("name: orderNo", yaml.indexOf("name: planIds")));
|
||||||
|
assertTrue(planIdsParameter.contains("required: false"));
|
||||||
String dispatchContract = yaml.substring(0,
|
String dispatchContract = yaml.substring(0,
|
||||||
yaml.indexOf("/api/platform/v1/last-leg/overseas-procurements:"));
|
yaml.indexOf("/api/platform/v1/last-leg/overseas-procurements:"));
|
||||||
assertFalse(dispatchContract.contains("name: customerId"));
|
assertFalse(dispatchContract.contains("name: customerId"));
|
||||||
|
|||||||
+3
-4
@@ -34,14 +34,13 @@ class DispatchQueryControllerTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void platformPageUsesServerResolvedSingleCustomerWithoutCustomerId() {
|
void platformPageQueriesAcrossCustomersWithoutCustomerId() {
|
||||||
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.resolveSingleCustomerScope()).thenReturn("100");
|
when(service.platformPage(query)).thenReturn(new PageResult<>(List.of(), 1, 20, 0, 0));
|
||||||
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("100", query);
|
verify(service).platformPage(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))
|
||||||
|
|||||||
+9
@@ -40,6 +40,15 @@ class SmartDispatchControllerTest {
|
|||||||
verify(service).candidates("100", List.of(1L, 2L), "LMO-1", "ONT8", "PLAN");
|
verify(service).candidates("100", List.of(1L, 2L), "LMO-1", "ONT8", "PLAN");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void candidatesWithoutSelectedPlansUsePlatformScope() {
|
||||||
|
when(service.platformCandidates(null, null, "RECEIVED")).thenReturn(List.of());
|
||||||
|
|
||||||
|
controller.candidates(platform(), null, null, null, "RECEIVED");
|
||||||
|
|
||||||
|
verify(service).platformCandidates(null, null, "RECEIVED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void preArrangeResolvesSingleCustomerAndAuthenticatedOperator() {
|
void preArrangeResolvesSingleCustomerAndAuthenticatedOperator() {
|
||||||
DispatchRequests.PreArrange request = new DispatchRequests.PreArrange(
|
DispatchRequests.PreArrange request = new DispatchRequests.PreArrange(
|
||||||
|
|||||||
+113
-12
@@ -1,5 +1,7 @@
|
|||||||
package com.baiyue.lastleg.dispatch.service;
|
package com.baiyue.lastleg.dispatch.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
import com.baiyue.lastleg.dispatch.dto.DispatchRequests;
|
||||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||||
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
|
||||||
@@ -69,6 +71,74 @@ class DispatchServiceTest {
|
|||||||
verify(items).insert(any(DispatchItem.class));
|
verify(items).insert(any(DispatchItem.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void movingPalletIntoDraftImmediatelyCreatesExpiringExclusiveLock() {
|
||||||
|
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||||
|
|
||||||
|
service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1"));
|
||||||
|
|
||||||
|
ArgumentCaptor<DispatchPalletLock> lock = ArgumentCaptor.forClass(DispatchPalletLock.class);
|
||||||
|
verify(locks).insert(lock.capture());
|
||||||
|
assertEquals("draft-1", lock.getValue().getDraftToken());
|
||||||
|
assertEquals("7", lock.getValue().getOperatorId());
|
||||||
|
assertNotNull(lock.getValue().getExpiresAt());
|
||||||
|
assertEquals(null, lock.getValue().getDispatchOrderId());
|
||||||
|
verify(locks).clearInactiveActiveKey("100", "PLAN:10");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void movingVolumeOnlyPlanPalletIntoDraftCreatesLock() {
|
||||||
|
when(candidates.lockSelected("100", List.of(10L)))
|
||||||
|
.thenReturn(List.of(candidate(10L, null, "ONT8", 0)));
|
||||||
|
|
||||||
|
service.lockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1"));
|
||||||
|
|
||||||
|
verify(locks).insert(any(DispatchPalletLock.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preArrangeAcceptsVolumeOnlyPlanPallet() {
|
||||||
|
when(candidates.lockSelected("100", List.of(10L)))
|
||||||
|
.thenReturn(List.of(candidate(10L, null, "ONT8", 0)));
|
||||||
|
|
||||||
|
service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null,
|
||||||
|
null, null, null));
|
||||||
|
|
||||||
|
ArgumentCaptor<DispatchOrder> dispatch = ArgumentCaptor.forClass(DispatchOrder.class);
|
||||||
|
verify(orders).insert(dispatch.capture());
|
||||||
|
assertEquals(0, dispatch.getValue().getPackageCount());
|
||||||
|
verify(items).insert(any(DispatchItem.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void removingPalletFromOwnedDraftImmediatelyReleasesLock() {
|
||||||
|
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||||
|
DispatchPalletLock lock = new DispatchPalletLock();
|
||||||
|
lock.setDraftToken("draft-1"); lock.setOperatorId("7"); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10");
|
||||||
|
when(locks.lockActive("100", "PLAN:10")).thenReturn(lock);
|
||||||
|
|
||||||
|
service.unlockDraftPallet("100", "7", new DispatchRequests.DraftPallet(10L, "draft-1"));
|
||||||
|
|
||||||
|
assertEquals("RELEASED", lock.getStatus());
|
||||||
|
assertEquals(null, lock.getActiveKey());
|
||||||
|
verify(locks).updateById(lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void preArrangeConvertsOwnedDraftLockInsteadOfInsertingAnotherLock() {
|
||||||
|
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||||
|
DispatchPalletLock lock = new DispatchPalletLock();
|
||||||
|
lock.setDraftToken("draft-1"); lock.setOperatorId("7"); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10");
|
||||||
|
when(locks.lockActive("100", "PLAN:10")).thenReturn(lock);
|
||||||
|
|
||||||
|
service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, null, null, null, "draft-1"));
|
||||||
|
|
||||||
|
assertNotNull(lock.getDispatchOrderId());
|
||||||
|
assertEquals(null, lock.getDraftToken());
|
||||||
|
verify(locks).updateById(lock);
|
||||||
|
verify(locks, never()).insert(any(DispatchPalletLock.class));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void rejectsPalletAlreadyLockedByAnotherDispatch() {
|
void rejectsPalletAlreadyLockedByAnotherDispatch() {
|
||||||
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8")));
|
||||||
@@ -87,6 +157,15 @@ class DispatchServiceTest {
|
|||||||
verify(candidates, never()).available(any(), any(), any(), any(), any());
|
verify(candidates, never()).available(any(), any(), any(), any(), any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformCandidatesQueryAcrossCustomersWithoutPlanScope() {
|
||||||
|
when(candidates.available(null, null, null, null, "RECEIVED")).thenReturn(List.of());
|
||||||
|
|
||||||
|
service.platformCandidates(null, null, "RECEIVED");
|
||||||
|
|
||||||
|
verify(candidates).available(null, null, null, null, "RECEIVED");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolvesCustomerFromSelectedPlansAndPalletsInMultiCustomerDeployment() {
|
void resolvesCustomerFromSelectedPlansAndPalletsInMultiCustomerDeployment() {
|
||||||
when(candidates.findCustomerScopesByPlanIds(List.of(1L, 2L))).thenReturn(List.of("100"));
|
when(candidates.findCustomerScopesByPlanIds(List.of(1L, 2L))).thenReturn(List.of("100"));
|
||||||
@@ -106,17 +185,6 @@ class DispatchServiceTest {
|
|||||||
assertEquals(409, exception.getStatusCode().value());
|
assertEquals(409, exception.getStatusCode().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
@Test
|
||||||
void emptyCustomerScopeProducesEmptyCandidatesAndRejectsPreArrange() {
|
void emptyCustomerScopeProducesEmptyCandidatesAndRejectsPreArrange() {
|
||||||
assertEquals(List.of(), service.candidates(null, null, null, null, null));
|
assertEquals(List.of(), service.candidates(null, null, null, null, null));
|
||||||
@@ -159,9 +227,42 @@ class DispatchServiceTest {
|
|||||||
verify(items).findByDispatchOrder("200", 1L);
|
verify(items).findByDispatchOrder("200", 1L);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() {
|
||||||
|
DispatchOrder first = new DispatchOrder();
|
||||||
|
first.setId(1L); first.setCustomerId("100"); first.setDispatchNo("PC1");
|
||||||
|
DispatchOrder second = new DispatchOrder();
|
||||||
|
second.setId(2L); second.setCustomerId("200"); second.setDispatchNo("PC2");
|
||||||
|
Page<DispatchOrder> resultPage = new Page<>(1, 20, 2);
|
||||||
|
resultPage.setRecords(List.of(first, second));
|
||||||
|
when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(resultPage);
|
||||||
|
|
||||||
|
var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null));
|
||||||
|
|
||||||
|
assertEquals(2, result.total());
|
||||||
|
verify(items).findByDispatchOrder("100", 1L);
|
||||||
|
verify(items).findByDispatchOrder("200", 2L);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void platformDetailUsesCustomerFromMatchedDispatch() {
|
||||||
|
DispatchOrder dispatch = new DispatchOrder();
|
||||||
|
dispatch.setId(1L); dispatch.setCustomerId("200"); dispatch.setDispatchNo("PC1");
|
||||||
|
when(orders.findAcrossCustomersByDispatchNo("PC1")).thenReturn(List.of(dispatch));
|
||||||
|
|
||||||
|
service.platformDetail("PC1");
|
||||||
|
|
||||||
|
verify(items).findByDispatchOrder("200", 1L);
|
||||||
|
}
|
||||||
|
|
||||||
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) {
|
||||||
|
return candidate(planPalletId, warehousePalletId, destination, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination,
|
||||||
|
int packageCount) {
|
||||||
return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1",
|
return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1",
|
||||||
"LIP1-P001", destination, destination, destination, destination, 12,
|
"LIP1-P001", destination, destination, destination, destination, packageCount,
|
||||||
new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED");
|
new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
package com.baiyue.lastleg.relabel.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.relabel.dto.RelabelRequests;
|
||||||
|
import com.baiyue.lastleg.relabel.service.RelabelApplicationService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class MerchantRelabelApplicationControllerTest {
|
||||||
|
private final RelabelApplicationService service = mock(RelabelApplicationService.class);
|
||||||
|
private final MerchantRelabelApplicationController controller = controller();
|
||||||
|
private final RelabelRequests.Query query = new RelabelRequests.Query(
|
||||||
|
1, 20, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantPageAndDetailUseOnlyAuthenticatedTenant() {
|
||||||
|
AuthenticatedUser user = user(100L, "MERCHANT");
|
||||||
|
controller.page(user, query);
|
||||||
|
controller.detail(user, "RL001");
|
||||||
|
verify(service).page("100", query);
|
||||||
|
verify(service).detail("100", "RL001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantEndpointRejectsPlatformIdentity() {
|
||||||
|
assertThrows(ResponseStatusException.class,
|
||||||
|
() -> controller.page(user(0L, "PLATFORM"), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||||
|
return new AuthenticatedUser(1L, tenantId, tenantId, 9L, "merchant", appCode, "WEB", Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantRelabelApplicationController controller() {
|
||||||
|
MerchantRelabelApplicationController controller = new MerchantRelabelApplicationController();
|
||||||
|
ReflectionTestUtils.setField(controller, "service", service);
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
package com.baiyue.lastleg.transfer.controller;
|
||||||
|
|
||||||
|
import com.baiyue.common.security.AuthenticatedUser;
|
||||||
|
import com.baiyue.lastleg.transfer.dto.WarehouseTransferRequests;
|
||||||
|
import com.baiyue.lastleg.transfer.service.WarehouseTransferService;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
class MerchantWarehouseTransferControllerTest {
|
||||||
|
private final WarehouseTransferService service = mock(WarehouseTransferService.class);
|
||||||
|
private final MerchantWarehouseTransferController controller = controller();
|
||||||
|
private final WarehouseTransferRequests.Query query = new WarehouseTransferRequests.Query(
|
||||||
|
1, 20, null, null, null, null, null, null, null, null);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantPageUsesOnlyAuthenticatedTenant() {
|
||||||
|
controller.page(user(100L, "MERCHANT"), query);
|
||||||
|
verify(service).page("100", query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantDetailUsesOnlyAuthenticatedTenant() {
|
||||||
|
controller.detail(user(100L, "MERCHANT"), "TR001");
|
||||||
|
verify(service).detail("100", "TR001");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void merchantEndpointRejectsPlatformIdentity() {
|
||||||
|
assertThrows(ResponseStatusException.class,
|
||||||
|
() -> controller.page(user(0L, "PLATFORM"), query));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AuthenticatedUser user(long tenantId, String appCode) {
|
||||||
|
return new AuthenticatedUser(1L, tenantId, tenantId, 9L, "merchant", appCode, "WEB", Set.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private MerchantWarehouseTransferController controller() {
|
||||||
|
MerchantWarehouseTransferController controller = new MerchantWarehouseTransferController();
|
||||||
|
ReflectionTestUtils.setField(controller, "service", service);
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-2
@@ -87,8 +87,7 @@ public class PlatformWarehouseResourceController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/pallets")
|
@PostMapping("/pallets")
|
||||||
@PreAuthorize("hasAuthority('" + OverseasWarehousePermissions.PALLET_CREATE + "')")
|
@Operation(summary = "Create a warehouse pallet", description = "Platform access required")
|
||||||
@Operation(summary = "Create a warehouse pallet", description = "Required authority: " + OverseasWarehousePermissions.PALLET_CREATE)
|
|
||||||
public ApiResponse<Long> createPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
public ApiResponse<Long> createPallet(@AuthenticationPrincipal AuthenticatedUser user,
|
||||||
@PathVariable long warehouseId,
|
@PathVariable long warehouseId,
|
||||||
@Valid @RequestBody PalletRequests.SaveRequest request) {
|
@Valid @RequestBody PalletRequests.SaveRequest request) {
|
||||||
|
|||||||
@@ -18,17 +18,17 @@ spring:
|
|||||||
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
|
- optional:nacos:${spring.application.name}.yml?group=BAIYUE&refreshEnabled=true
|
||||||
cloud:
|
cloud:
|
||||||
nacos:
|
nacos:
|
||||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
|
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||||
username: ${NACOS_USERNAME:nacos}
|
username: ${NACOS_USERNAME:nacos}
|
||||||
password: ${NACOS_PASSWORD:nacos}
|
password: ${NACOS_PASSWORD:nacos}
|
||||||
discovery:
|
discovery:
|
||||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
|
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||||
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
enabled: ${NACOS_DISCOVERY_ENABLED:true}
|
||||||
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
register-enabled: ${NACOS_REGISTER_ENABLED:true}
|
||||||
group: ${NACOS_GROUP:BAIYUE}
|
group: ${NACOS_GROUP:BAIYUE}
|
||||||
namespace: ${NACOS_NAMESPACE:public}
|
namespace: ${NACOS_NAMESPACE:public}
|
||||||
config:
|
config:
|
||||||
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8858}
|
server-addr: ${NACOS_SERVER_ADDR:127.0.0.1:8848}
|
||||||
namespace: ${NACOS_NAMESPACE:public}
|
namespace: ${NACOS_NAMESPACE:public}
|
||||||
datasource:
|
datasource:
|
||||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
|||||||
+8
-1
@@ -10,6 +10,7 @@ import java.util.Map;
|
|||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||||
|
|
||||||
class WarehouseResourcePermissionContractTest {
|
class WarehouseResourcePermissionContractTest {
|
||||||
private static final String PREFIX = "platform:overseas-warehouse:";
|
private static final String PREFIX = "platform:overseas-warehouse:";
|
||||||
@@ -27,9 +28,15 @@ class WarehouseResourcePermissionContractTest {
|
|||||||
assertPermission(methods.get("deleteLocation"), PREFIX + "location:delete");
|
assertPermission(methods.get("deleteLocation"), PREFIX + "location:delete");
|
||||||
assertPermission(methods.get("pallets"), PREFIX + "pallet:list");
|
assertPermission(methods.get("pallets"), PREFIX + "pallet:list");
|
||||||
assertPermission(methods.get("pallet"), PREFIX + "pallet:list");
|
assertPermission(methods.get("pallet"), PREFIX + "pallet:list");
|
||||||
assertPermission(methods.get("createPallet"), PREFIX + "pallet:create");
|
|
||||||
assertPermission(methods.get("updatePallet"), PREFIX + "pallet:edit");
|
assertPermission(methods.get("updatePallet"), PREFIX + "pallet:edit");
|
||||||
assertPermission(methods.get("deletePallet"), PREFIX + "pallet:delete");
|
assertPermission(methods.get("deletePallet"), PREFIX + "pallet:delete");
|
||||||
|
|
||||||
|
Method createPallet = Arrays.stream(PlatformWarehouseResourceController.class.getDeclaredMethods())
|
||||||
|
.filter(method -> "createPallet".equals(method.getName()))
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
assertNull(createPallet.getAnnotation(PreAuthorize.class));
|
||||||
|
assertEquals("Platform access required", createPallet.getAnnotation(Operation.class).description());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertPermission(Method method, String authority) {
|
private static void assertPermission(Method method, String authority) {
|
||||||
|
|||||||
Reference in New Issue
Block a user