197 lines
14 KiB
Java
197 lines
14 KiB
Java
package com.baiyue.firstleg.service;
|
|
|
|
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
|
import com.baiyue.common.core.api.ResultCode;
|
|
import com.baiyue.common.core.exception.BizException;
|
|
import com.baiyue.firstleg.dto.DropshipPalletizationDtos;
|
|
import com.baiyue.firstleg.dto.DropshipPalletizationDtos.*;
|
|
import com.baiyue.firstleg.entity.InboundOrderEntity;
|
|
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper;
|
|
import com.baiyue.firstleg.mapper.DropshipPalletizationMapper.*;
|
|
import jakarta.annotation.Resource;
|
|
import org.springframework.dao.DuplicateKeyException;
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.util.StringUtils;
|
|
|
|
import java.math.BigDecimal;
|
|
import java.util.*;
|
|
|
|
/** 一件代发测量后的逐箱清点、待绑托和封托,不创建第二套装柜工作托盘。 */
|
|
@Service
|
|
public class DropshipPalletizationService {
|
|
@Resource private DropshipPalletizationMapper mapper;
|
|
|
|
public void createTask(InboundOrderEntity inbound) {
|
|
if (!"ONE_CLICK_DROPSHIP".equals(inbound.getFulfillmentType())) return;
|
|
if (inbound.getTotalPackageCount()==null || inbound.getTotalPackageCount()<1) throw invalid("一件代发测量完成箱数无效,不能下发清点组托任务");
|
|
mapper.insertTask(IdWorker.getId(),inbound.getTenantId(),inbound.getId(),inbound.getWarehouseId(),inbound.getTotalPackageCount());
|
|
}
|
|
|
|
public List<PendingTask> pending(long operatorId,String keyword){ return mapper.pending(operatorId,normalize(keyword)); }
|
|
|
|
@Transactional
|
|
public Task claim(long taskId,long warehouseId,long operatorId,String operatorName){
|
|
if(mapper.claim(taskId,warehouseId,operatorId,operatorName)!=1) throw conflict("任务已被其他人员领取或状态已变化");
|
|
return task(taskId,warehouseId,operatorId);
|
|
}
|
|
|
|
public Task task(long taskId,long warehouseId,long operatorId){
|
|
TaskRow row=requireTask(taskId,warehouseId);
|
|
if(row.getAssigneeId()!=null && row.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
|
|
return view(row);
|
|
}
|
|
|
|
public Carton resolveCarton(long taskId,long warehouseId,String boxCode,long operatorId){
|
|
TaskRow task=requireTask(taskId,warehouseId);
|
|
if(task.getAssigneeId()!=null && task.getAssigneeId()!=operatorId) throw conflict("任务已由其他人员领取");
|
|
LabelRow label=requireLabel(taskId,boxCode);
|
|
CartonRow counted=mapper.countedByLabel(taskId,label.getCartonLabelId());
|
|
if(counted!=null) return cartonView(counted,task.getInboundCode(),false,"箱码已清点");
|
|
String block=null;
|
|
if(!"CLAIMED".equals(task.getTaskStatus())) block="请先领取任务";
|
|
else if(!Objects.equals(task.getAssigneeId(),operatorId)) block="任务不属于当前操作员";
|
|
else if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) block="入仓单已不在待清点组托状态";
|
|
return new Carton(null,label.getBoxCode(),task.getInboundId(),label.getInboundCode(),label.getInboundDetailId(),
|
|
label.getSkuCode(),label.getCargoName(),label.getUnitsPerCarton(),null,null,null,"UNCOUNTED",null,null,
|
|
block==null,block);
|
|
}
|
|
|
|
@Transactional
|
|
public CountResult countCarton(long taskId,CountCartonRequest request,long operatorId,String operatorName){
|
|
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
|
String operation=operation(request.operationId());
|
|
CartonRow replay=mapper.cartonByCountOperation(operation);
|
|
if(replay!=null){
|
|
if(!Objects.equals(replay.getTaskId(),taskId) || !Objects.equals(replay.getBoxCode(),request.boxCode().trim())
|
|
|| !Objects.equals(replay.getActualSkuQuantity(),request.actualSkuQuantity())) throw conflict("清点幂等号已用于不同操作");
|
|
return countResult(task,replay);
|
|
}
|
|
LabelRow label=requireLabel(taskId,request.boxCode());
|
|
if(mapper.countedByLabel(taskId,label.getCartonLabelId())!=null) throw conflict("箱码已清点,不能重复清点");
|
|
int expected=requireExpectedQuantity(label);
|
|
boolean mismatch=expected!=request.actualSkuQuantity();
|
|
String remark=normalize(request.discrepancyRemark());
|
|
if(!mismatch && request.discrepancyAction()!=DiscrepancyAction.MATCH) throw invalid("实收数量一致时差异动作必须为 MATCH");
|
|
if(mismatch && request.discrepancyAction()!=DiscrepancyAction.ACCEPT_VARIANCE) throw conflict("实收数量存在差异,请确认差异处理结果");
|
|
if(mismatch && !StringUtils.hasText(remark)) throw invalid("接受数量差异时必须填写处理说明");
|
|
PalletRow pallet=StringUtils.hasText(request.palletNo())
|
|
? getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName):null;
|
|
long cartonId=IdWorker.getId();
|
|
mapper.insertCarton(cartonId,taskId,pallet==null?null:pallet.getId(),task.getTenantId(),task.getInboundId(),
|
|
label.getInboundDetailId(),label.getCartonLabelId(),label.getSkuCode(),label.getCargoName(),label.getBoxCode(),
|
|
expected,request.actualSkuQuantity(),mismatch?"ACCEPTED_VARIANCE":"MATCH",remark,operation,operatorId,operatorName);
|
|
if(mapper.addCounted(taskId,operatorId,pallet==null?0:1,mismatch?1:0)!=1) throw conflict("任务清点数量已变化,请刷新后重试");
|
|
TaskRow refreshed=requireTask(taskId,request.warehouseId());
|
|
return countResult(refreshed,mapper.carton(taskId,cartonId));
|
|
}
|
|
|
|
@Transactional
|
|
public Task bindPending(long taskId,BindPendingRequest request,long operatorId,String operatorName){
|
|
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
|
String operation=operation(request.operationId());
|
|
if(mapper.bindOperationCount(taskId,operation)>0) return view(requireTask(taskId,request.warehouseId()));
|
|
PalletRow pallet=getOrCreatePallet(task,request.palletNo().trim(),operation,operatorId,operatorName);
|
|
int expected=mapper.unboundCount(taskId);
|
|
if(expected==0) throw conflict("当前没有待绑托箱");
|
|
int bound=mapper.bindPending(taskId,pallet.getId(),operation,operatorId,operatorName);
|
|
if(bound!=expected || mapper.adjustBound(taskId,operatorId,bound)!=1) throw conflict("待绑托箱已变化,请刷新后重试");
|
|
return view(requireTask(taskId,request.warehouseId()));
|
|
}
|
|
|
|
@Transactional
|
|
public Task moveCarton(long taskId,long cartonId,MoveCartonRequest request,long operatorId,String operatorName){
|
|
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
|
CartonRow carton=mapper.carton(taskId,cartonId);
|
|
if(carton==null) throw new BizException(ResultCode.NOT_FOUND,"清点箱不存在");
|
|
if(carton.getPalletId()!=null) requireOpenPallet(taskId,carton.getPalletId(),request.warehouseId());
|
|
PalletRow target=StringUtils.hasText(request.targetPalletNo())
|
|
? getOrCreatePallet(task,request.targetPalletNo().trim(),operation(request.operationId()),operatorId,operatorName):null;
|
|
if(Objects.equals(carton.getPalletId(),target==null?null:target.getId())) return view(task);
|
|
if(mapper.moveCarton(taskId,cartonId,target==null?null:target.getId(),operation(request.operationId()),operatorId,operatorName)!=1)
|
|
throw conflict("清点箱已被其他操作修改,请刷新后重试");
|
|
int delta=carton.getPalletId()==null?1:(target==null?-1:0);
|
|
if(delta!=0 && mapper.adjustBound(taskId,operatorId,delta)!=1) throw conflict("任务绑托数量已变化,请刷新后重试");
|
|
return view(requireTask(taskId,request.warehouseId()));
|
|
}
|
|
|
|
@Transactional
|
|
public Task seal(long taskId,long palletId,SealPalletRequest request,long operatorId,String operatorName){
|
|
TaskRow task=requireWorkingTask(taskId,request.warehouseId(),operatorId);
|
|
PalletRow replay=mapper.palletBySealOperation(operation(request.operationId()));
|
|
if(replay!=null){ if(!Objects.equals(replay.getTaskId(),taskId)||!Objects.equals(replay.getId(),palletId)) throw conflict("封托幂等号已用于其他托盘"); return view(requireTask(taskId,request.warehouseId())); }
|
|
requireOpenPallet(taskId,palletId,request.warehouseId());
|
|
if(mapper.unboundCount(taskId)>0) throw conflict("仍有已清点待绑托箱,不能封托");
|
|
if(mapper.seal(taskId,palletId,request.expectedVersion(),operation(request.operationId()),operatorId,operatorName)!=1)
|
|
throw conflict("空托盘、托盘状态或版本已变化,请刷新后重试");
|
|
TaskRow refreshed=requireTask(taskId,request.warehouseId());
|
|
if(Objects.equals(refreshed.getCountedCartons(),refreshed.getTotalCartons()) &&
|
|
Objects.equals(refreshed.getAllocatedCartons(),refreshed.getTotalCartons()) && mapper.openPalletCount(taskId)==0){
|
|
if(mapper.completeTask(taskId)!=1 || mapper.completeInbound(taskId,refreshed.getTenantId(),refreshed.getInboundId(),operatorId)!=1)
|
|
throw conflict("组托完成状态已变化,请刷新后重试");
|
|
}
|
|
return view(requireTask(taskId,request.warehouseId()));
|
|
}
|
|
|
|
public List<Pallet> inboundPallets(long tenantId,long inboundId){
|
|
String code=mapper.inboundCode(tenantId,inboundId);
|
|
if(code==null) throw new BizException(ResultCode.NOT_FOUND,"入库单不存在");
|
|
return mapper.palletsByInbound(tenantId,inboundId).stream().map(p->palletView(p,code)).toList();
|
|
}
|
|
|
|
private PalletRow getOrCreatePallet(TaskRow task,String palletNo,String operation,long operatorId,String operatorName){
|
|
PalletRow existing=mapper.palletByNo(palletNo);
|
|
if(existing!=null){
|
|
if(!Objects.equals(existing.getTaskId(),task.getId()) || !Objects.equals(existing.getTenantId(),task.getTenantId())) throw conflict("托盘码已被其他客户或入仓任务占用");
|
|
if(!"OPEN".equals(existing.getPalletStatus())) throw conflict("托盘已封托,不能继续绑定箱");
|
|
return existing;
|
|
}
|
|
try{ mapper.insertPallet(IdWorker.getId(),task.getId(),task.getTenantId(),task.getInboundId(),task.getWarehouseId(),palletNo,operation,operatorId,operatorName); }
|
|
catch(DuplicateKeyException e){ existing=mapper.palletByNo(palletNo); if(existing==null) throw conflict("托盘码已被并发占用"); }
|
|
existing=mapper.palletByNo(palletNo);
|
|
if(existing==null || !Objects.equals(existing.getTaskId(),task.getId())) throw conflict("托盘码已被其他任务占用");
|
|
return existing;
|
|
}
|
|
|
|
private CountResult countResult(TaskRow task,CartonRow carton){
|
|
Pallet p=carton.getPalletId()==null?null:palletView(mapper.pallet(task.getId(),carton.getPalletId(),task.getWarehouseId()),task.getInboundCode());
|
|
return new CountResult(cartonView(carton,task.getInboundCode(),false,null),task.getCountedCartons(),task.getAllocatedCartons(),
|
|
task.getCountedCartons()-task.getAllocatedCartons(),task.getTotalCartons()-task.getCountedCartons(),p);
|
|
}
|
|
|
|
private Task view(TaskRow row){
|
|
return new Task(row.getId(),row.getInboundId(),row.getInboundCode(),row.getTenantId(),row.getCustomerName(),row.getWarehouseId(),row.getWarehouseName(),
|
|
row.getTotalCartons(),row.getCountedCartons(),row.getAllocatedCartons(),row.getCountedCartons()-row.getAllocatedCartons(),row.getDiscrepancyCartons(),
|
|
row.getTaskStatus(),row.getAssigneeId(),row.getAssigneeName(),row.getVersion(),mapper.pallets(row.getId()).stream().map(p->palletView(p,row.getInboundCode())).toList(),
|
|
mapper.unboundCartons(row.getId()).stream().map(c->cartonView(c,row.getInboundCode(),false,null)).toList());
|
|
}
|
|
|
|
private Pallet palletView(PalletRow row,String inboundCode){
|
|
List<CartonRow> cartons=mapper.cartons(row.getId()); Map<String,List<CartonRow>> groups=new LinkedHashMap<>();
|
|
for(CartonRow c:cartons) groups.computeIfAbsent(c.getInboundDetailId()+"|"+c.getSkuCode(),ignored->new ArrayList<>()).add(c);
|
|
List<SkuItem> items=groups.values().stream().map(values->{CartonRow first=values.get(0);return new SkuItem(first.getInboundId(),inboundCode,
|
|
first.getInboundDetailId(),first.getSkuCode(),first.getCargoName(),values.size(),values.stream().mapToInt(v->v.getActualSkuQuantity()==null?0:v.getActualSkuQuantity()).sum(),
|
|
values.stream().map(CartonRow::getBoxCode).toList());}).toList();
|
|
int discrepancies=(int)cartons.stream().filter(c->"ACCEPTED_VARIANCE".equals(c.getDiscrepancyStatus())).count();
|
|
PalletMetrics metrics=mapper.metrics(row.getId());
|
|
return new Pallet(row.getId(),row.getPalletNo(),row.getPalletStatus(),cartons.size(),items.size(),discrepancies,
|
|
metrics==null?BigDecimal.ZERO:metrics.weightKg(),metrics==null?BigDecimal.ZERO:metrics.volumeCbm(),row.getVersion(),items,
|
|
cartons.stream().map(c->cartonView(c,inboundCode,false,null)).toList(),row.getCompletedAt());
|
|
}
|
|
|
|
private Carton cartonView(CartonRow row,String inboundCode,boolean canCount,String blockReason){
|
|
return new Carton(row.getId(),row.getBoxCode(),row.getInboundId(),inboundCode,row.getInboundDetailId(),row.getSkuCode(),row.getCargoName(),
|
|
row.getExpectedSkuQuantity(),row.getActualSkuQuantity(),row.getDiscrepancyStatus(),row.getDiscrepancyRemark(),
|
|
row.getPalletId()==null?"COUNTED_UNBOUND":"BOUND",row.getPalletId(),row.getPalletNo(),canCount,blockReason);
|
|
}
|
|
private LabelRow requireLabel(long taskId,String boxCode){ LabelRow label=mapper.label(taskId,boxCode.trim()); if(label==null) throw new BizException(ResultCode.NOT_FOUND,"箱码不属于当前一件代发入仓单"); if(!StringUtils.hasText(label.getSkuCode())) throw conflict("箱码缺少SKU快照,不能清点"); requireExpectedQuantity(label); return label; }
|
|
private int requireExpectedQuantity(LabelRow label){ if(label.getUnitsPerCarton()==null||label.getUnitsPerCarton()<1) throw conflict("箱码缺少每箱SKU数量快照,不能清点"); return label.getUnitsPerCarton(); }
|
|
private TaskRow requireWorkingTask(long taskId,long warehouseId,long operatorId){ TaskRow task=requireTask(taskId,warehouseId); if(!"CLAIMED".equals(task.getTaskStatus())) throw conflict("请先领取待清点组托任务"); if(!Objects.equals(task.getAssigneeId(),operatorId)) throw conflict("任务不属于当前操作员"); if(!"WAIT_PALLETIZE".equals(task.getProcessStatus())) throw conflict("入仓单已不在待清点组托状态"); return task; }
|
|
private TaskRow requireTask(long taskId,long warehouseId){ TaskRow task=mapper.task(taskId,warehouseId); if(task==null) throw new BizException(ResultCode.NOT_FOUND,"未找到当前仓库的一件代发清点组托任务"); return task; }
|
|
private PalletRow requireOpenPallet(long taskId,long palletId,long warehouseId){ PalletRow p=mapper.pallet(taskId,palletId,warehouseId); if(p==null) throw new BizException(ResultCode.NOT_FOUND,"托盘不存在"); if(!"OPEN".equals(p.getPalletStatus())) throw conflict("托盘已封托,不能修改"); return p; }
|
|
private static String operation(String v){String n=normalize(v);if(!StringUtils.hasText(n))throw invalid("操作幂等号不能为空");return n;}
|
|
private static String normalize(String v){return StringUtils.hasText(v)?v.trim():null;}
|
|
private static BizException conflict(String m){return new BizException(ResultCode.CONFLICT,m);}
|
|
private static BizException invalid(String m){return new BizException(ResultCode.VALIDATION_FAILED,m);}
|
|
}
|