|
|
|
@@ -31,30 +31,33 @@ public class OrderFulfillmentSplitService {
|
|
|
|
|
String hash = hash(req);
|
|
|
|
|
List<OrderFulfillmentSplitEntity> replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
|
|
|
|
|
if (!replay.isEmpty()) {
|
|
|
|
|
if (replay.stream().anyMatch(x -> x.getRequestHash()!=null && !hash.equals(x.getRequestHash())))
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "幂等键对应的请求内容不一致");
|
|
|
|
|
requireMatchingReplay(replay, hash);
|
|
|
|
|
return response(req, splits.lockOrder(req.tenantId(), req.parentOrderId()), replay);
|
|
|
|
|
}
|
|
|
|
|
OrderEntity parent = splits.lockOrder(req.tenantId(), req.parentOrderId());
|
|
|
|
|
replay = splits.findByIdempotency(req.tenantId(), req.parentOrderId(), req.idempotencyKey());
|
|
|
|
|
if (!replay.isEmpty()) return response(req, parent, replay);
|
|
|
|
|
if (!replay.isEmpty()) {
|
|
|
|
|
requireMatchingReplay(replay, hash);
|
|
|
|
|
return response(req, parent, replay);
|
|
|
|
|
}
|
|
|
|
|
if (parent == null) throw new BizException(ResultCode.NOT_FOUND, "父订单不存在或不属于该客户");
|
|
|
|
|
if (!req.parentOrderId().equals(req.sourceOrderId()) && !isDirectChild(req.sourceOrderId(), parent))
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "来源订单必须是父订单或其直接子订单");
|
|
|
|
|
if (!"MASTER".equals(parent.getOrderRole())) throw new BizException(ResultCode.CONFLICT, "子订单不能继续拆分");
|
|
|
|
|
if (parent.getVersion()!=null && !req.expectedParentVersion().equals(parent.getVersion()))
|
|
|
|
|
requireUnconsumedSource(req, parent);
|
|
|
|
|
if (req.expectedParentVersion()!=null && parent.getVersion()!=null
|
|
|
|
|
&& !req.expectedParentVersion().equals(parent.getVersion()))
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "父订单版本已变化,请刷新后重试");
|
|
|
|
|
int parentVersion = parent.getVersion() == null ? 0 : parent.getVersion();
|
|
|
|
|
int next = Optional.ofNullable(splits.maxSplitSequence(req.tenantId(), req.parentOrderId())).orElse(0) + 1;
|
|
|
|
|
List<OrderFulfillmentSplitEntity> rows = new ArrayList<>();
|
|
|
|
|
OrderSplitDtos.Child loaded = createChild(req, parent, next++, "LOADED", req.loadedPackageCount(), hash, operatorId, rows);
|
|
|
|
|
OrderSplitDtos.Child remainder = req.remainingPackageCount() > 0
|
|
|
|
|
? createChild(req, parent, next, "REMAINDER", req.remainingPackageCount(), hash, operatorId, rows) : null;
|
|
|
|
|
for (OrderFulfillmentSplitEntity row : rows) splits.insert(row);
|
|
|
|
|
parent.setVersion((parent.getVersion()==null?0:parent.getVersion()) + 1);
|
|
|
|
|
parent.setVersion(parentVersion + 1);
|
|
|
|
|
LambdaUpdateWrapper<OrderEntity> guard = new LambdaUpdateWrapper<OrderEntity>()
|
|
|
|
|
.eq(OrderEntity::getId, parent.getId())
|
|
|
|
|
.eq(OrderEntity::getTenantId, parent.getTenantId())
|
|
|
|
|
.eq(OrderEntity::getVersion, req.expectedParentVersion());
|
|
|
|
|
.eq(OrderEntity::getVersion, parentVersion);
|
|
|
|
|
int updated = orders.update(parent, guard);
|
|
|
|
|
if (updated != 1) throw new BizException(ResultCode.CONFLICT, "父订单版本已变化,请刷新后重试");
|
|
|
|
|
return new OrderSplitDtos.Response(parent.getId(), parent.getOrderNo(), parent.getVersion(), loaded, remainder,
|
|
|
|
@@ -74,12 +77,28 @@ public class OrderFulfillmentSplitService {
|
|
|
|
|
return new OrderSplitDtos.Tree(parent.getId(), parent.getOrderNo(), parent.getVersion(), result, allocated, Math.max(0, rows.stream().mapToInt(OrderFulfillmentSplitEntity::getSourcePackageCount).findFirst().orElse(0)-allocated));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isDirectChild(long source, OrderEntity parent) { OrderEntity c=orders.selectOne(new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OrderEntity>().eq(OrderEntity::getId,source).eq(OrderEntity::getTenantId,parent.getTenantId())); return c!=null && "CHILD".equals(c.getOrderRole()) && parent.getId().equals(c.getParentOrderId()); }
|
|
|
|
|
private void requireUnconsumedSource(OrderSplitDtos.CreateRequest request, OrderEntity parent) {
|
|
|
|
|
if (!request.parentOrderId().equals(request.sourceOrderId())) {
|
|
|
|
|
OrderFulfillmentSplitEntity source = splits.lockSourceSlice(request.tenantId(),
|
|
|
|
|
request.parentOrderId(), request.sourceOrderId());
|
|
|
|
|
if (source == null || !"REMAINDER".equals(source.getSplitRole())) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "来源订单必须是未消费的余量子订单");
|
|
|
|
|
}
|
|
|
|
|
if (!Objects.equals(source.getRemainingPackageCount(), request.sourcePackageCount())) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "余量子订单箱数与本次来源箱数不一致");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (splits.consumedSourceCount(request.tenantId(), parent.getId(), request.sourceOrderId()) != 0) {
|
|
|
|
|
throw new BizException(ResultCode.CONFLICT, "来源订单已经完成履约拆分,不能重复消费");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
private OrderSplitDtos.Child createChild(OrderSplitDtos.CreateRequest r, OrderEntity p, int seq, String role, int count, String hash, long op, List<OrderFulfillmentSplitEntity> rows) {
|
|
|
|
|
OrderEntity c = new OrderEntity(); c.setId(IdWorker.getId()); c.setTenantId(p.getTenantId()); c.setOrderNo(p.getOrderNo()+"-"+seq); c.setOrderRole("CHILD"); c.setParentOrderId(p.getId()); c.setRootOrderId(p.getId()); c.setSplitSequence(seq); c.setStatus(p.getStatus()); c.setCustomerId(p.getCustomerId()); c.setCustomerName(p.getCustomerName()); c.setTransportMode(p.getTransportMode()); c.setTradeTerm(p.getTradeTerm()); c.setDestinationCountryCode(p.getDestinationCountryCode()); c.setDestinationWarehouseId(p.getDestinationWarehouseId()); c.setDestinationWarehouseName(p.getDestinationWarehouseName()); c.setReceivingWarehouseId(p.getReceivingWarehouseId()); c.setReceivingWarehouseName(p.getReceivingWarehouseName()); c.setChannelCode(p.getChannelCode()); c.setChannelName(p.getChannelName()); c.setDestinationType(p.getDestinationType()); c.setCreatedBy(op); c.setUpdatedBy(op); orders.insert(c);
|
|
|
|
|
OrderFulfillmentSplitEntity row=new OrderFulfillmentSplitEntity(); row.setId(IdWorker.getId()); row.setTenantId(r.tenantId()); row.setParentOrderId(p.getId()); row.setChildOrderId(c.getId()); row.setLoadingPlanId(r.loadingPlanId()); row.setContainerBookingId(r.containerBookingId()); row.setSourcePackageCount(r.sourcePackageCount()); row.setLoadedPackageCount(r.loadedPackageCount()); row.setRemainingPackageCount(r.remainingPackageCount()); row.setOperationId(Optional.ofNullable(r.operationId()).orElse("split-"+r.idempotencyKey())); row.setIdempotencyKey(r.idempotencyKey()); row.setRequestHash(hash); row.setSplitRole(role); row.setExpectedParentVersion(r.expectedParentVersion()); row.setCreatedBy(op); row.setCreatedByName(r.operatorName()); row.setCreatedAt(LocalDateTime.now()); rows.add(row); return new OrderSplitDtos.Child(c.getId(), c.getOrderNo(), seq, role, count);
|
|
|
|
|
OrderFulfillmentSplitEntity row=new OrderFulfillmentSplitEntity(); row.setId(IdWorker.getId()); row.setTenantId(r.tenantId()); row.setParentOrderId(p.getId()); row.setSourceOrderId(r.sourceOrderId()); row.setChildOrderId(c.getId()); row.setLoadingPlanId(r.loadingPlanId()); row.setContainerBookingId(r.containerBookingId()); row.setSourcePackageCount(r.sourcePackageCount()); row.setLoadedPackageCount(r.loadedPackageCount()); row.setRemainingPackageCount(r.remainingPackageCount()); row.setSourceDetailJson(sourceDetails(r)); row.setOperationId(Optional.ofNullable(r.operationId()).orElse("split-"+r.idempotencyKey())); row.setIdempotencyKey(r.idempotencyKey()); row.setRequestHash(hash); row.setSplitRole(role); row.setExpectedParentVersion(Optional.ofNullable(r.expectedParentVersion()).orElse(p.getVersion()==null?0:p.getVersion())); row.setCreatedBy(op); row.setCreatedByName(r.operatorName()); row.setCreatedAt(LocalDateTime.now()); rows.add(row); return new OrderSplitDtos.Child(c.getId(), c.getOrderNo(), seq, role, count, r.sourceOrderId(), r.loadingPlanId(), r.containerBookingId());
|
|
|
|
|
}
|
|
|
|
|
private void validate(OrderSplitDtos.CreateRequest r) { if(r==null||r.tenantId()==null||r.parentOrderId()==null||r.sourceOrderId()==null||r.loadingPlanId()==null||r.containerBookingId()==null||r.sourcePackageCount()==null||r.loadedPackageCount()==null||r.remainingPackageCount()==null||r.expectedParentVersion()==null||r.idempotencyKey()==null||r.idempotencyKey().isBlank()) throw new BizException(ResultCode.BAD_REQUEST,"拆分请求字段不完整"); if(r.loadedPackageCount()<1||r.remainingPackageCount()<0||r.loadedPackageCount()+r.remainingPackageCount()!=r.sourcePackageCount()) throw new BizException(ResultCode.VALIDATION_FAILED,"装柜数量与剩余数量必须守恒"); }
|
|
|
|
|
private void validate(OrderSplitDtos.CreateRequest r) { if(r==null||r.tenantId()==null||r.parentOrderId()==null||r.sourceOrderId()==null||r.loadingPlanId()==null||r.containerBookingId()==null||r.sourcePackageCount()==null||r.loadedPackageCount()==null||r.remainingPackageCount()==null||r.idempotencyKey()==null||r.idempotencyKey().isBlank()||r.lines()==null||r.lines().isEmpty()) throw new BizException(ResultCode.BAD_REQUEST,"拆分请求字段不完整"); if(r.loadedPackageCount()<1||r.remainingPackageCount()<0||r.loadedPackageCount()+r.remainingPackageCount()!=r.sourcePackageCount()) throw new BizException(ResultCode.VALIDATION_FAILED,"装柜数量与剩余数量必须守恒"); if(r.lines().stream().anyMatch(line -> line==null||line.sourceDetailId()==null||line.sourceDetailId()<=0||line.packageCount()==null||line.packageCount()<=0)||r.lines().stream().map(OrderSplitDtos.QuantityLine::sourceDetailId).distinct().count()!=r.lines().size()||r.lines().stream().mapToInt(OrderSplitDtos.QuantityLine::packageCount).sum()!=r.sourcePackageCount()) throw new BizException(ResultCode.VALIDATION_FAILED,"来源明细箱数必须唯一且合计等于来源箱数"); }
|
|
|
|
|
private void requireMatchingReplay(List<OrderFulfillmentSplitEntity> rows, String hash) { if(rows.stream().anyMatch(row -> !Objects.equals(hash,row.getRequestHash()))) throw new BizException(ResultCode.CONFLICT,"幂等键对应的请求内容不一致"); }
|
|
|
|
|
private String sourceDetails(OrderSplitDtos.CreateRequest request) { try { return json.writeValueAsString(request.lines()); } catch(Exception exception) { throw new BizException(ResultCode.INTERNAL_ERROR,"无法保存来源明细快照"); } }
|
|
|
|
|
private String hash(Object o) { try { byte[] b=json.writeValueAsString(o).getBytes(StandardCharsets.UTF_8); byte[] d=MessageDigest.getInstance("SHA-256").digest(b); StringBuilder s=new StringBuilder(); for(byte x:d)s.append(String.format("%02x",x)); return s.toString(); } catch(Exception e){throw new BizException("无法生成请求指纹");} }
|
|
|
|
|
private OrderSplitDtos.Response response(OrderSplitDtos.CreateRequest r, OrderEntity p, List<OrderFulfillmentSplitEntity> rows) { OrderSplitDtos.Child l=null,m=null; for(var x:rows){OrderEntity c=orders.selectById(x.getChildOrderId()); var z=new OrderSplitDtos.Child(x.getChildOrderId(),c==null?null:c.getOrderNo(),c==null?null:c.getSplitSequence(),x.getSplitRole(),"LOADED".equals(x.getSplitRole())?x.getLoadedPackageCount():x.getRemainingPackageCount()); if("LOADED".equals(x.getSplitRole()))l=z;else m=z;} return new OrderSplitDtos.Response(p.getId(),p.getOrderNo(),p.getVersion(),l,m,r.sourcePackageCount(),r.loadedPackageCount(),r.remainingPackageCount(),r.loadingPlanId(),r.containerBookingId(),r.idempotencyKey(),rows.get(0).getCreatedAt()); }
|
|
|
|
|
private OrderSplitDtos.Response response(OrderSplitDtos.CreateRequest r, OrderEntity p, List<OrderFulfillmentSplitEntity> rows) { OrderSplitDtos.Child l=null,m=null; for(var x:rows){OrderEntity c=orders.selectById(x.getChildOrderId()); var z=new OrderSplitDtos.Child(x.getChildOrderId(),c==null?null:c.getOrderNo(),c==null?null:c.getSplitSequence(),x.getSplitRole(),"LOADED".equals(x.getSplitRole())?x.getLoadedPackageCount():x.getRemainingPackageCount(),x.getSourceOrderId(),x.getLoadingPlanId(),x.getContainerBookingId()); if("LOADED".equals(x.getSplitRole()))l=z;else m=z;} return new OrderSplitDtos.Response(p.getId(),p.getOrderNo(),p.getVersion(),l,m,r.sourcePackageCount(),r.loadedPackageCount(),r.remainingPackageCount(),r.loadingPlanId(),r.containerBookingId(),r.idempotencyKey(),rows.get(0).getCreatedAt()); }
|
|
|
|
|
}
|
|
|
|
|