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
@@ -62,28 +62,6 @@ public class PlatformLoadingPdaController {
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")
@PreAuthorize("hasAuthority('platform:firstleg:pda:work')")
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,
Integer plannedPackageCount, Integer pickedPackageCount,
String itemStatus, Integer version, Assignment assignment) {
String itemStatus, Integer version) {
}
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.LoadingPickTaskMapper;
import com.baiyue.firstleg.mapper.LoadingPickAssignmentMapper;
import com.baiyue.firstleg.mapper.LoadingPickItemAssignmentMapper;
import com.baiyue.firstleg.mapper.LoadingPickOperationMapper;
import com.baiyue.firstleg.mapper.LoadingPickReviewMapper;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
import com.baiyue.firstleg.entity.LoadingPickReviewEntity;
import com.baiyue.firstleg.loading.mapper.InboundLoadingPickTaskMapper;
@@ -35,8 +33,6 @@ public class LoadingPickRepository {
@Resource
private LoadingPickAssignmentMapper assignments;
@Resource
private LoadingPickItemAssignmentMapper itemAssignments;
@Resource
private LoadingPickReviewMapper reviews;
@Resource
private LoadingPickOperationMapper operations;
@@ -169,20 +165,6 @@ public class LoadingPickRepository {
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) {
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.Operation;
import com.baiyue.firstleg.entity.LoadingPickAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickItemAssignmentEntity;
import com.baiyue.firstleg.entity.LoadingPickHistoryEntity;
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
import com.baiyue.firstleg.entity.LoadingPickOperationEntity;
@@ -119,38 +118,6 @@ public class LoadingPickService {
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
public Task assign(long taskId, AssignRequest request, long operatorId, String operatorName) {
String operationId = operation(request.operationId());
@@ -172,29 +139,6 @@ public class LoadingPickService {
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
public Task pick(long taskId, long itemId, PickRequest request, long operatorId, String operatorName) {
ReleaseSelection release = releaseSelection(request.releaseMode(), request.locationIds());
@@ -210,12 +154,11 @@ public class LoadingPickService {
}
return view(requiredTask(taskId), request.warehouseId());
}
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
LoadingPickItemEntity item = repository.item(taskId, itemId, request.warehouseId());
if (item == null) {
throw new BizException(ResultCode.NOT_FOUND, "PDA拣货明细不存在");
}
requireItemAssignee(taskId, itemId, operatorId);
if (repository.pick(itemId, request.pickedPackageCount(), release.mode(), release.json(),
request.version(), request.warehouseId()) != 1) {
throw conflict("拣货数量超过计划箱数或明细版本已变化");
@@ -247,9 +190,8 @@ public class LoadingPickService {
assertReplay(replay, taskId, itemId, requestHash);
return view(requiredTask(taskId), request.warehouseId());
}
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
requireWarehouseItem(taskId, itemId, request.warehouseId());
requireItemAssignee(taskId, itemId, operatorId);
if (repository.partial(itemId, request.finalPickedPackageCount(), release.mode(), release.json(),
request.version(), request.warehouseId()) != 1) {
throw conflict("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
@@ -272,9 +214,8 @@ public class LoadingPickService {
assertReplay(replay, taskId, itemId, requestHash);
return view(requiredTask(taskId), request.warehouseId());
}
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
requireItemAssignee(taskId, itemId, operatorId);
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
}
@@ -402,27 +343,14 @@ public class LoadingPickService {
return task;
}
private void requireItemAssignee(long taskId, long itemId, long operatorId) {
LoadingPickItemAssignmentEntity assignment = repository.itemAssignment(itemId);
if (assignment != null) {
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明细不能再领取或改派");
private LoadingPickTaskEntity requiredAssignedLoadingTaskForUpdate(long taskId, long operatorId) {
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
LoadingPickAssignmentEntity assignment = repository.assignment(taskId);
if (assignment == null) throw conflict("请先领取PDA拣货任务");
if (!Long.valueOf(operatorId).equals(assignment.getAssigneeId())) {
throw conflict("PDA拣货任务已分配给其他作业员");
}
return task;
}
private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) {
@@ -436,15 +364,10 @@ public class LoadingPickService {
private Task view(LoadingPickTaskEntity task, Long warehouseId) {
List<LoadingPickItemEntity> source = warehouseId == null
? repository.items(task.getId()) : repository.items(task.getId(), warehouseId);
List<PickItem> items = source.stream().map(item -> {
LoadingPickItemAssignmentEntity assignedItem = repository.itemAssignment(item.getId());
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(),
List<PickItem> items = source.stream()
.map(item -> new PickItem(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
item.getInboundDetailId(), item.getPlannedPackageCount(), item.getPickedPackageCount(),
item.getItemStatus(), item.getVersion(), itemAssignment);
}).toList();
item.getItemStatus(), item.getVersion())).toList();
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
LoadingPickReviewEntity reviewed = repository.review(task.getId());
Assignment assignment = assigned == null ? null : new Assignment(assigned.getAssigneeId(),
@@ -452,11 +375,9 @@ public class LoadingPickService {
assigned.getAssignmentType(), assigned.getVersion());
Review review = reviewed == null ? null : new Review(reviewed.getReviewStatus(),
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"
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW"
: hasPickedItem ? "IN_PROGRESS" : "PENDING";
: assigned == null ? "UNASSIGNED"
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(),
workflowStatus, task.getVersion(), assignment, review, items);
}