diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java index 7e82f21b1..08721e58f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java @@ -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 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 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") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchContainerResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchContainerResponse.java new file mode 100644 index 000000000..5f50ba7f6 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchContainerResponse.java @@ -0,0 +1,5 @@ +package com.baiyue.lastleg.dispatch.dto; + +import java.util.List; + +public record DispatchContainerResponse(String id, int capacity, List palletIds) {} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java index 75949095b..9f7a3f5cc 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java @@ -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 containers) { + public DispatchDraftResponse(String draftToken, String planIdsJson, String containersJson) { this(draftToken, planIdsJson, containersJson, List.of()); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java index d0176a129..b4627ab55 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java @@ -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) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java index 7e569b07f..bf4ce5818 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java @@ -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; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java new file mode 100644 index 000000000..74599d6bc --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java @@ -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; +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java index 4869e059b..a62da4a4d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java @@ -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; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java new file mode 100644 index 000000000..43b428aaf --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java @@ -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 { + @Select("SELECT * FROM ll_dispatch_draft_container WHERE draft_token=#{draftToken} ORDER BY sequence_no,id") + List findByToken(@Param("draftToken") String draftToken); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index 6170a55e4..54a7aa03e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -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 planIds) { @@ -64,6 +68,10 @@ public class DispatchService { return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(sourceStage), trimToNull(draftToken)); } + public List candidates(String customerId, List planIds, String orderNo, + String fbaWarehouseCode, String sourceStage) { + return candidates(customerId, planIds, orderNo, fbaWarehouseCode, sourceStage, null); + } public List 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 platformCandidates(String orderNo, String fbaWarehouseCode, String sourceStage) { + return platformCandidates(orderNo, fbaWarehouseCode, sourceStage, null); + } public List 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 containers(String token) { + return draftContainers.findByToken(token).stream().map(c -> new DispatchContainerResponse(String.valueOf(c.getId()), c.getCapacity(), + locks.selectList(new LambdaQueryWrapper().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().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 bound = locks.selectList(new LambdaQueryWrapper().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)) { diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index 6d4e355ff..48bf8e2a2 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -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;