Merge remote-tracking branch 'origin/test' into test

# Conflicts:
#	deploy/sql/头程数据库统一增量-MySQL8.sql
This commit is contained in:
2026-08-10 13:07:15 +08:00
12 changed files with 18 additions and 330 deletions
@@ -8585,7 +8585,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_operation (
UNIQUE KEY uk_loading_pick_operation_id (operation_id), UNIQUE KEY uk_loading_pick_operation_id (operation_id),
KEY idx_loading_pick_operation_task (pick_task_id, created_at), KEY idx_loading_pick_operation_task (pick_task_id, created_at),
CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW')) CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'REVIEW'))
) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计'; ) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计';
DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update; DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update;
@@ -8735,58 +8735,6 @@ CALL phase3_pda_add_column('fl_loading_pick_item', 'release_mode',
'`release_mode` VARCHAR(10) NULL COMMENT ''AUTO按比例释放,MANUAL按人工选择释放'' AFTER `item_status`'); '`release_mode` VARCHAR(10) NULL COMMENT ''AUTO按比例释放,MANUAL按人工选择释放'' AFTER `item_status`');
CALL phase3_pda_add_column('fl_loading_pick_item', 'release_location_ids', CALL phase3_pda_add_column('fl_loading_pick_item', 'release_location_ids',
'`release_location_ids` JSON NULL COMMENT ''人工选择释放库位ID;空数组表示明确释放0个'' AFTER `release_mode`'); '`release_location_ids` JSON NULL COMMENT ''人工选择释放库位ID;空数组表示明确释放0个'' AFTER `release_mode`');
-- PDA 多人协作:领取粒度由整柜任务下沉为单条拣货明细。
CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment (
pick_item_id BIGINT UNSIGNED NOT NULL,
assignee_id BIGINT UNSIGNED NOT NULL,
assignee_name VARCHAR(64) NOT NULL,
assigned_by BIGINT UNSIGNED NOT NULL,
assigned_by_name VARCHAR(64) NULL,
assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED',
version INT UNSIGNED NOT NULL DEFAULT 0,
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 (pick_item_id),
KEY idx_loading_pick_item_assignee (assignee_id, updated_at),
CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
CONSTRAINT chk_loading_pick_item_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED'))
) ENGINE=InnoDB COMMENT='PDA拣货明细领取与作业员分配';
SET @phase3_pda_check_ddl = IF(
EXISTS (SELECT 1 FROM information_schema.table_constraints
WHERE constraint_schema = DATABASE()
AND table_name = 'fl_loading_pick_operation'
AND constraint_name = 'chk_loading_pick_operation_action'
AND constraint_type = 'CHECK'),
'ALTER TABLE fl_loading_pick_operation DROP CHECK chk_loading_pick_operation_action',
'SELECT 1');
PREPARE phase3_pda_check_stmt FROM @phase3_pda_check_ddl;
EXECUTE phase3_pda_check_stmt;
DEALLOCATE PREPARE phase3_pda_check_stmt;
ALTER TABLE fl_loading_pick_operation
ADD CONSTRAINT chk_loading_pick_operation_action
CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW'));
DROP PROCEDURE IF EXISTS assert_phase3_pda_item_assignment_schema;
DELIMITER $$
CREATE PROCEDURE assert_phase3_pda_item_assignment_schema()
BEGIN
IF (SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_pick_item_assignment') <> 1
OR (SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema='baiyue_firstleg' AND table_name='fl_loading_pick_item_assignment'
AND index_name='idx_loading_pick_item_assignee') <> 2
OR (SELECT COUNT(*) FROM information_schema.referential_constraints
WHERE constraint_schema='baiyue_firstleg'
AND constraint_name='fk_loading_pick_item_assignment_item') <> 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='PDA明细级多人分配表创建失败';
END IF;
END$$
DELIMITER ;
CALL assert_phase3_pda_item_assignment_schema();
DROP PROCEDURE IF EXISTS assert_phase3_pda_item_assignment_schema;
CALL phase3_pda_add_column('fl_inbound_pallet_allocation', 'reservation_id', CALL phase3_pda_add_column('fl_inbound_pallet_allocation', 'reservation_id',
'`reservation_id` BIGINT UNSIGNED NULL COMMENT ''仓库预占资源唯一ID,A0共享场景用于区分托盘'' AFTER `location_code`'); '`reservation_id` BIGINT UNSIGNED NULL COMMENT ''仓库预占资源唯一ID,A0共享场景用于区分托盘'' AFTER `location_code`');
@@ -62,28 +62,6 @@ public class PlatformLoadingPdaController {
return ApiResponse.success(service.assign(taskId, request, user.userId(), user.username())); return ApiResponse.success(service.assign(taskId, request, user.userId(), user.username()));
} }
/** 同一柜按 PDA 明细领取,多个作业员可分别处理不同入库单明细。 */
@PostMapping("/tasks/{taskId}/items/{itemId}/claim")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> claimItem(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long itemId,
@Valid @RequestBody ClaimRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.claimItem(taskId, itemId, request, user.userId(), user.username()));
}
/** 管理员可将单条 PDA 明细分配给指定仓库作业员。 */
@PostMapping("/tasks/{taskId}/items/{itemId}/assign")
@PreAuthorize("hasAuthority('platform:firstleg:pda:assign')")
public ApiResponse<Task> assignItem(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Positive long taskId,
@PathVariable @Positive long itemId,
@Valid @RequestBody AssignRequest request) {
FirstlegScopeGuard.platform(user);
return ApiResponse.success(service.assignItem(taskId, itemId, request, user.userId(), user.username()));
}
@PostMapping("/tasks/{taskId}/items/{itemId}/pick") @PostMapping("/tasks/{taskId}/items/{itemId}/pick")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')") @PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
public ApiResponse<Task> pick(@AuthenticationPrincipal AuthenticatedUser user, public ApiResponse<Task> pick(@AuthenticationPrincipal AuthenticatedUser user,
@@ -76,7 +76,7 @@ public final class LoadingPickDtos {
public record PickItem(Long id, Long loadingPlanItemId, Long inboundId, Long inboundDetailId, public record PickItem(Long id, Long loadingPlanItemId, Long inboundId, Long inboundDetailId,
Integer plannedPackageCount, Integer pickedPackageCount, Integer plannedPackageCount, Integer pickedPackageCount,
String itemStatus, Integer version, Assignment assignment) { String itemStatus, Integer version) {
} }
public record Assignment(Long assigneeId, String assigneeName, Long assignedBy, public record Assignment(Long assigneeId, String assigneeName, Long assignedBy,
@@ -1,25 +0,0 @@
package com.baiyue.firstleg.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
/** PDA 拣货明细的作业员领取或分配记录。 */
@Getter
@Setter
@TableName("fl_loading_pick_item_assignment")
public class LoadingPickItemAssignmentEntity {
@TableId
private Long pickItemId;
private Long assigneeId;
private String assigneeName;
private Long assignedBy;
private String assignedByName;
private String assignmentType;
private Integer version;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
@@ -1,27 +0,0 @@
package com.baiyue.firstleg.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/** 明细级作业员分配,允许同一柜的不同明细并行领取。 */
@Mapper
public interface LoadingPickItemAssignmentMapper extends BaseMapper<LoadingPickItemAssignmentEntity> {
@Insert("INSERT INTO fl_loading_pick_item_assignment "
+ "(pick_item_id,assignee_id,assignee_name,assigned_by,assigned_by_name,assignment_type,version) "
+ "VALUES (#{itemId},#{assigneeId},#{assigneeName},#{operatorId},#{operatorName},#{type},0)")
int create(@Param("itemId") long itemId, @Param("assigneeId") long assigneeId,
@Param("assigneeName") String assigneeName, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName, @Param("type") String type);
@Update("UPDATE fl_loading_pick_item_assignment SET assignee_id=#{assigneeId},assignee_name=#{assigneeName},"
+ "assigned_by=#{operatorId},assigned_by_name=#{operatorName},assignment_type=#{type},version=version+1 "
+ "WHERE pick_item_id=#{itemId} AND version=#{expectedVersion}")
int reassign(@Param("itemId") long itemId, @Param("assigneeId") long assigneeId,
@Param("assigneeName") String assigneeName, @Param("operatorId") long operatorId,
@Param("operatorName") String operatorName, @Param("type") String type,
@Param("expectedVersion") int expectedVersion);
}
@@ -9,11 +9,9 @@ import com.baiyue.firstleg.entity.LoadingPlanItemEntity;
import com.baiyue.firstleg.mapper.LoadingPickItemMapper; import com.baiyue.firstleg.mapper.LoadingPickItemMapper;
import com.baiyue.firstleg.mapper.LoadingPickTaskMapper; import com.baiyue.firstleg.mapper.LoadingPickTaskMapper;
import com.baiyue.firstleg.mapper.LoadingPickAssignmentMapper; import com.baiyue.firstleg.mapper.LoadingPickAssignmentMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemAssignmentMapper;
import com.baiyue.firstleg.mapper.LoadingPickOperationMapper; import com.baiyue.firstleg.mapper.LoadingPickOperationMapper;
import com.baiyue.firstleg.mapper.LoadingPickReviewMapper; import com.baiyue.firstleg.mapper.LoadingPickReviewMapper;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity; import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickReviewEntity; import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper; import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
@@ -35,8 +33,6 @@ public class LoadingPickRepository {
@Resource @Resource
private LoadingPickAssignmentMapper assignments; private LoadingPickAssignmentMapper assignments;
@Resource @Resource
private LoadingPickItemAssignmentMapper itemAssignments;
@Resource
private LoadingPickReviewMapper reviews; private LoadingPickReviewMapper reviews;
@Resource @Resource
private LoadingPickOperationMapper operations; private LoadingPickOperationMapper operations;
@@ -169,20 +165,6 @@ public class LoadingPickRepository {
type, expectedVersion); type, expectedVersion);
} }
public LoadingPickItemAssignmentEntity itemAssignment(long itemId) {
return itemAssignments.selectById(itemId);
}
public int createItemAssignment(long itemId, long assigneeId, String assigneeName,
long operatorId, String operatorName, String type) {
return itemAssignments.create(itemId, assigneeId, assigneeName, operatorId, operatorName, type);
}
public int reassignItem(long itemId, long assigneeId, String assigneeName, long operatorId,
String operatorName, String type, int expectedVersion) {
return itemAssignments.reassign(itemId, assigneeId, assigneeName, operatorId, operatorName,
type, expectedVersion);
}
public LoadingPickReviewEntity review(long taskId) { public LoadingPickReviewEntity review(long taskId) {
return reviews.selectById(taskId); return reviews.selectById(taskId);
@@ -14,7 +14,6 @@ import com.baiyue.firstleg.dto.LoadingPickDtos.AssignRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.Operation; import com.baiyue.firstleg.dto.LoadingPickDtos.Operation;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity; import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity; import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity; import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
@@ -119,38 +118,6 @@ public class LoadingPickService {
return view(task, null); return view(task, null);
} }
/**
* 领取单条 PDA 明细。柜次任务只是汇总容器,作业所有权必须以明细为粒度,
* 才能让多个仓库人员在同一柜中并行处理不同入库单。
*/
@Transactional
public Task claimItem(long taskId, long itemId, ClaimRequest request, long operatorId, String operatorName) {
String operationId = operation(request.operationId());
String requestHash = hash("ITEM_CLAIM|" + taskId + '|' + itemId + '|'
+ request.expectedAssignmentVersion());
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
if (replay != null) return replay;
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickItemEntity item = repository.item(taskId, itemId);
requireAssignableItem(item);
LoadingPickItemAssignmentEntity existing = repository.itemAssignment(itemId);
if (existing == null) {
try {
if (request.expectedAssignmentVersion() != 0 || repository.createItemAssignment(itemId, operatorId,
operatorName, operatorId, operatorName, "CLAIMED") != 1) {
throw conflict("PDA拣货明细已被其他作业员领取");
}
} catch (DuplicateKeyException exception) {
throw conflict("PDA拣货明细已被其他作业员领取");
}
} else if (!Long.valueOf(operatorId).equals(existing.getAssigneeId())
|| !request.expectedAssignmentVersion().equals(existing.getVersion())) {
throw conflict("PDA拣货明细已分配给其他作业员或分配版本已变化");
}
insertOperation(taskId, operationId, requestHash, "ITEM_CLAIM", operatorId, operatorName);
return view(task, null);
}
@Transactional @Transactional
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) { public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
String operationId = operation(request.operationId()); String operationId = operation(request.operationId());
@@ -172,29 +139,6 @@ public class LoadingPickService {
return view(task, null); return view(task, null);
} }
@Transactional
public Task assignItem(long taskId, long itemId, AssignRequest request, long operatorId, String operatorName) {
String operationId = operation(request.operationId());
String assigneeName = request.assigneeName().trim();
String requestHash = hash("ITEM_ASSIGN|" + taskId + '|' + itemId + '|'
+ request.assigneeId() + '|' + assigneeName + '|' + request.expectedAssignmentVersion());
Task replay = replayTaskOperation(taskId, operationId, requestHash, operatorId);
if (replay != null) return replay;
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickItemEntity item = repository.item(taskId, itemId);
requireAssignableItem(item);
LoadingPickItemAssignmentEntity existing = repository.itemAssignment(itemId);
int changed = existing == null
? request.expectedAssignmentVersion() == 0
? repository.createItemAssignment(itemId, request.assigneeId(), assigneeName, operatorId,
operatorName, "ASSIGNED") : 0
: repository.reassignItem(itemId, request.assigneeId(), assigneeName, operatorId, operatorName,
"ASSIGNED", request.expectedAssignmentVersion());
if (changed != 1) throw conflict("PDA拣货明细分配版本已变化");
insertOperation(taskId, operationId, requestHash, "ITEM_ASSIGN", operatorId, operatorName);
return view(task, null);
}
@Transactional @Transactional
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) { public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds()); ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
@@ -210,12 +154,11 @@ public class LoadingPickService {
} }
return view(requiredTask(taskId), request.warehouseId()); return view(requiredTask(taskId), request.warehouseId());
} }
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId); LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId()); LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId());
if (item == null) { if (item == null) {
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在"); throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
} }
requireItemAssignee(taskId, itemId, operatorId);
if (repository.pick(itemId, request.pickedPackageCount(), release.mode(), release.json(), if (repository.pick(itemId, request.pickedPackageCount(), release.mode(), release.json(),
request.version(), request.warehouseId()) != 1) { request.version(), request.warehouseId()) != 1) {
throw conflict("拣货数量超过计划箱数或明细版本已变化"); throw conflict("拣货数量超过计划箱数或明细版本已变化");
@@ -247,9 +190,8 @@ public class LoadingPickService {
assertReplay(replay, taskId, itemId, requestHash); assertReplay(replay, taskId, itemId, requestHash);
return view(requiredTask(taskId), request.warehouseId()); return view(requiredTask(taskId), request.warehouseId());
} }
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId); LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
requireWarehouseItem(taskId, itemId, request.warehouseId()); requireWarehouseItem(taskId, itemId, request.warehouseId());
requireItemAssignee(taskId, itemId, operatorId);
if (repository.partial(itemId, request.finalPickedPackageCount(), release.mode(), release.json(), if (repository.partial(itemId, request.finalPickedPackageCount(), release.mode(), release.json(),
request.version(), request.warehouseId()) != 1) { request.version(), request.warehouseId()) != 1) {
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化"); throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
@@ -272,9 +214,8 @@ public class LoadingPickService {
assertReplay(replay, taskId, itemId, requestHash); assertReplay(replay, taskId, itemId, requestHash);
return view(requiredTask(taskId), request.warehouseId()); return view(requiredTask(taskId), request.warehouseId());
} }
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId); LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId()); LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
requireItemAssignee(taskId, itemId, operatorId);
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) { if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选"); throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
} }
@@ -402,27 +343,14 @@ public class LoadingPickService {
return task; return task;
} }
private void requireItemAssignee(long taskId, long itemId, long operatorId) { private LoadingPickTaskEntity requiredAssignedLoadingTaskForUpdate(long taskId, long operatorId) {
LoadingPickItemAssignmentEntity assignment = repository.itemAssignment(itemId); LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
if (assignment != null) { LoadingPickAssignmentEntity assignment = repository.assignment(taskId);
if (!Long.valueOf(operatorId).equals(assignment.getAssigneeId())) { if (assignment == null) throw conflict("请先领取PDA拣货任务");
throw conflict("PDA拣货明细已分配给其他作业员"); if (!Long.valueOf(operatorId).equals(assignment.getAssigneeId())) {
} throw conflict("PDA拣货任务已分配给其他作业员");
return;
}
// 兼容已上线的整柜领取调用;新作业必须走明细领取接口。
LoadingPickAssignmentEntity legacy = repository.assignment(taskId);
if (legacy == null || !Long.valueOf(operatorId).equals(legacy.getAssigneeId())) {
throw conflict("请先领取PDA拣货明细");
}
}
private static void requireAssignableItem(LoadingPickItemEntity item) {
if (item == null) throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
if ("PICKED".equals(item.getItemStatus()) || "PARTIAL".equals(item.getItemStatus())
|| "SKIPPED".equals(item.getItemStatus())) {
throw conflict("已完成或已跳拣的PDA明细不能再领取或改派");
} }
return task;
} }
private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) { private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) {
@@ -436,15 +364,10 @@ public class LoadingPickService {
private Task view(LoadingPickTaskEntity task, Long warehouseId) { private Task view(LoadingPickTaskEntity task, Long warehouseId) {
List<LoadingPickItemEntity> source = warehouseId == null List<LoadingPickItemEntity> source = warehouseId == null
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId); ? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
List<PickItem> items = source.stream().map(item -> { List<PickItem> items = source.stream()
LoadingPickItemAssignmentEntity assignedItem = repository.itemAssignment(item.getId()); .map(item -> new PickItem(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
Assignment itemAssignment = assignedItem == null ? null : new Assignment(assignedItem.getAssigneeId(),
assignedItem.getAssigneeName(), assignedItem.getAssignedBy(), assignedItem.getAssignedByName(),
assignedItem.getAssignmentType(), assignedItem.getVersion());
return new PickItem(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
item.getInboundDetailId(), item.getPlannedPackageCount(), item.getPickedPackageCount(), item.getInboundDetailId(), item.getPlannedPackageCount(), item.getPickedPackageCount(),
item.getItemStatus(), item.getVersion(), itemAssignment); item.getItemStatus(), item.getVersion())).toList();
}).toList();
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId()); LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
LoadingPickReviewEntity reviewed = repository.review(task.getId()); LoadingPickReviewEntity reviewed = repository.review(task.getId());
Assignment assignment = assigned == null ? null : new Assignment(assigned.getAssigneeId(), Assignment assignment = assigned == null ? null : new Assignment(assigned.getAssigneeId(),
@@ -452,11 +375,9 @@ public class LoadingPickService {
assigned.getAssignmentType(), assigned.getVersion()); assigned.getAssignmentType(), assigned.getVersion());
Review review = reviewed == null ? null : new Review(reviewed.getReviewStatus(), Review review = reviewed == null ? null : new Review(reviewed.getReviewStatus(),
reviewed.getReviewedBy(), reviewed.getReviewedByName(), reviewed.getRemark(), reviewed.getVersion()); reviewed.getReviewedBy(), reviewed.getReviewedByName(), reviewed.getRemark(), reviewed.getVersion());
boolean hasPickedItem = source.stream().anyMatch(item -> item.getPickedPackageCount() != null
&& item.getPickedPackageCount() > 0);
String workflowStatus = "COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED" String workflowStatus = "COMPLETED".equals(task.getTaskStatus()) ? "COMPLETED"
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : assigned == null ? "UNASSIGNED"
: hasPickedItem ? "IN_PROGRESS" : "PENDING"; : repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(), return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(),
workflowStatus, task.getVersion(), assignment, review, items); workflowStatus, task.getVersion(), assignment, review, items);
} }
@@ -45,25 +45,9 @@ CREATE TABLE IF NOT EXISTS fl_loading_pick_operation (
UNIQUE KEY uk_loading_pick_operation_id (operation_id), UNIQUE KEY uk_loading_pick_operation_id (operation_id),
KEY idx_loading_pick_operation_task (pick_task_id, created_at), KEY idx_loading_pick_operation_task (pick_task_id, created_at),
CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id), CONSTRAINT fk_loading_pick_operation_task FOREIGN KEY (pick_task_id) REFERENCES fl_loading_pick_task (id),
CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'ITEM_CLAIM', 'ITEM_ASSIGN', 'REVIEW')) CONSTRAINT chk_loading_pick_operation_action CHECK (action_type IN ('CLAIM', 'ASSIGN', 'REVIEW'))
) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计'; ) ENGINE=InnoDB COMMENT='柜次PDA任务级幂等审计';
CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment (
pick_item_id BIGINT UNSIGNED NOT NULL,
assignee_id BIGINT UNSIGNED NOT NULL,
assignee_name VARCHAR(64) NOT NULL,
assigned_by BIGINT UNSIGNED NOT NULL,
assigned_by_name VARCHAR(64) NULL,
assignment_type VARCHAR(16) NOT NULL COMMENT 'CLAIMED/ASSIGNED',
version INT UNSIGNED NOT NULL DEFAULT 0,
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 (pick_item_id),
KEY idx_loading_pick_item_assignee (assignee_id, updated_at),
CONSTRAINT fk_loading_pick_item_assignment_item FOREIGN KEY (pick_item_id) REFERENCES fl_loading_pick_item (id),
CONSTRAINT chk_loading_pick_item_assignment_type CHECK (assignment_type IN ('CLAIMED', 'ASSIGNED'))
) ENGINE=InnoDB COMMENT='PDA拣货明细领取与作业员分配';
DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update; DROP TRIGGER IF EXISTS trg_loading_pick_review_before_update;
DELIMITER $$ DELIMITER $$
CREATE TRIGGER trg_loading_pick_review_before_update CREATE TRIGGER trg_loading_pick_review_before_update
@@ -29,24 +29,6 @@ paths:
content: {application/json: {schema: {$ref: '#/components/schemas/AssignRequest'}}} content: {application/json: {schema: {$ref: '#/components/schemas/AssignRequest'}}}
responses: responses:
'200': {description: Assigned task or A0409 on assignment version conflict} '200': {description: Assigned task or A0409 on assignment version conflict}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/claim:
post:
summary: 领取单条PDA拣货明细;同柜不同明细可由不同仓库人员并行作业
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/ItemId'}]
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/ClaimRequest'}}}
responses:
'200': {description: 返回含明细作业员信息的PDA任务}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/assign:
post:
summary: 将单条PDA拣货明细分配或改派给指定仓库人员
parameters: [{$ref: '#/components/parameters/TaskId'}, {$ref: '#/components/parameters/ItemId'}]
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/AssignRequest'}}}
responses:
'200': {description: 返回含明细作业员信息的PDA任务}
/api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/pick: /api/platform/v1/first-leg/pda-loading/tasks/{taskId}/items/{itemId}/pick:
post: post:
summary: Increment a picked item owned by the authenticated assignee summary: Increment a picked item owned by the authenticated assignee
@@ -16,8 +16,6 @@ class PdaConvergenceOpenApiContractTest {
.contains("/api/platform/v1/first-leg/pda-loading/tasks:") .contains("/api/platform/v1/first-leg/pda-loading/tasks:")
.contains("/tasks/{taskId}/claim:") .contains("/tasks/{taskId}/claim:")
.contains("/tasks/{taskId}/assign:") .contains("/tasks/{taskId}/assign:")
.contains("/tasks/{taskId}/items/{itemId}/claim:")
.contains("/tasks/{taskId}/items/{itemId}/assign:")
.contains("/tasks/{taskId}/items/{itemId}/pick:") .contains("/tasks/{taskId}/items/{itemId}/pick:")
.contains("/tasks/{taskId}/items/{itemId}/partial:") .contains("/tasks/{taskId}/items/{itemId}/partial:")
.contains("/tasks/{taskId}/items/{itemId}/skip:") .contains("/tasks/{taskId}/items/{itemId}/skip:")
@@ -18,10 +18,8 @@ class PdaConvergenceSqlContractTest {
.getContentAsString(StandardCharsets.UTF_8); .getContentAsString(StandardCharsets.UTF_8);
assertThat(sql) assertThat(sql)
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_assignment")
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_item_assignment")
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_review")
.contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation") .contains("CREATE TABLE IF NOT EXISTS fl_loading_pick_operation")
.contains("'ITEM_CLAIM', 'ITEM_ASSIGN'")
.contains("UNIQUE KEY uk_loading_pick_operation_id") .contains("UNIQUE KEY uk_loading_pick_operation_id")
.contains("trg_loading_pick_review_before_update") .contains("trg_loading_pick_review_before_update")
.contains("trg_loading_pick_operation_before_delete") .contains("trg_loading_pick_operation_before_delete")
@@ -3,13 +3,11 @@ package com.baiyue.firstleg.service;
import com.baiyue.common.core.exception.BizException; import com.baiyue.common.core.exception.BizException;
import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.PartialPickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.PickRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ClaimRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.ReviewRequest;
import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest; import com.baiyue.firstleg.dto.LoadingPickDtos.SkipRequest;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity; import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity; import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity; import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity; import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickTaskEntity; import com.baiyue.firstleg.entity.LoadingPickTaskEntity;
import com.baiyue.firstleg.repository.LoadingPickRepository; import com.baiyue.firstleg.repository.LoadingPickRepository;
@@ -112,47 +110,6 @@ class LoadingPickServiceTest {
verify(settlementService, never()).settle(20L, 9L, "operator"); verify(settlementService, never()).settle(20L, 9L, "operator");
} }
@Test
void permitsItemAssigneeWithoutCabinetAssignee() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity item = item();
when(repository.history("item-owner-pick")).thenReturn(null);
when(repository.task(20L)).thenReturn(task);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.item(20L, 30L, 101L)).thenReturn(item);
when(repository.itemAssignment(30L)).thenReturn(itemAssignment(9L, "operator"));
when(repository.pick(30L, 2, "MANUAL", "[]", 0, 101L)).thenReturn(1);
when(repository.touchTask(20L, 0)).thenReturn(1);
when(repository.items(20L, 101L)).thenReturn(List.of(item));
service.pick(20L, 30L, new PickRequest(101L, 2, List.of(), 0, "item-owner-pick"),
9L, "operator");
}
@Test
void permitsTwoWorkersToClaimDifferentItemsInOneCabinet() {
LoadingPickTaskEntity task = task();
LoadingPickItemEntity first = item();
LoadingPickItemEntity second = item();
second.setId(31L);
when(repository.taskForUpdate(20L)).thenReturn(task);
when(repository.task(20L)).thenReturn(task);
when(repository.item(20L, 30L)).thenReturn(first);
when(repository.item(20L, 31L)).thenReturn(second);
when(repository.itemAssignment(30L)).thenReturn(null);
when(repository.itemAssignment(31L)).thenReturn(null);
when(repository.createItemAssignment(30L, 9L, "worker-a", 9L, "worker-a", "CLAIMED")).thenReturn(1);
when(repository.createItemAssignment(31L, 10L, "worker-b", 10L, "worker-b", "CLAIMED")).thenReturn(1);
when(repository.items(20L)).thenReturn(List.of(first, second));
service.claimItem(20L, 30L, new ClaimRequest(0, "claim-a"), 9L, "worker-a");
service.claimItem(20L, 31L, new ClaimRequest(0, "claim-b"), 10L, "worker-b");
verify(repository).createItemAssignment(30L, 9L, "worker-a", 9L, "worker-a", "CLAIMED");
verify(repository).createItemAssignment(31L, 10L, "worker-b", 10L, "worker-b", "CLAIMED");
}
@Test @Test
void reviewSettlesResolvedItemsAndCompletesPlan() { void reviewSettlesResolvedItemsAndCompletesPlan() {
LoadingPickTaskEntity task = task(); LoadingPickTaskEntity task = task();
@@ -231,12 +188,4 @@ class LoadingPickServiceTest {
return assignment; return assignment;
} }
private static LoadingPickItemAssignmentEntity itemAssignment(long assigneeId, String assigneeName) {
LoadingPickItemAssignmentEntity assignment = new LoadingPickItemAssignmentEntity();
assignment.setPickItemId(30L);
assignment.setAssigneeId(assigneeId);
assignment.setAssigneeName(assigneeName);
assignment.setVersion(0);
return assignment;
}
} }