装柜
This commit is contained in:
-15
@@ -3,7 +3,6 @@ package com.baiyue.firstleg.controller;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.AdvanceRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
|
||||
@@ -24,7 +23,6 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -70,19 +68,6 @@ public class PlatformLoadingListController {
|
||||
planId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "完成装柜", description = "状态仅允许 LOADING 推进到 COMPLETED,完成后主单和明细冻结。")
|
||||
@PostMapping("/loading-plans/{planId}/complete")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:preload:complete')")
|
||||
public ApiResponse<ConfirmedDetail> complete(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@PathVariable @Positive long planId,
|
||||
@RequestHeader("Idempotency-Key") String idempotencyKey,
|
||||
@Valid @RequestBody AdvanceRequest request) {
|
||||
FirstlegScopeGuard.platform(user);
|
||||
return ApiResponse.success(service.complete(
|
||||
planId, user.userId(), user.username(), idempotencyKey, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询完成装柜清单", description = "只读取 COMPLETED 状态的装柜计划。")
|
||||
@GetMapping("/loading-lists")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:loading-list:list')")
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.firstleg.entity.LoadingPickItemEntity;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
@Mapper
|
||||
@@ -18,4 +19,24 @@ public interface LoadingPickItemMapper extends BaseMapper<LoadingPickItemEntity>
|
||||
AND picked_package_count + #{count} <= planned_package_count
|
||||
""")
|
||||
int pick(@Param("id") long id, @Param("count") int count, @Param("version") int version);
|
||||
|
||||
/**
|
||||
* 以装柜计划的全部有效明细为完成口径,不能只检查当前 PDA 任务中的明细。
|
||||
*/
|
||||
@Select("""
|
||||
SELECT COUNT(*)
|
||||
FROM fl_loading_plan_item plan_item
|
||||
WHERE plan_item.loading_plan_id = #{planId}
|
||||
AND plan_item.deleted = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM fl_loading_pick_item pick_item
|
||||
JOIN fl_loading_pick_task pick_task ON pick_task.id = pick_item.pick_task_id
|
||||
WHERE pick_item.loading_plan_item_id = plan_item.id
|
||||
AND pick_task.loading_plan_id = #{planId}
|
||||
AND pick_task.task_status = 'LOADING'
|
||||
AND pick_item.picked_package_count = plan_item.package_count
|
||||
)
|
||||
""")
|
||||
long unfinishedCount(@Param("planId") long planId);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
o.warehouse_id warehouseId,o.destination_warehouse_id destinationWarehouseId,
|
||||
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0) availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
@@ -15,10 +16,10 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
AND m.id=(SELECT MAX(m2.id) FROM fl_inbound_measurement m2
|
||||
WHERE m2.tenant_id=o.tenant_id AND m2.inbound_id=o.id AND m2.status='APPROVED')
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
LEFT JOIN fl_loading_plan_item l
|
||||
ON l.account_id=o.tenant_id AND l.active_inbound_detail_id=i.inbound_detail_id
|
||||
WHERE o.tenant_id=#{accountId} AND o.process_status='INBOUNDED'
|
||||
AND i.inbound_detail_id IS NOT NULL AND l.id IS NULL
|
||||
AND i.inbound_detail_id IS NOT NULL
|
||||
AND i.package_count>COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0)
|
||||
ORDER BY o.inbounded_at ASC, o.id ASC, i.id ASC
|
||||
""")
|
||||
List<Candidate> candidates(@Param("accountId") long accountId);
|
||||
@@ -29,7 +30,8 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
o.warehouse_id warehouseId,o.destination_warehouse_id destinationWarehouseId,
|
||||
o.destination_code destinationWarehouseCode,o.destination_name destinationWarehouseName,
|
||||
o.customer_name customerName,COALESCE(m.actual_goods_name,'') goodsName,
|
||||
i.package_count availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
i.package_count-COALESCE((SELECT SUM(li.package_count) FROM fl_loading_plan_item li
|
||||
WHERE li.account_id=o.tenant_id AND li.inbound_detail_id=i.inbound_detail_id AND li.deleted=0),0) availablePackageCount,COALESCE(i.unit_weight_kg,0) unitWeightKg,
|
||||
COALESCE(i.unit_volume_cbm,0) unitVolumeCbm
|
||||
FROM fl_inbound_order o
|
||||
JOIN fl_inbound_measurement m ON m.tenant_id=o.tenant_id AND m.inbound_id=o.id
|
||||
@@ -39,7 +41,7 @@ import com.baiyue.firstleg.dto.SmartLoadingDtos.Candidate; import org.apache.iba
|
||||
JOIN fl_inbound_measurement_item i ON i.tenant_id=o.tenant_id AND i.measurement_id=m.id
|
||||
WHERE o.tenant_id=#{accountId} AND o.id=#{inboundId} AND o.process_status='INBOUNDED'
|
||||
AND i.inbound_detail_id=#{detailId}
|
||||
ORDER BY m.id DESC LIMIT 1
|
||||
ORDER BY m.id DESC LIMIT 1 FOR UPDATE
|
||||
""")
|
||||
Candidate candidate(@Param("accountId") long accountId,@Param("inboundId") long inboundId,
|
||||
@Param("detailId") long detailId);
|
||||
|
||||
@@ -67,6 +67,10 @@ public class LoadingPickRepository {
|
||||
return items.pick(itemId, count, version);
|
||||
}
|
||||
|
||||
public boolean allPlanItemsPicked(long planId) {
|
||||
return items.unfinishedCount(planId) == 0;
|
||||
}
|
||||
|
||||
public LoadingPickHistoryEntity history(String operationId) {
|
||||
return histories.selectOne(new LambdaQueryWrapper<LoadingPickHistoryEntity>()
|
||||
.eq(LoadingPickHistoryEntity::getOperationId, operationId));
|
||||
|
||||
@@ -48,8 +48,9 @@ public class SmartLoadingRepository {
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
.eq(LoadingPlanItemEntity::getId, itemId).eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
}
|
||||
public LoadingPlanItemEntity activeItem(long accountId, long detailId) {
|
||||
public LoadingPlanItemEntity activeItem(long planId, long accountId, long detailId) {
|
||||
return items.selectOne(new LambdaQueryWrapper<LoadingPlanItemEntity>()
|
||||
.eq(LoadingPlanItemEntity::getLoadingPlanId, planId)
|
||||
.eq(LoadingPlanItemEntity::getAccountId, accountId)
|
||||
.eq(LoadingPlanItemEntity::getInboundDetailId, detailId)
|
||||
.eq(LoadingPlanItemEntity::getDeleted, false));
|
||||
|
||||
@@ -4,7 +4,6 @@ import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.AdvanceRequest;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedDetail;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedQuery;
|
||||
import com.baiyue.firstleg.dto.LoadingListDtos.ConfirmedSummary;
|
||||
@@ -15,6 +14,7 @@ import com.baiyue.firstleg.dto.LoadingListDtos.TransportUpdateRequest;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.AggregateRow;
|
||||
import com.baiyue.firstleg.mapper.LoadingListQueryMapper.HistoryRow;
|
||||
import com.baiyue.firstleg.repository.LoadingListRepository;
|
||||
import com.baiyue.firstleg.repository.LoadingPickRepository;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
@@ -36,6 +36,8 @@ public class LoadingListService {
|
||||
@Resource
|
||||
private LoadingListRepository repository;
|
||||
@Resource
|
||||
private LoadingPickRepository pickRepository;
|
||||
@Resource
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public PageResult<PreloadSummary> preloadPage(PreloadQuery query) {
|
||||
@@ -101,40 +103,40 @@ public class LoadingListService {
|
||||
return repository.preloadDetail(requirePlan(planId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 最后一箱 PDA 拣货完成后自动调用。服务与数据库触发器均以计划全部有效明细为口径复核。
|
||||
*/
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PRELOAD_COMPLETE", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "完成预装")
|
||||
public ConfirmedDetail complete(long planId, long operatorId, String operatorName,
|
||||
String idempotencyKey, AdvanceRequest request) {
|
||||
advance(planId, operatorId, operatorName, idempotencyKey, request,
|
||||
"LOADING", "COMPLETED");
|
||||
return repository.confirmedDetail(requirePlan(planId));
|
||||
}
|
||||
|
||||
private void advance(long planId, long operatorId, String operatorName, String idempotencyKey,
|
||||
AdvanceRequest request, String fromStatus, String toStatus) {
|
||||
String requestHash = sha256(toStatus + '|' + planId + '|' + request.expectedVersion()
|
||||
+ '|' + trim(request.operationReason()));
|
||||
String operationId = operationId(toStatus, planId, idempotencyKey);
|
||||
HistoryRow replay = repository.history(operationId);
|
||||
if (replay != null) {
|
||||
requireSameRequest(replay, requestHash);
|
||||
@BusinessAudit(event = "PDA_COMPLETE_LOADING", resourceType = "LOADING_PLAN",
|
||||
resourceId = "#planId", description = "PDA拣货完成后自动生成装柜清单")
|
||||
public void finishFromPda(long planId, long pickTaskId, long operatorId, String operatorName) {
|
||||
String operationId = "PDA-COMPLETE-" + planId + '-' + pickTaskId;
|
||||
if (repository.history(operationId) != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pickRepository.allPlanItemsPicked(planId)) {
|
||||
throw conflict("计划仍存在未完成PDA拣货的箱数,不能生成装柜清单");
|
||||
}
|
||||
AggregateRow before = requirePlan(planId);
|
||||
if (!fromStatus.equals(before.getPlanStatus())) {
|
||||
throw conflict("当前状态为 " + before.getPlanStatus() + ",不能推进到 " + toStatus);
|
||||
if ("COMPLETED".equals(before.getPlanStatus())) {
|
||||
return;
|
||||
}
|
||||
if (repository.advance(planId, fromStatus, toStatus, request.expectedVersion(),
|
||||
if (!"LOADING".equals(before.getPlanStatus())) {
|
||||
throw conflict("当前状态为 " + before.getPlanStatus() + ",不能由PDA完成装柜");
|
||||
}
|
||||
if (repository.advance(planId, "LOADING", "COMPLETED", before.getVersion(),
|
||||
operatorId, trim(operatorName)) != 1) {
|
||||
throw conflict("预装计划已被其他操作修改,请刷新后重试");
|
||||
AggregateRow current = requirePlan(planId);
|
||||
if ("COMPLETED".equals(current.getPlanStatus())) {
|
||||
return;
|
||||
}
|
||||
throw conflict("装柜计划已被其他操作修改,请刷新后重试");
|
||||
}
|
||||
repository.appendHistory(planId, operationId, "ADVANCE_PRELOAD_STATUS",
|
||||
fromStatus, toStatus, json(snapshot(before)),
|
||||
json(values("requestHash", requestHash, "status", toStatus,
|
||||
"version", request.expectedVersion() + 1)), operatorId, trim(operatorName),
|
||||
trim(request.operationReason()));
|
||||
repository.appendHistory(planId, operationId, "PDA_COMPLETE_LOADING",
|
||||
"LOADING", "COMPLETED", json(snapshot(before)),
|
||||
json(values("source", "PDA", "pickTaskId", pickTaskId, "status", "COMPLETED",
|
||||
"version", before.getVersion() + 1)), operatorId, trim(operatorName),
|
||||
"全部箱数已完成PDA拣货");
|
||||
}
|
||||
|
||||
private AggregateRow requirePlan(long planId) {
|
||||
|
||||
@@ -26,6 +26,8 @@ import java.util.List;
|
||||
public class LoadingPickService {
|
||||
@Resource
|
||||
private LoadingPickRepository repository;
|
||||
@Resource
|
||||
private LoadingListService loadingListService;
|
||||
|
||||
@Transactional
|
||||
public void begin(long planId, String containerNo, long operatorId, String operatorName,
|
||||
@@ -104,6 +106,9 @@ public class LoadingPickService {
|
||||
history.setOperatorId(operatorId);
|
||||
history.setOperatorName(operatorName);
|
||||
repository.insert(history);
|
||||
if (repository.allPlanItemsPicked(task.getLoadingPlanId())) {
|
||||
loadingListService.finishFromPda(task.getLoadingPlanId(), taskId, operatorId, operatorName);
|
||||
}
|
||||
return view(requiredTask(taskId));
|
||||
}
|
||||
|
||||
|
||||
@@ -91,10 +91,11 @@ public class SmartLoadingService {
|
||||
if (candidate == null) throw conflict("入库货物不存在或尚未完成入库");
|
||||
if (!Objects.equals(candidate.accountId(), request.accountId())) throw forbidden();
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
LoadingPlanItemEntity existing = repository.activeItem(request.accountId(), request.inboundDetailId());
|
||||
if (existing != null && !Objects.equals(existing.getLoadingPlanId(), planId)) throw conflict("货物明细已被有效计划锁定");
|
||||
LoadingPlanItemEntity existing = repository.activeItem(planId, request.accountId(), request.inboundDetailId());
|
||||
int total = request.packageCount() + (existing == null ? 0 : existing.getPackageCount());
|
||||
if (total > candidate.availablePackageCount()) throw validation("累计分配箱数超过可用箱数");
|
||||
int availableIncludingCurrent = candidate.availablePackageCount()
|
||||
+ (existing == null ? 0 : existing.getPackageCount());
|
||||
if (total > availableIncludingCurrent) throw validation("累计分配箱数超过可用箱数");
|
||||
ContainerBookingEntity booking = requireBooking(plan.getContainerBookingId());
|
||||
List<LoadingPlanItemEntity> lines = new ArrayList<>(repository.items(planId));
|
||||
if (existing != null) lines.removeIf(x -> Objects.equals(x.getId(), existing.getId()));
|
||||
@@ -167,7 +168,7 @@ public class SmartLoadingService {
|
||||
if (!Objects.equals(item.getAccountId(), request.accountId())) throw forbidden();
|
||||
if (!Objects.equals(item.getVersion(), request.expectedItemVersion())) throw conflict("装柜明细版本冲突");
|
||||
Candidate candidate = repository.candidate(item.getAccountId(), item.getInboundId(), item.getInboundDetailId());
|
||||
if (candidate == null || request.packageCount() > candidate.availablePackageCount()) throw validation("调整箱数超过可用箱数");
|
||||
if (candidate == null || request.packageCount() > candidate.availablePackageCount() + item.getPackageCount()) throw validation("调整箱数超过可用箱数");
|
||||
OrderCargoClient.CargoBinding cargo = loadCargo(candidate);
|
||||
List<LoadingPlanItemEntity> others = new ArrayList<>(repository.items(planId));
|
||||
others.removeIf(x -> Objects.equals(x.getId(), itemId));
|
||||
|
||||
Reference in New Issue
Block a user