修改提交

This commit is contained in:
2026-08-21 17:38:14 +08:00
parent bbd303cd65
commit d1d269e96f
10 changed files with 135 additions and 6 deletions
@@ -17,6 +17,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -90,6 +92,19 @@ public class SmartDispatchController {
return ApiResponse.success(null);
}
@PostMapping("/drafts/containers")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
public ApiResponse<com.baiyue.lastleg.dispatch.dto.DispatchContainerResponse> createContainer(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody DispatchRequests.DraftContainer request) {
requirePlatform(user); return ApiResponse.success(service.createDraftContainer(operatorId(user), request));
}
@DeleteMapping("/drafts/{draftToken}/containers/{containerId}")
@PreAuthorize("hasAuthority('" + OverseasPermissions.INBOUND_ORDER_SMART_DISPATCH + "')")
public ApiResponse<Void> deleteContainer(@AuthenticationPrincipal AuthenticatedUser user, @PathVariable String draftToken, @PathVariable Long containerId) {
requirePlatform(user); service.deleteDraftContainer(operatorId(user), draftToken, containerId); 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")
@@ -0,0 +1,5 @@
package com.baiyue.lastleg.dispatch.dto;
import java.util.List;
public record DispatchContainerResponse(String id, int capacity, List<String> palletIds) {}
@@ -1,4 +1,6 @@
package com.baiyue.lastleg.dispatch.dto;
public record DispatchDraftResponse(String draftToken, String planIdsJson, String containersJson) {
import java.util.List;
public record DispatchDraftResponse(String draftToken, String planIdsJson, String containersJson, List<DispatchContainerResponse> containers) {
public DispatchDraftResponse(String draftToken, String planIdsJson, String containersJson) { this(draftToken, planIdsJson, containersJson, List.of()); }
}
@@ -30,9 +30,12 @@ public final class DispatchRequests {
}
}
public record DraftPallet(@NotNull Long planPalletId, @NotBlank @Size(max = 64) String draftToken) {
public record DraftPallet(@NotNull Long planPalletId, @NotBlank @Size(max = 64) String draftToken, Long draftContainerId) {
public DraftPallet(Long planPalletId, String draftToken) { this(planPalletId, draftToken, null); }
}
public record DraftContainer(@NotBlank @Size(max = 64) String draftToken, @NotNull @Min(1) @Max(500) Integer capacity) {}
public record DraftRelease(@NotBlank @Size(max = 64) String draftToken) {
}
@@ -2,11 +2,13 @@ package com.baiyue.lastleg.dispatch.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_dispatch_draft")
public class DispatchDraft {
@TableId(type = IdType.INPUT)
private Long id;
@@ -0,0 +1,20 @@
package com.baiyue.lastleg.dispatch.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("ll_dispatch_draft_container")
public class DispatchDraftContainer {
@TableId(type = IdType.INPUT)
private Long id;
private Long draftId;
private String draftToken;
private Integer sequenceNo;
private Integer capacity;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -24,6 +24,7 @@ public class DispatchPalletLock {
private Long planPalletId;
private Long warehousePalletId;
private Long dispatchOrderId;
private Long draftContainerId;
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String draftToken;
private String operatorId;
@@ -0,0 +1,14 @@
package com.baiyue.lastleg.dispatch.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.lastleg.dispatch.entity.DispatchDraftContainer;
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 DispatchDraftContainerMapper extends BaseMapper<DispatchDraftContainer> {
@Select("SELECT * FROM ll_dispatch_draft_container WHERE draft_token=#{draftToken} ORDER BY sequence_no,id")
List<DispatchDraftContainer> findByToken(@Param("draftToken") String draftToken);
}
@@ -10,11 +10,14 @@ import com.baiyue.lastleg.dispatch.entity.DispatchItem;
import com.baiyue.lastleg.dispatch.entity.DispatchOrder;
import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock;
import com.baiyue.lastleg.dispatch.entity.DispatchDraft;
import com.baiyue.lastleg.dispatch.entity.DispatchDraftContainer;
import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchDraftMapper;
import com.baiyue.lastleg.dispatch.mapper.DispatchDraftContainerMapper;
import com.baiyue.lastleg.dispatch.dto.DispatchContainerResponse;
import com.baiyue.lastleg.dispatch.dto.DispatchDraftResponse;
import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
import jakarta.annotation.Resource;
@@ -43,6 +46,7 @@ public class DispatchService {
@Resource private DispatchItemMapper items;
@Resource private DispatchPalletLockMapper locks;
@Resource private DispatchDraftMapper drafts;
@Resource private DispatchDraftContainerMapper draftContainers;
@Resource private DispatchCandidateMapper candidates;
public String resolveCustomerScopeForPlans(List<Long> planIds) {
@@ -64,6 +68,10 @@ public class DispatchService {
return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
trimToNull(sourceStage), trimToNull(draftToken));
}
public List<DispatchPalletCandidate> candidates(String customerId, List<Long> planIds, String orderNo,
String fbaWarehouseCode, String sourceStage) {
return candidates(customerId, planIds, orderNo, fbaWarehouseCode, sourceStage, null);
}
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode,
String sourceStage, String draftToken) {
@@ -71,10 +79,32 @@ public class DispatchService {
return candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode),
trimToNull(sourceStage), trimToNull(draftToken));
}
public List<DispatchPalletCandidate> platformCandidates(String orderNo, String fbaWarehouseCode, String sourceStage) {
return platformCandidates(orderNo, fbaWarehouseCode, sourceStage, null);
}
public List<DispatchDraftResponse> activeDrafts(String operatorId) {
return drafts.findActiveByOperator(trimToNull(operatorId)).stream()
.map(item -> new DispatchDraftResponse(item.getDraftToken(), item.getPlanIdsJson(), item.getContainersJson())).toList();
.map(item -> new DispatchDraftResponse(item.getDraftToken(), item.getPlanIdsJson(), item.getContainersJson(), containers(item.getDraftToken()))).toList();
}
private List<DispatchContainerResponse> containers(String token) {
return draftContainers.findByToken(token).stream().map(c -> new DispatchContainerResponse(String.valueOf(c.getId()), c.getCapacity(),
locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>().eq(DispatchPalletLock::getDraftContainerId, c.getId()).eq(DispatchPalletLock::getStatus, "ACTIVE")).stream().map(x -> String.valueOf(x.getPlanPalletId())).toList())).toList();
}
@Transactional
public DispatchContainerResponse createDraftContainer(String operatorId, DispatchRequests.DraftContainer request) {
DispatchDraft draft = ensureDraft(operatorId, request.draftToken());
int next = draftContainers.findByToken(request.draftToken()).stream().mapToInt(DispatchDraftContainer::getSequenceNo).max().orElse(0) + 1;
DispatchDraftContainer c = new DispatchDraftContainer(); c.setId(IdWorker.getId()); c.setDraftId(draft.getId()); c.setDraftToken(request.draftToken()); c.setSequenceNo(next); c.setCapacity(request.capacity()); c.setCreatedAt(LocalDateTime.now()); c.setUpdatedAt(LocalDateTime.now()); draftContainers.insert(c);
return new DispatchContainerResponse(String.valueOf(c.getId()), c.getCapacity(), List.of());
}
private DispatchDraft ensureDraft(String operatorId, String token) {
DispatchDraft draft = drafts.lockActive(token, trimToNull(operatorId));
if (draft != null) return draft;
LocalDateTime now = LocalDateTime.now(); draft = new DispatchDraft(); draft.setId(IdWorker.getId()); draft.setDraftToken(token); draft.setOperatorId(trimToNull(operatorId)); draft.setStatus("ACTIVE"); draft.setCreatedAt(now); draft.setUpdatedAt(now); draft.setExpiresAt(now.plusMinutes(30)); drafts.insert(draft); return draft;
}
@Transactional
@@ -94,6 +124,12 @@ public class DispatchService {
@Transactional
public void lockDraftPallet(String customerId, String operatorId, DispatchRequests.DraftPallet request) {
DispatchDraft draft = ensureDraft(operatorId, request.draftToken());
DispatchDraftContainer boundContainer = request.draftContainerId() == null ? null : draftContainers.selectById(request.draftContainerId());
if (boundContainer == null && request.draftContainerId() == null) {
boundContainer = new DispatchDraftContainer(); boundContainer.setId(IdWorker.getId()); boundContainer.setDraftId(draft.getId()); boundContainer.setDraftToken(request.draftToken()); boundContainer.setSequenceNo(1); boundContainer.setCapacity(500); boundContainer.setCreatedAt(LocalDateTime.now()); boundContainer.setUpdatedAt(LocalDateTime.now()); draftContainers.insert(boundContainer);
}
if (boundContainer == null || !request.draftToken().equals(boundContainer.getDraftToken()) || !Objects.equals(boundContainer.getDraftId(), draft.getId())) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "装车托盘不存在");
DispatchPalletCandidate candidate = requireDraftCandidate(customerId, request.planPalletId());
LocalDateTime now = LocalDateTime.now();
DispatchPalletLock active = activeLock(customerId, candidate);
@@ -118,6 +154,7 @@ public class DispatchService {
lock.setPlanPalletId(candidate.planPalletId());
lock.setWarehousePalletId(candidate.warehousePalletId());
lock.setDraftToken(request.draftToken());
lock.setDraftContainerId(request.draftContainerId());
lock.setOperatorId(trimToNull(operatorId));
lock.setStatus("ACTIVE");
lock.setLockedAt(now);
@@ -149,10 +186,21 @@ public class DispatchService {
.eq(DispatchPalletLock::getStatus, "ACTIVE"));
LocalDateTime now = LocalDateTime.now();
activeLocks.forEach(lock -> releaseLock(lock, now));
draftContainers.delete(new LambdaQueryWrapper<DispatchDraftContainer>().eq(DispatchDraftContainer::getDraftToken, draftToken));
DispatchDraft draft = drafts.lockActive(draftToken, trimToNull(operatorId));
if (draft != null) { draft.setStatus("RELEASED"); draft.setReleasedAt(now); draft.setUpdatedAt(now); drafts.updateById(draft); }
}
@Transactional
public void deleteDraftContainer(String operatorId, String token, Long containerId) {
if (drafts.lockActive(token, trimToNull(operatorId)) == null) throw new ResponseStatusException(HttpStatus.CONFLICT, "草稿不存在或已失效");
DispatchDraftContainer c = draftContainers.selectById(containerId);
if (c == null || !token.equals(c.getDraftToken())) return;
List<DispatchPalletLock> bound = locks.selectList(new LambdaQueryWrapper<DispatchPalletLock>().eq(DispatchPalletLock::getDraftContainerId, containerId).eq(DispatchPalletLock::getStatus, "ACTIVE"));
bound.forEach(lock -> { requireDraftOwner(lock, operatorId, token); releaseLock(lock, LocalDateTime.now()); });
draftContainers.deleteById(containerId);
}
@Transactional
public DispatchResponse preArrange(String customerId, String operatorId, DispatchRequests.PreArrange request) {
if (!StringUtils.hasText(customerId)) {
@@ -820,7 +820,7 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_item (
KEY idx_ll_dispatch_item_pallet (customer_id,plan_pallet_id,warehouse_pallet_id)
) ENGINE=InnoDB COMMENT='派车货物冻结明细';
CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
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 '来源托盘业务键',
@@ -828,7 +828,8 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
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,草稿锁阶段为空',
dispatch_order_id BIGINT NULL COMMENT '正式派车单ID,草稿锁阶段为空',
draft_container_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 '业务状态',
@@ -841,7 +842,25 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock (
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='派车托盘排他锁';
) ENGINE=InnoDB COMMENT='派车托盘排他锁';
CREATE TABLE IF NOT EXISTS ll_dispatch_draft (
id BIGINT NOT NULL, draft_token VARCHAR(64) NOT NULL, customer_id VARCHAR(128) NULL,
operator_id VARCHAR(128) NULL, plan_ids_json VARCHAR(4000) NULL, containers_json LONGTEXT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE', expires_at DATETIME(3) NULL, released_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id), UNIQUE KEY uk_ll_dispatch_draft_token(draft_token), KEY idx_ll_dispatch_draft_operator(operator_id,status)
) ENGINE=InnoDB COMMENT='智能排车草稿';
CREATE TABLE IF NOT EXISTS ll_dispatch_draft_container (
id BIGINT NOT NULL, draft_id BIGINT NOT NULL, draft_token VARCHAR(64) NOT NULL,
sequence_no INT NOT NULL, capacity INT NOT NULL, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY(id),
KEY idx_ll_dispatch_draft_container_token(draft_token)
) 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_container_id'), 'SELECT 1', 'ALTER TABLE ll_dispatch_pallet_lock ADD COLUMN draft_container_id BIGINT NULL COMMENT ''智能排车草稿装车托盘ID'' AFTER dispatch_order_id'));
PREPARE dispatch_draft_container_column_stmt FROM @ddl; EXECUTE dispatch_draft_container_column_stmt; DEALLOCATE PREPARE dispatch_draft_container_column_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='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;