装柜回归测试
This commit is contained in:
@@ -12,9 +12,13 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
"com.baiyue.firstleg.mapper",
|
||||
"com.baiyue.firstleg.split.mapper",
|
||||
"com.baiyue.firstleg.loading.mapper",
|
||||
"com.baiyue.firstleg.oceanbooking.mapper"
|
||||
"com.baiyue.firstleg.loading.bridge.mapper",
|
||||
"com.baiyue.firstleg.oceanbooking.mapper",
|
||||
"com.baiyue.firstleg.outbound.dispatch.mapper",
|
||||
"com.baiyue.firstleg.portschedule.mapper",
|
||||
"com.baiyue.firstleg.customs.mapper"
|
||||
})
|
||||
@EnableFeignClients(basePackages = "com.baiyue.firstleg.client")
|
||||
@EnableFeignClients(basePackages = "com.baiyue.firstleg")
|
||||
@SpringBootApplication(scanBasePackages = "com.baiyue")
|
||||
public class BaiyueFirstlegApplication {
|
||||
public static void main(String[] args) { SpringApplication.run(BaiyueFirstlegApplication.class, args); }
|
||||
|
||||
+22
@@ -62,6 +62,28 @@ 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) {
|
||||
String itemStatus, Integer version, Assignment assignment) {
|
||||
}
|
||||
|
||||
public record Assignment(Long assigneeId, String assigneeName, Long assignedBy,
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
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;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
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,9 +9,11 @@ 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;
|
||||
@@ -33,6 +35,8 @@ public class LoadingPickRepository {
|
||||
@Resource
|
||||
private LoadingPickAssignmentMapper assignments;
|
||||
@Resource
|
||||
private LoadingPickItemAssignmentMapper itemAssignments;
|
||||
@Resource
|
||||
private LoadingPickReviewMapper reviews;
|
||||
@Resource
|
||||
private LoadingPickOperationMapper operations;
|
||||
@@ -165,6 +169,21 @@ 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,6 +14,7 @@ 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;
|
||||
@@ -118,6 +119,38 @@ 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());
|
||||
@@ -139,6 +172,29 @@ 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());
|
||||
@@ -154,11 +210,12 @@ public class LoadingPickService {
|
||||
}
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
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("拣货数量超过计划箱数或明细版本已变化");
|
||||
@@ -190,8 +247,9 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
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("部分拣货箱数必须大于已拣箱数且小于计划箱数,或明细版本已变化");
|
||||
@@ -214,8 +272,9 @@ public class LoadingPickService {
|
||||
assertReplay(replay, taskId, itemId, requestHash);
|
||||
return view(requiredTask(taskId), request.warehouseId());
|
||||
}
|
||||
LoadingPickTaskEntity task = requiredAssignedLoadingTaskForUpdate(taskId, operatorId);
|
||||
LoadingPickTaskEntity task = requiredLoadingTaskForUpdate(taskId);
|
||||
LoadingPickItemEntity item = requireWarehouseItem(taskId, itemId, request.warehouseId());
|
||||
requireItemAssignee(taskId, itemId, operatorId);
|
||||
if (repository.hasPickedOrPartialItem(taskId, item.getInboundId())) {
|
||||
throw conflict("同一入库单已存在已拣或部分拣货明细,不能跳拣并恢复智能装柜候选");
|
||||
}
|
||||
@@ -343,14 +402,27 @@ public class LoadingPickService {
|
||||
return task;
|
||||
}
|
||||
|
||||
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拣货任务已分配给其他作业员");
|
||||
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明细不能再领取或改派");
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private LoadingPickItemEntity requireWarehouseItem(long taskId, long itemId, long warehouseId) {
|
||||
@@ -364,10 +436,15 @@ 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 -> new PickItem(item.getId(), item.getLoadingPlanItemId(), item.getInboundId(),
|
||||
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(),
|
||||
item.getInboundDetailId(), item.getPlannedPackageCount(), item.getPickedPackageCount(),
|
||||
item.getItemStatus(), item.getVersion())).toList();
|
||||
item.getItemStatus(), item.getVersion(), itemAssignment);
|
||||
}).toList();
|
||||
LoadingPickAssignmentEntity assigned = repository.assignment(task.getId());
|
||||
LoadingPickReviewEntity reviewed = repository.review(task.getId());
|
||||
Assignment assignment = assigned == null ? null : new Assignment(assigned.getAssigneeId(),
|
||||
@@ -375,9 +452,11 @@ 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"
|
||||
: assigned == null ? "UNASSIGNED"
|
||||
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW" : "IN_PROGRESS";
|
||||
: repository.allTaskItemsResolved(task.getId()) ? "AWAITING_REVIEW"
|
||||
: hasPickedItem ? "IN_PROGRESS" : "PENDING";
|
||||
return new Task(task.getId(), task.getLoadingPlanId(), task.getContainerNo(), task.getTaskStatus(),
|
||||
workflowStatus, task.getVersion(), assignment, review, items);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user