优化
This commit is contained in:
+8
-5
@@ -132,6 +132,7 @@ public class DeliveryTransferService {
|
||||
@Resource private com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository cancellations;
|
||||
@Resource private com.baiyue.lastleg.delivery.repository.OutboundQuantityRepository outboundQuantities;
|
||||
|
||||
/** 创建一件代发出库任务,使用独立的 DFCK 日流水标识。 */
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask createOneClick(String customerId, String operatorId, DeliveryRequests.OneClickOutbound request) {
|
||||
String requestHash = oneClickRequestHash(request);
|
||||
@@ -145,7 +146,7 @@ public class DeliveryTransferService {
|
||||
String selectedWarehouseId = required(firstPosition.getOverseasWarehouseId(), "库存缺少所属海外仓");
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP", selectedWarehouseId, outbound.getId()));
|
||||
outbound.setOutboundNo(orderNoGenerator.next("ONE_CLICK_DROPSHIP"));
|
||||
outbound.setOutboundType("ONE_CLICK_DROPSHIP"); outbound.setStatus("WAIT_PICK"); outbound.setRecipientInfo(request.recipient().trim());
|
||||
outbound.setPackageCount(request.items().stream().mapToInt(i -> i.packages().intValue()).sum()); outbound.setPalletCount(request.items().size());
|
||||
outbound.setIdempotencyKey(request.idempotencyKey().trim()); outbound.setCreatedBy(operatorId); outbound.setCreatedAt(now); outbound.setUpdatedAt(now);
|
||||
@@ -227,6 +228,7 @@ public class DeliveryTransferService {
|
||||
value(c.getTotalQuantity()), c.getStatus())).toList();
|
||||
}
|
||||
|
||||
/** 校验退货库存后建立出库和派送单,各自分配业务日流水。 */
|
||||
@Transactional
|
||||
public DeliveryResponses.PdaTask createReturnOutbound(String customerId, String operatorId,
|
||||
DeliveryRequests.ReturnOutbound request) {
|
||||
@@ -279,7 +281,7 @@ public class DeliveryTransferService {
|
||||
}
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND", warehouseId, outbound.getId()));
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RETURN_OUTBOUND"));
|
||||
outbound.setOutboundType("RETURN_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo(request.recipient().trim()); outbound.setPackageCount(locked.size()); outbound.setPalletCount("PALLET".equals(request.unitType()) ? 1 : 0);
|
||||
outbound.setOverseasWarehouseId(warehouseId); outbound.setIdempotencyKey(request.idempotencyKey().trim());
|
||||
@@ -296,7 +298,7 @@ public class DeliveryTransferService {
|
||||
carton.setStatus("OUTBOUND_PENDING"); carton.setUpdatedAt(now); returnCartons.updateById(carton);
|
||||
}
|
||||
DeliveryOrder delivery = new DeliveryOrder(); delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", warehouseId, delivery.getId()));
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY"));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(warehouseId); delivery.setDeliveryGroupKey("RETURN:" + request.returnNo().trim());
|
||||
delivery.setStatus("WAIT_DELIVERY"); delivery.setDeliveryAddress(request.recipient().trim());
|
||||
@@ -358,6 +360,7 @@ public class DeliveryTransferService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 为仓储转运建立普通出库和派送单,保留原库存关联。 */
|
||||
@Transactional
|
||||
public DeliveryResponses.OutboundDetail createWarehouseTransferOutbound(String customerId, String operatorId,
|
||||
DeliveryRequests.WarehouseTransferOutbound request) {
|
||||
@@ -397,7 +400,7 @@ public class DeliveryTransferService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(customerId);
|
||||
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND", transfer.getOperatingWarehouseId(), outbound.getId()));
|
||||
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND"));
|
||||
outbound.setOverseasWarehouseId(transfer.getOperatingWarehouseId());
|
||||
outbound.setOutboundType("WAREHOUSE_TRANSFER_OUTBOUND"); outbound.setStatus("WAIT_PICK");
|
||||
outbound.setRecipientInfo("PRIVATE".equals(destinationType)
|
||||
@@ -434,7 +437,7 @@ public class DeliveryTransferService {
|
||||
LastlegOrder tailOrder = tailOrders.selectById(transfer.getLastlegOrderId());
|
||||
DeliveryOrder delivery = new DeliveryOrder();
|
||||
delivery.setId(IdWorker.getId()); delivery.setCustomerId(customerId);
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", transfer.getOperatingWarehouseId(), delivery.getId()));
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY"));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(transfer.getOperatingWarehouseId());
|
||||
delivery.setCustomerName(tailOrder == null ? null : tailOrder.getCustomerName());
|
||||
|
||||
+2
-2
@@ -130,7 +130,7 @@ public class DispatchDeliveryService implements DispatchDeliveryPort {
|
||||
for (var customer:groups.stream().map(Group::customerId).distinct().sorted().toList()) {
|
||||
var cargo=context.cargo().stream().filter(c->customer.equals(c.customerId())).toList();
|
||||
var order=new OutboundOrder();order.setId(IdWorker.getId());order.setCustomerId(customer);
|
||||
order.setOutboundNo(numbers.next("OUTBOUND",context.warehouseId(),order.getId()));
|
||||
order.setOutboundNo(numbers.next("OUTBOUND"));
|
||||
order.setDispatchOrderId(context.dispatchId());order.setDispatchNo(context.dispatchNo());order.setOverseasWarehouseId(context.warehouseId());
|
||||
order.setOutboundType("DISPATCH_DELIVERY");order.setStatus("WAIT_OUTBOUND");order.setLxCodes("");
|
||||
order.setPackageCount(Math.toIntExact(cargo.stream().mapToLong(CargoLine::packages).sum()));
|
||||
@@ -276,7 +276,7 @@ public class DispatchDeliveryService implements DispatchDeliveryPort {
|
||||
/** 客户、目的地和车辆司机仅来自权威事实,请求不能覆盖本趟资源。 */
|
||||
private DeliveryOrder delivery(Context context,Group group,ArrangeGroup input,OutboundOrder outbound,String operator,String key,Instant now) {
|
||||
var d=new DeliveryOrder();d.setId(IdWorker.getId());d.setCustomerId(group.customerId());d.setCustomerName(group.customerName());
|
||||
d.setDeliveryNo(numbers.next("DELIVERY",context.warehouseId(),d.getId()));d.setOutboundOrderId(outbound.getId());d.setOutboundNo(outbound.getOutboundNo());
|
||||
d.setDeliveryNo(numbers.next("DELIVERY"));d.setOutboundOrderId(outbound.getId());d.setOutboundNo(outbound.getOutboundNo());
|
||||
d.setDispatchOrderId(context.dispatchId());d.setDispatchNo(context.dispatchNo());d.setOverseasWarehouseId(context.warehouseId());d.setOverseasWarehouseName(context.warehouseName());
|
||||
var destination=group.destination();d.setDeliveryGroupKey(grouping.documentGroupKey(destination));d.setDestinationType(destination.type());
|
||||
d.setDestinationWarehouseId(destination.warehouseId());d.setDestinationWarehouseCode(destination.warehouseCode());d.setDestinationWarehouseName(destination.warehouseName());
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ public class CargoDiscardOutboundService {
|
||||
LocalDateTime at = LocalDateTime.now();
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
|
||||
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOutboundNo(orderNoGenerator.next("OUTBOUND"));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType(TYPE);
|
||||
outbound.setLxCodes(null); outbound.setRecipientInfo("废弃单 " + application.getDiscardNo());
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(com.baiyue.lastleg.discard.model.CargoDiscardQuantities.boxes(lines, false));
|
||||
|
||||
+1
-1
@@ -222,7 +222,7 @@ public class CargoDiscardService {
|
||||
if (!StringUtils.hasText(warehouseName)) warehouseName = warehouseNames().get(warehouseId);
|
||||
CargoDiscardApplication application = new CargoDiscardApplication();
|
||||
application.setId(IdWorker.getId()); application.setCustomerId(customerId);
|
||||
application.setDiscardNo(orderNoGenerator.next("DISCARD", warehouseId, application.getId()));
|
||||
application.setDiscardNo(orderNoGenerator.next("DISCARD"));
|
||||
application.setOverseasWarehouseId(warehouseId); application.setOverseasWarehouseName(warehouseName);
|
||||
application.setInboundCodes(java.util.stream.Stream.concat(
|
||||
java.util.stream.Stream.concat(orderCache.values().stream().map(LastlegOrder::getInboundCode),
|
||||
|
||||
+3
-2
@@ -301,8 +301,9 @@ public interface DispatchCandidateMapper {
|
||||
"c.pallet_code AS palletNo,COUNT(DISTINCT c.id) AS packageCount,'RELABEL' AS sourceStage,1 AS returnCartonSource," +
|
||||
"GROUP_CONCAT(DISTINCT c.carton_mark ORDER BY c.carton_mark SEPARATOR ',') AS relabelCartonMarks," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS fbaWarehouseCode," +
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.weight) THEN SUM(x.weight) END FROM (SELECT DISTINCT c2.id,c2.weight FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.weight FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS grossWeightKg," +
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(x.volume) THEN SUM(x.volume) END FROM (SELECT DISTINCT c2.id,(c2.length*c2.width*c2.height)/1000000 AS volume FROM ll_return_inventory_carton c2 JOIN ll_relabel_item ri2 ON ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id JOIN ll_relabel_application a2 ON a2.id=ri2.application_id AND a2.customer_id=ri2.customer_id WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND a2.id=a.id) x),(SELECT rr.length*rr.width*rr.height/1000000 FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS volumeCbm," +
|
||||
// 从唯一实箱汇总并用EXISTS核对申请,避免一箱多明细重复计量和跨层相关派生表。
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(c2.weight) THEN SUM(c2.weight) END FROM ll_return_inventory_carton c2 WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND EXISTS (SELECT 1 FROM ll_relabel_item ri2 WHERE ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id AND ri2.application_id=a.id)),(SELECT rr.weight FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS grossWeightKg," +
|
||||
"COALESCE((SELECT CASE WHEN COUNT(*)=COUNT(c2.length*c2.width*c2.height) THEN SUM(c2.length*c2.width*c2.height/1000000) END FROM ll_return_inventory_carton c2 WHERE c2.customer_id=a.customer_id AND c2.pallet_code=c.pallet_code AND c2.status='RELABEL_PENDING' AND EXISTS (SELECT 1 FROM ll_relabel_item ri2 WHERE ri2.return_carton_id=c2.id AND ri2.customer_id=c2.customer_id AND ri2.application_id=a.id)),(SELECT rr.length*rr.width*rr.height/1000000 FROM ll_return_receipt rr WHERE rr.customer_id=a.customer_id AND rr.unit_type='PALLET' AND rr.unit_code=c.pallet_code ORDER BY rr.signed_at DESC LIMIT 1)) AS volumeCbm," +
|
||||
"GROUP_CONCAT(DISTINCT ri.destination_fba_warehouse_code ORDER BY ri.destination_fba_warehouse_code SEPARATOR ',') AS destinationWarehouseCode " +
|
||||
"FROM ll_relabel_application a JOIN ll_relabel_item ri ON ri.application_id=a.id AND ri.customer_id=a.customer_id " +
|
||||
"JOIN ll_return_inventory_carton c ON c.id=ri.return_carton_id AND c.customer_id=a.customer_id " +
|
||||
|
||||
+18
-12
@@ -19,6 +19,7 @@ public interface DispatchCandidatePageMapper {
|
||||
/** 复用既有资格SQL,只统一各来源投影的列序,不复制资格或客户隔离规则。 */
|
||||
final class Sql {
|
||||
private static final String FIELDS="planId,planPalletId,warehousePalletId,orderId,customerId,customerName,planNo,orderType,volumeWeightKg,grossWeightKg,containerNo,appointmentAt,expectedLoadingAt,overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName,orderNo,orderRefs,inboundCode,palletNo,destinationWarehouseId,destinationWarehouseCode,destinationWarehouseName,fbaWarehouseCode,packageCount,volumeCbm,sourceStage,cartonMarks,relabelCartonMarks,destinationType,deliveryAddress,postalCode,contactName,contactPhone,returnReceiptId,returnReceiptNo,returnCartonSource";
|
||||
private static final Set<String> TEXT_FIELDS=Set.of(("customerId,customerName,planNo,orderType,containerNo,overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName,orderNo,orderRefs,inboundCode,palletNo,destinationWarehouseId,destinationWarehouseCode,destinationWarehouseName,fbaWarehouseCode,sourceStage,cartonMarks,relabelCartonMarks,destinationType,deliveryAddress,postalCode,contactName,contactPhone,returnReceiptNo").split(","));
|
||||
private static final Set<String> PLAN_MISSING=Set.of("returnReceiptId","returnReceiptNo","returnCartonSource");
|
||||
private static final Set<String> RETURN_FIELDS=Set.of(("returnReceiptId,returnReceiptNo,orderId,customerId,customerName,orderType,orderNo,orderRefs,inboundCode,warehousePalletId,palletNo,overseasWarehouseId,overseasWarehouseName,overseasWarehouseCode,destinationType,destinationWarehouseCode,fbaWarehouseCode,destinationWarehouseName,deliveryAddress,postalCode,contactName,contactPhone,sourceStage,packageCount").split(","));
|
||||
private static final Set<String> NATIVE_FIELDS=Set.of(("customerId,orderId,overseasWarehouseId,overseasWarehouseName,orderType,orderNo,orderRefs,inboundCode,palletNo,packageCount,sourceStage,returnCartonSource,relabelCartonMarks,fbaWarehouseCode,grossWeightKg,volumeCbm,destinationWarehouseCode").split(","));
|
||||
@@ -27,32 +28,37 @@ public interface DispatchCandidatePageMapper {
|
||||
+"<bind name='fbaWarehouseCode' value='query.destinationWarehouseCode'/><bind name='fba' value='query.destinationWarehouseCode'/>"
|
||||
+"<bind name='sourceStage' value='query.sourceStage'/><bind name='draftToken' value='query.draftToken'/>";
|
||||
/** 来源字段缺省仅表示该来源没有此字段,不伪造业务编号或实托身份。 */
|
||||
private static String project(String body,Set<String> fields) {
|
||||
return "SELECT "+Arrays.stream(FIELDS.split(",")).map(field->fields.contains(field)?"s."+field:
|
||||
("returnCartonSource".equals(field)?"0":"NULL")+" AS "+field).collect(Collectors.joining(","))
|
||||
private static String project(String body,Set<String> fields,boolean count) {
|
||||
return "SELECT "+Arrays.stream((count?"overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName,fbaWarehouseCode":FIELDS).split(",")).map(field->column(field,fields.contains(field)?"s."+field:
|
||||
("returnCartonSource".equals(field)?"0":"NULL"))).collect(Collectors.joining(","))
|
||||
+" FROM ("+body+") s";
|
||||
}
|
||||
/** 在UNION前统一文本列,避免表字段、CAST及聚合结果采用不同collation而报1271;数值与时间保持原类型。 */
|
||||
private static String column(String field,String expression) {
|
||||
return (TEXT_FIELDS.contains(field)?"CONVERT("+expression+" USING utf8mb4) COLLATE utf8mb4_unicode_ci":expression)+" AS "+field;
|
||||
}
|
||||
/** 不同来源用同一组筛选和稳定行标识组合,排序前排除未选来源类型。 */
|
||||
private static String rows() {
|
||||
private static String rows(boolean count) {
|
||||
var planFields=new HashSet<>(Arrays.asList(FIELDS.split(",")));planFields.removeAll(PLAN_MISSING);
|
||||
return "<choose><when test=\"sourceStage != 'RELABEL'\">"+project(DispatchCandidateMapper.AVAILABLE_PLAN_BODY,planFields)
|
||||
+"</when><otherwise>SELECT "+Arrays.stream(FIELDS.split(",")).map(f->"NULL AS "+f).collect(Collectors.joining(","))+" WHERE 1=0</otherwise></choose>"
|
||||
var fields=count?"overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName,fbaWarehouseCode":FIELDS;
|
||||
return "<choose><when test=\"sourceStage != 'RELABEL'\">"+project(DispatchCandidateMapper.AVAILABLE_PLAN_BODY,planFields,count)
|
||||
+"</when><otherwise>SELECT "+Arrays.stream(fields.split(",")).map(f->column(f,"NULL")).collect(Collectors.joining(","))+" WHERE 1=0</otherwise></choose>"
|
||||
+"<if test='planIds == null or planIds.isEmpty()'>"
|
||||
+"<if test=\"sourceStage == null or sourceStage == 'RECEIVED'\"> UNION ALL "+project(ReturnDispatchCandidateMapper.AVAILABLE_BODY,RETURN_FIELDS)+"</if>"
|
||||
+"<if test=\"sourceStage == null or sourceStage == 'RECEIVED'\"> UNION ALL "+project(ReturnDispatchCandidateMapper.AVAILABLE_BODY,RETURN_FIELDS,count)+"</if>"
|
||||
+"<if test=\"sourceStage == null or sourceStage == 'RECEIVED' or sourceStage == 'RELABEL'\"> UNION ALL "
|
||||
+project(DispatchCandidateMapper.RELABEL_BODY,planFields)+" UNION ALL "+project(DispatchCandidateMapper.NATIVE_RELABEL_BODY,NATIVE_FIELDS)+"</if></if>";
|
||||
+project(DispatchCandidateMapper.RELABEL_BODY,planFields,count)+" UNION ALL "+project(DispatchCandidateMapper.NATIVE_RELABEL_BODY,NATIVE_FIELDS,count)+"</if></if>";
|
||||
}
|
||||
/** 仓库页签也是服务端筛选,分页后不能再用本地筛选伪装完整结果。 */
|
||||
private static String filtered() {
|
||||
return " FROM ("+rows()+") candidates WHERE overseasWarehouseId=#{query.warehouseId}"
|
||||
private static String filtered(boolean count) {
|
||||
return " FROM ("+rows(count)+") candidates WHERE overseasWarehouseId=#{query.warehouseId}"
|
||||
+"<if test='query.destinationWarehouseCode != null'> AND fbaWarehouseCode=#{query.destinationWarehouseCode}</if>"
|
||||
+"<if test=\"query.warehouseTab == 'LAX'\"> AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'lax|洛杉矶|los angeles'</if>"
|
||||
+"<if test=\"query.warehouseTab == 'NYC'\"> AND LOWER(CONCAT(COALESCE(overseasWarehouseCode,''),' ',COALESCE(overseasWarehouseName,''))) REGEXP 'nyc|jfk|(^|[^a-z])ny([^a-z]|$)|纽约|new[ -]?york'</if>";
|
||||
}
|
||||
/** 同一事务内统计分页全集。 */
|
||||
public static String count(){return "<script>"+BINDINGS+"SELECT COUNT(*)"+filtered()+"</script>";}
|
||||
public static String count(){return "<script>"+BINDINGS+"SELECT COUNT(*)"+filtered(true)+"</script>";}
|
||||
/** LIMIT发生在任何逐组Java装配之前,最后补来源身份列以保证同名来源不乱序。 */
|
||||
public static String page(){return "<script>"+BINDINGS+"SELECT *"+filtered()
|
||||
public static String page(){return "<script>"+BINDINGS+"SELECT *"+filtered(false)
|
||||
+" ORDER BY customerId,orderNo,palletNo,sourceStage,planPalletId,returnReceiptId,orderRefs,returnCartonSource"
|
||||
+" LIMIT #{query.size} OFFSET #{offset}</script>";}
|
||||
}
|
||||
|
||||
+4
-1
@@ -524,7 +524,10 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var identity=identity(actor,number,Action.LOADING_COMPLETE,request.recordVersion(),request.idempotencyKey(),request);
|
||||
var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get();
|
||||
if(row.getRecordVersion()!=request.recordVersion().longValue() || !Status.ACTIVE.name().equals(row.getStatus()) || !Stage.LOADING.name().equals(row.getStage()) || !ReviewState.APPROVED.name().equals(row.getReviewState()))throw conflict("当前版本或审核阶段不能确认装车");
|
||||
lockExecutionSources(row,repository.cargo(row.getId()));
|
||||
var items=repository.cargo(row.getId());
|
||||
// 未实收/未首拣的PLAN尚未物化实托,先按未核平拒绝,不能将空实托集合传给执行锁。
|
||||
if(items.isEmpty() || items.stream().anyMatch(i->i.getWarehousePalletId()==null))throw conflict("逐项真实拣货尚未核平");
|
||||
lockExecutionSources(row,items);
|
||||
var preview=loading(actor,number);if(!preview.pickingReconciled())throw conflict("逐项真实拣货尚未核平");
|
||||
if(!Objects.equals(preview.manifestHash(),request.manifestHash()))throw conflict("真实装载清单已变化,请重新核对");
|
||||
if(request.measurements()==null || request.measurements().size()!=preview.pallets().size())throw bad("请确认本车全部实托测量");
|
||||
|
||||
+3
-2
@@ -30,7 +30,7 @@ public class DispatchPdaQueryService {
|
||||
require(actor);if(number==null || number.isBlank())throw new BizException(ResultCode.BAD_REQUEST,"请输入排车单号");
|
||||
var rows=repository.headers(actor.boundWarehouseId(),number,null);if(rows.size()!=1)throw new BizException(ResultCode.NOT_FOUND,"当前绑定仓无此排车作业");return assemble(actor,rows).get(0);
|
||||
}
|
||||
/** 固定批量查询数量,不因车数、客户数或托盘数执行逐行详情查询。 */
|
||||
/** 已绑定实托批量读取;尚未绑定的原PLAN仅在本次只读快照内核对自然实收。 */
|
||||
private List<DispatchViews.PdaTask> assemble(Actor actor,List<DispatchAggregate> roots) {
|
||||
if(roots.isEmpty())return List.of();if(roots.size()>200)throw new BizException(ResultCode.BAD_REQUEST,"作业超过完整读取上限,请按阶段筛选或扫描具体排车单号");
|
||||
var ids=roots.stream().map(DispatchAggregate::getId).toList();var cargo=repository.items(ids).stream().collect(Collectors.groupingBy(DispatchItem::getDispatchOrderId));
|
||||
@@ -100,6 +100,7 @@ public class DispatchPdaQueryService {
|
||||
if(rows.size()!=1)return List.of();
|
||||
var actual=rows.get(0);
|
||||
if(!"RECEIVED".equals(actual.sourceStage()) || actual.warehousePalletId()==null
|
||||
|| !Objects.equals(actual.planPalletId(),item.getPlanPalletId())
|
||||
|| !Objects.equals(actual.customerId(),item.getCustomerId())
|
||||
|| !Objects.equals(actual.overseasWarehouseId(),item.getOverseasWarehouseId())
|
||||
|| !Objects.equals(actual.packageCount(),item.getPackageCount())
|
||||
@@ -112,7 +113,7 @@ public class DispatchPdaQueryService {
|
||||
// 同托跨客户、换标及整申请来源必须全部已在原车;不能把新合托货物暴露为原计划可拣库存。
|
||||
var group=sourceRules.selectionGroup(actor.boundWarehouseId(),new DispatchCommands.CargoSelection(
|
||||
SourceType.INBOUND_PLAN,item.getPlanPalletId(),null,null,actual.warehousePalletId(),null));
|
||||
if(!group.complete() || group.sources().isEmpty())return List.of();
|
||||
if(!group.complete() || group.sources().stream().noneMatch(s->Objects.equals(s.sourceKey(),item.getSourceKey())))return List.of();
|
||||
for(var member:group.sources()) {
|
||||
var selected=items.stream().filter(i->Objects.equals(i.getSourceKey(),member.sourceKey())).findFirst().orElse(null);
|
||||
if(selected==null || !Objects.equals(selected.getCustomerId(),member.customerId())
|
||||
|
||||
+2
-1
@@ -418,6 +418,7 @@ public class ExceptionOrderService {
|
||||
tail == null ? inboundOrder.getPackageCount() : tail.getPackageCount(), List.of());
|
||||
}
|
||||
|
||||
/** 保存异常单及来源关系,异常编号使用 EX 日流水。 */
|
||||
private ExceptionOrderResponses.Created insert(ExceptionOrderResponses.Relation relation, DeliveryOrder delivery,
|
||||
String type, String reason, Integer packages, String sku,
|
||||
String direction, String logisticsNo, String sourceType,
|
||||
@@ -428,7 +429,7 @@ public class ExceptionOrderService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
ExceptionOrder value = new ExceptionOrder();
|
||||
value.setId(IdWorker.getId()); value.setCustomerId(relation.customerId());
|
||||
value.setExceptionNo(orderNoGenerator.next("EXCEPTION", relation.overseasWarehouseId(), value.getId()));
|
||||
value.setExceptionNo(orderNoGenerator.next("EXCEPTION"));
|
||||
value.setExceptionType(type); value.setStatus("PENDING"); value.setReason(trim(reason));
|
||||
value.setLastMileOrderId(relation.lastMileOrderId()); value.setLastMileOrderNo(relation.lastMileOrderNo());
|
||||
value.setInboundOrderId(relation.inboundOrderId()); value.setInboundCode(relation.inboundCode());
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class ReturnCartonReturnService {
|
||||
var pallet=pallets.resolve(input.targetPalletNo());
|
||||
var location=warehouse.resolveLocation(Long.parseLong(order.getOverseasWarehouseId()),input.targetLocationCode());
|
||||
if(pallet==null||pallet.palletId()!=input.targetPalletId()||!String.valueOf(pallet.warehouseId()).equals(order.getOverseasWarehouseId())||!Set.of("IDLE","OCCUPIED").contains(pallet.status())||location==null||location.locationId()!=input.targetLocationId()||!Objects.equals(pallet.locationId(),location.locationId())||!Set.of("ACTIVE","IDLE","OCCUPIED").contains(location.status())||"VIRTUAL".equalsIgnoreCase(location.locationType()))throw conflict("目标托盘、库位或海外仓不匹配,不能确认原箱回仓");
|
||||
LocalDateTime now=LocalDateTime.now();String receiptNo=numbers.next("RETURN_RECEIPT",order.getOverseasWarehouseId(),IdWorker.getId());boolean first=true;
|
||||
LocalDateTime now=LocalDateTime.now();String receiptNo=numbers.next("RETURN_RECEIPT");boolean first=true;
|
||||
for(var movement:shipped) {
|
||||
repository.restore(movement);
|
||||
var receipt=new ReturnCartonStockReceipt();receipt.setId(IdWorker.getId());receipt.setReceiptNo(receiptNo);receipt.setCustomerId(order.getCustomerId());
|
||||
|
||||
+3
-3
@@ -22,13 +22,13 @@ import static com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.*;
|
||||
public class FleetPoolOrderBridge {
|
||||
@jakarta.annotation.Resource private FleetLooseboardDemandRepository demands;
|
||||
private final FleetPoolOrderRepository repository;
|
||||
private final FleetPoolOrderNumberGenerator numbers;
|
||||
private final com.baiyue.lastleg.service.LastlegDocumentNumberGenerator numbers;
|
||||
private final ObjectMapper json;
|
||||
private final FleetPoolDeliveryEvidence deliveryEvidence;
|
||||
private final com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker appointmentSources;
|
||||
/** 注入同库商业委托仓储及履约协作服务。 */
|
||||
public FleetPoolOrderBridge(FleetPoolOrderRepository repository,
|
||||
FleetPoolOrderNumberGenerator numbers,ObjectMapper json,FleetPoolDeliveryEvidence deliveryEvidence,
|
||||
com.baiyue.lastleg.service.LastlegDocumentNumberGenerator numbers,ObjectMapper json,FleetPoolDeliveryEvidence deliveryEvidence,
|
||||
com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker appointmentSources) {
|
||||
this.repository=repository;
|
||||
this.numbers=numbers;this.json=json;this.deliveryEvidence=deliveryEvidence;
|
||||
@@ -53,7 +53,7 @@ public class FleetPoolOrderBridge {
|
||||
var now=LocalDateTime.now();var row=new FleetPoolOrderLink();
|
||||
row.setId(IdWorker.getId());row.setBookingId(in.bookingId());row.setBookingNo(in.bookingNo());
|
||||
row.setCustomerId(in.customerId());row.setBusinessType(in.businessType());row.setBusinessOrderId(source==null?IdWorker.getId():source.getId());
|
||||
row.setBusinessOrderNo(source==null?numbers.next("US_TRUCK".equals(in.businessType())?"TRUCK":"LOOSEBOARD",in.destinationLabel(),row.getBusinessOrderId()):source.getOrderNo());
|
||||
row.setBusinessOrderNo(source==null?numbers.next("US_TRUCK".equals(in.businessType())?"TR":"LB"):source.getOrderNo());
|
||||
row.setWarehouseId(in.warehouseId());row.setDestinationKey(in.destinationKey());
|
||||
row.setDeliveryMode(in.deliveryMode());row.setDeliveryInstructions(in.deliveryInstructions());
|
||||
row.setFulfillmentStage("WAITING_INBOUND");row.setStageOccurredAt(now);row.setRecordVersion(0L);
|
||||
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
package com.baiyue.lastleg.fleet.business;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/** Commercial pool orders own LB/TR numbering; no tail-leg order sequence is consumed. */
|
||||
@Component
|
||||
public class FleetPoolOrderNumberGenerator {
|
||||
private static final Pattern LOCATION=Pattern.compile("LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD");
|
||||
public String next(String businessType,String destination,long uniqueId){
|
||||
String prefix=switch(businessType){case "TRUCK"->"TR";case "LOOSEBOARD"->"LB";default->throw new IllegalArgumentException("Unsupported pool business type");};
|
||||
return prefix+"-"+location(destination)+"-"+LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
|
||||
+"-"+String.format(Locale.ROOT,"%06d",Math.floorMod(uniqueId,1_000_000));
|
||||
}
|
||||
private static String location(String value){
|
||||
String code=value==null?"":value.trim().toUpperCase(Locale.ROOT);var match=LOCATION.matcher(code);
|
||||
if(Pattern.matches(".*("+LOCATION.pattern()+").*",code)&&match.find())return match.group();
|
||||
return code.matches("[A-Z0-9]{2,8}")?code:"WH";
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -22,6 +22,8 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class VehicleCareService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final VehicleCareMapper records;
|
||||
private final VehicleCareHistoryMapper histories;
|
||||
private final VehicleRepository vehicles;
|
||||
@@ -41,6 +43,7 @@ public class VehicleCareService {
|
||||
return new PageResult<>(page.getRecords().stream().map(this::response).toList(), page.getCurrent(), page.getSize(), page.getTotal(), page.getPages());
|
||||
}
|
||||
public VehicleCareModels.Response detail(long vehicleId, String number) { return response(required(vehicleId, number, false)); }
|
||||
/** 建立车辆运维记录,首次建单分配 VR 编号。 */
|
||||
@Transactional
|
||||
public VehicleCareModels.Response create(long vehicleId, String operator, VehicleCareModels.Create request) {
|
||||
// Serialize creation per vehicle; no empty-range locks or duplicate creation after a lost response.
|
||||
@@ -53,7 +56,7 @@ public class VehicleCareService {
|
||||
}
|
||||
validate(request.details());
|
||||
var row = new VehicleCareEntity(); row.setId(IdWorker.getId());
|
||||
row.setRecordNo("VR-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase(Locale.ROOT));
|
||||
row.setRecordNo(documentNumbers.next("VR"));
|
||||
row.setVehicleId(vehicleId); row.setVehiclePlate(vehicle.plateNumber()); row.setRecordType(request.details().recordType());
|
||||
row.setOccurredAt(utc(request.details().occurredAt())); row.setDetailsJson(write(request.details())); row.setFilesJson("[]");
|
||||
row.setIdempotencyKey(request.idempotencyKey()); row.setRequestHash(fingerprint); row.setRecordVersion(0L);
|
||||
|
||||
+4
-1
@@ -26,6 +26,8 @@ import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class FleetBookingService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final FleetTripMapper trips;
|
||||
private final FleetBookingMapper bookings;
|
||||
private final FleetBookingReservations reservations;
|
||||
@@ -56,13 +58,14 @@ public class FleetBookingService {
|
||||
var context=quoteContext.resolve(trip(tripNo,false),query.customerId(),query.destinationKey(),query.currency(),query.priceBasis());
|
||||
return quotes.candidates(new FleetQuoteCandidateModels.Query(context.customerId(),context.originCity(),context.destinationCode(),context.currency(),context.effectiveOn(),context.priceBasis(),query.page(),query.size()));
|
||||
}
|
||||
/** 在车次内登记预订,首次请求分配 FB 业务号。 */
|
||||
@Transactional
|
||||
public FleetBookingModels.Result create(String tripNo,String operator,FleetBookingModels.Create request){
|
||||
var trip=trip(tripNo,true);String hash=hash(request);
|
||||
var existing=bookings.created(trip.getId(),operator,request.idempotencyKey());
|
||||
if(existing!=null){if(!hash.equals(existing.getRequestHash()))throw conflict("同一登记请求不能变更客户或货物内容");return result(trip,existing,true);}
|
||||
requireTrip(trip,request.tripRecordVersion());var customer=customers.required(request.customerId());
|
||||
var row=new FleetBookingEntity();row.setId(IdWorker.getId());row.setBookingNo("FB-"+row.getId());row.setTripId(trip.getId());row.setTripNo(tripNo);
|
||||
var row=new FleetBookingEntity();row.setId(IdWorker.getId());row.setBookingNo(documentNumbers.next("FB"));row.setTripId(trip.getId());row.setTripNo(tripNo);
|
||||
row.setCustomerId(customer.id());row.setCustomerCode(customer.code());row.setCustomerName(customer.name());row.setStatus("DRAFT");
|
||||
apply(trip,row,request.cargo());applyQuote(trip,row,request.quoteRef());row.setIdempotencyKey(request.idempotencyKey());row.setRequestHash(hash);row.setRecordVersion(0L);
|
||||
row.setCreatedBy(operator);row.setCreatedAt(now());row.setUpdatedBy(operator);row.setUpdatedAt(row.getCreatedAt());
|
||||
|
||||
+4
-1
@@ -25,6 +25,8 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class FleetCapacityOfferService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.business.FleetLooseboardDemandService demands;
|
||||
public static final String LIST="platform:fleet:capacity-offer:list", MANAGE="platform:fleet:capacity-offer:manage";
|
||||
private final FleetCapacityOfferMapper offers;
|
||||
@@ -131,6 +133,7 @@ public class FleetCapacityOfferService {
|
||||
var row=offer(number,false);requireVisible(row);customers.required(query.customerId());
|
||||
return bookingService.quoteCandidates(row.getTripNo(),query);
|
||||
}
|
||||
/** 发布车队运力并分配 FO 日流水,重放复用原发布记录。 */
|
||||
@Transactional
|
||||
public FleetCapacityOfferModels.OfferResult create(FleetCapacityOfferModels.Write request){
|
||||
requireManage();var trip=trip(request.tripNo(),true);
|
||||
@@ -139,7 +142,7 @@ public class FleetCapacityOfferService {
|
||||
tripVersion(trip,request.tripRecordVersion());
|
||||
if(offers.byTrip(trip.getId())!=null)throw conflict("该车次已有发布,请从既有发布修改");
|
||||
validateCutoff(trip,request.cutoffAt());
|
||||
var row=new FleetCapacityOfferEntity();row.setId(IdWorker.getId());row.setOfferNo("FO-"+row.getId());row.setTripId(trip.getId());row.setTripNo(trip.getTripNo());
|
||||
var row=new FleetCapacityOfferEntity();row.setId(IdWorker.getId());row.setOfferNo(documentNumbers.next("FO"));row.setTripId(trip.getId());row.setTripNo(trip.getTripNo());
|
||||
row.setStatus("DRAFT");row.setRecordVersion(0L);row.setCreatedBy(operator());row.setCreatedAt(now());apply(row,request);
|
||||
if(offers.insert(row)!=1)throw conflict("发布创建失败");
|
||||
var result=new FleetCapacityOfferModels.OfferResult(project(row,trip),false);saveCommand(trip,"CREATE",trip.getTripNo(),request.idempotencyKey(),request,result);return result;
|
||||
|
||||
@@ -27,6 +27,8 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class FleetTripService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired(required=false) private org.springframework.context.ApplicationEventPublisher transportEvents;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges workChanges;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort appointmentAllocations;
|
||||
@@ -114,7 +116,7 @@ public class FleetTripService {
|
||||
var warehouse = warehouses.required(request.warehouseId(), "OVERSEAS");
|
||||
var trip = new FleetTripEntity();
|
||||
trip.setId(IdWorker.getId());
|
||||
trip.setTripNo("TR-" + new java.math.BigInteger(UUID.randomUUID().toString().replace("-", ""), 16).toString(36).toUpperCase(Locale.ROOT));
|
||||
trip.setTripNo(documentNumbers.next("CT"));
|
||||
trip.setIdempotencyKey(request.idempotencyKey()); trip.setRequestHash(request.commandHash());
|
||||
trip.setStatus("PLANNED"); trip.setTripPurpose("DELIVERY"); trip.setWarehouseId(request.warehouseId());
|
||||
trip.setWarehouseCode(warehouse.code()); trip.setWarehouseName(warehouse.name());
|
||||
@@ -390,7 +392,7 @@ public class FleetTripService {
|
||||
var warehouse = warehouses.required(request.warehouseId(), "OVERSEAS");
|
||||
var trip = new FleetTripEntity();
|
||||
trip.setId(IdWorker.getId());
|
||||
trip.setTripNo("TR-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase(Locale.ROOT));
|
||||
trip.setTripNo(documentNumbers.next("CT"));
|
||||
trip.setIdempotencyKey(request.idempotencyKey()); trip.setRequestHash(hash);
|
||||
trip.setTripPurpose(purpose); trip.setStatus("PLANNED"); trip.setWarehouseId(Long.valueOf(warehouse.id()));
|
||||
trip.setWarehouseCode(warehouse.code()); trip.setWarehouseName(warehouse.name());
|
||||
|
||||
+13
-5
@@ -179,15 +179,14 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
.header("X-Internal-Service-Token", internalToken).retrieve()
|
||||
.onStatus(HttpStatusCode::isError, (request, result) -> { throw palletLookupFailure(result.getStatusCode()); })
|
||||
.body(WarehousePalletResponse.class);
|
||||
if (response == null || response.data() == null) throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Warehouse pallet service returned no usable response");
|
||||
requirePalletResponse(response);
|
||||
return new WarehousePalletReference(response.data().id(), response.data().palletCode(), response.data().warehouseId(),
|
||||
response.data().locationId(), response.data().status(), response.data().fbaWarehouseCode(),
|
||||
response.data().unitPrice(), response.data().version());
|
||||
}
|
||||
|
||||
private ResponseStatusException palletLookupFailure(HttpStatusCode status) {
|
||||
return new ResponseStatusException(status.value() == 404 ? HttpStatus.NOT_FOUND
|
||||
return new ResponseStatusException(status.value() == 409 ? HttpStatus.CONFLICT : status.value() == 404 ? HttpStatus.NOT_FOUND
|
||||
: (status.is4xxClientError() ? HttpStatus.BAD_REQUEST : HttpStatus.SERVICE_UNAVAILABLE),
|
||||
status.value() == 404 ? "Warehouse pallet was not found" : "Warehouse pallet lookup failed");
|
||||
}
|
||||
@@ -199,14 +198,23 @@ public class HttpWarehouseResourceGateway implements WarehouseResourceGateway {
|
||||
.retrieve().onStatus(HttpStatusCode::isError, (request, result) -> {
|
||||
throw palletLookupFailure(result.getStatusCode());
|
||||
}).body(WarehousePalletResponse.class);
|
||||
requirePalletResponse(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
/** 托盘状态冲突是业务拒绝,不能把HTTP 200内的A0409误报为仓储服务不可用。 */
|
||||
private void requirePalletResponse(WarehousePalletResponse response) {
|
||||
if (response != null && "A0409".equals(response.code())) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT,
|
||||
response.message() == null || response.message().isBlank() ? "Warehouse pallet state conflicts with this operation" : response.message());
|
||||
}
|
||||
if (response != null && "A0404".equals(response.code())) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Warehouse pallet was not found");
|
||||
}
|
||||
if (response == null || response.data() == null) {
|
||||
if (response == null || !"200".equals(response.code()) || response.data() == null) {
|
||||
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
|
||||
"Warehouse pallet service returned no usable response");
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
private WarehousePalletReference reference(WarehousePalletData pallet) {
|
||||
|
||||
+1
-1
@@ -292,7 +292,7 @@ public class ContainerInboundPlanConversionCoordinator {
|
||||
? plans.existingPlanIdentity(order.customerId(), Long.parseLong(order.orderId())) : null;
|
||||
long planId = existing == null ? IdWorker.getId() : existing.planId();
|
||||
String planNo = existing == null
|
||||
? numbers.next("INBOUND_PLAN", order.destinationWarehouseCode(), planId) : existing.planNo();
|
||||
? numbers.next("INBOUND_PLAN") : existing.planNo();
|
||||
if (existing != null && existing.palletCount() != targetPalletCount) {
|
||||
throw new ResponseStatusException(HttpStatus.CONFLICT, "人工计划托数与柜级预览已不一致");
|
||||
}
|
||||
|
||||
+7
-3
@@ -23,6 +23,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class InboundFbaAppointmentService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
@Resource private InboundFbaAppointmentMapper appointmentMapper;
|
||||
@Resource private InboundFbaAppointmentItemMapper itemMapper;
|
||||
@Resource private InboundFbaAppointmentQueryMapper queryMapper;
|
||||
@@ -76,6 +78,7 @@ public class InboundFbaAppointmentService {
|
||||
new AppointmentCoverageService.SourceRef(source.customerId(),source.sourceKey(),source.destinationKey())).toList(),
|
||||
request.originWarehouseId(),request.fbaWarehouseCode(),request.details()));
|
||||
}
|
||||
/** 校验预约覆盖的来源单据,首次创建时分配 YY 预约号。 */
|
||||
private FbaAppointmentResponse create(String operator,List<AppointmentCoverageService.SourceRef> refs,String expectedOrigin,String expectedDestination,FbaAppointmentDetails details) {
|
||||
var resolved=resolveForWrite(refs);
|
||||
var group=group(resolved);
|
||||
@@ -83,7 +86,7 @@ public class InboundFbaAppointmentService {
|
||||
if(!Objects.equals(group.originWarehouseId(),expectedOrigin) || !Objects.equals(group.destinationKey(),"FBA:"+expectedDestination)) throw conflict("接仓点或目的仓已变化,请重新核对预约货物");
|
||||
var appointment=new InboundFbaAppointment();
|
||||
appointment.setId(IdWorker.getId());
|
||||
appointment.setAppointmentNo("AP-"+LocalDate.now(ZoneOffset.UTC).toString().replace("-","")+"-"+UUID.randomUUID().toString().replace("-","").substring(0,12).toUpperCase(Locale.ROOT));
|
||||
appointment.setAppointmentNo(documentNumbers.next("YY"));
|
||||
appointment.setOriginWarehouseId(group.originWarehouseId());
|
||||
appointment.setFbaWarehouseCode(resolved.get(0).destinationWarehouseCode());
|
||||
appointment.setSelectedCount(resolved.size());appointment.setVersion(0);
|
||||
@@ -261,6 +264,7 @@ public class InboundFbaAppointmentService {
|
||||
appointment.setAppointmentStartUtc(null);appointment.setAppointmentEndUtc(null);
|
||||
}
|
||||
|
||||
/** 重新分组预约来源,新建预约分配 YY 编号并保留覆盖约束。 */
|
||||
@Transactional(isolation=org.springframework.transaction.annotation.Isolation.READ_COMMITTED)
|
||||
public RepartitionFbaAppointmentsResponse repartition(String operator,RepartitionFbaAppointmentsRequest request) {
|
||||
validateRepartition(request);
|
||||
@@ -330,7 +334,7 @@ public class InboundFbaAppointmentService {
|
||||
var rows=batch.allocations().stream().map(a->bySource.get(a.sourceKey())).toList();
|
||||
var quantities=batch.allocations().stream().collect(Collectors.toMap(RepartitionFbaAppointmentsRequest.Allocation::sourceKey,RepartitionFbaAppointmentsRequest.Allocation::packageCount));
|
||||
var row=new InboundFbaAppointment();row.setId(IdWorker.getId());
|
||||
row.setAppointmentNo("AP-"+LocalDate.now(ZoneOffset.UTC).toString().replace("-","")+"-"+UUID.randomUUID().toString().replace("-","").substring(0,12).toUpperCase(Locale.ROOT));
|
||||
row.setAppointmentNo(documentNumbers.next("YY"));
|
||||
row.setOriginWarehouseId(grouped.originWarehouseId());row.setFbaWarehouseCode(rows.get(0).destinationWarehouseCode());row.setSelectedCount(rows.size());row.setVersion(0);
|
||||
row.setCreatedBy(operator);row.setCreatedAt(now());row.setUpdatedAt(row.getCreatedAt());
|
||||
apply(row,new FbaAppointmentDetails(batch.plannedDate(),null,null,null,null,null,"PENDING",batch.remark(),null,null),rows,evidence);
|
||||
@@ -471,7 +475,7 @@ public class InboundFbaAppointmentService {
|
||||
}
|
||||
if(laterAmounts.isEmpty()) throw bad("本次货量没有变化,无需创建变化记录");
|
||||
var laterRows=resolved.stream().filter(s->laterAmounts.containsKey(s.sourceKey())).toList();
|
||||
var later=new InboundFbaAppointment();later.setId(IdWorker.getId());later.setAppointmentNo("AP-"+LocalDate.now(ZoneOffset.UTC).toString().replace("-","")+"-"+UUID.randomUUID().toString().replace("-","").substring(0,12).toUpperCase(Locale.ROOT));
|
||||
var later=new InboundFbaAppointment();later.setId(IdWorker.getId());later.setAppointmentNo(documentNumbers.next("YY"));
|
||||
later.setOriginWarehouseId(original.getOriginWarehouseId());later.setFbaWarehouseCode(original.getFbaWarehouseCode());later.setVersion(0);later.setSelectedCount(laterRows.size());later.setCreatedBy(operator);later.setCreatedAt(now());later.setUpdatedAt(now());
|
||||
apply(later,new FbaAppointmentDetails(intent.laterDate(),null,null,null,null,null,"PENDING",intent.reason(),null,null),laterRows,evidence);
|
||||
appointmentMapper.insert(later);writeItems(later,laterRows,List.of(),evidence,laterAmounts);
|
||||
|
||||
+2
-1
@@ -456,6 +456,7 @@ public class InboundPlanService {
|
||||
receivingWarehouse, plannedArrivalDate, null, Map.of());
|
||||
}
|
||||
|
||||
/** 创建入仓计划,复用预分配编号或首次分配 IP 日流水。 */
|
||||
private InboundPlanResponse createPlan(String customerId, Long selectedOrderId,
|
||||
List<SourceOrderSnapshot> orders, List<String> sourcePalletNos,
|
||||
List<WholePalletInput> wholePallets, ManualInboundPlanRequest manual,
|
||||
@@ -468,7 +469,7 @@ public class InboundPlanService {
|
||||
InboundPlan plan = new InboundPlan();
|
||||
plan.setId(preparedPlan == null ? IdWorker.getId() : preparedPlan.planId());
|
||||
plan.setPlanNo(preparedPlan == null
|
||||
? orderNoGenerator.next("INBOUND_PLAN", orders.get(0).destinationWarehouseCode(), plan.getId())
|
||||
? orderNoGenerator.next("INBOUND_PLAN")
|
||||
: preparedPlan.planNo());
|
||||
plan.setCustomerId(customerId);
|
||||
plan.setSourceOrderId(selectedOrderId);
|
||||
|
||||
+2
-2
@@ -63,7 +63,7 @@ public class LocalLooseboardService {
|
||||
LocalLooseboardOrder order = new LocalLooseboardOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD", request.destination(), order.getId()));
|
||||
order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD"));
|
||||
apply(order, request.origin(),
|
||||
request.originAddress(), request.destination(), request.destinationAddress(), request.productName(), request.estimatedPalletCount(),
|
||||
request.packageCount(), request.totalWeightKg(), request.totalVolumeCbm(), request.budgetAmount(),
|
||||
@@ -276,7 +276,7 @@ public class LocalLooseboardService {
|
||||
private LastlegOrder createLastlegOrder(LocalLooseboardOrder source, String requestHash, LocalDateTime now) {
|
||||
LastlegOrder order = new LastlegOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD", source.getDestination(), order.getId()));
|
||||
order.setOrderNo(lastlegOrderNoGenerator.next("LOOSEBOARD"));
|
||||
order.setCustomerId(source.getCustomerId());
|
||||
order.setOrderType("LOCAL_LOOSEBOARD");
|
||||
order.setInboundCode(null);
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Mapper
|
||||
public interface LastlegOrderNoDailySequenceMapper {
|
||||
@Insert("INSERT INTO ll_order_no_daily_sequence(business_date,last_sequence) " +
|
||||
"VALUES(#{businessDate},LAST_INSERT_ID(1)) " +
|
||||
"ON DUPLICATE KEY UPDATE last_sequence=LAST_INSERT_ID(last_sequence+1)")
|
||||
int nextValue(@Param("businessDate") LocalDate businessDate);
|
||||
|
||||
@Select("SELECT LAST_INSERT_ID()")
|
||||
long currentValue();
|
||||
}
|
||||
+1
@@ -33,6 +33,7 @@ public class OverseasProcurementFee {
|
||||
private Long sourceFeeId;
|
||||
private Long parentFeeId;
|
||||
private String requestHash;
|
||||
private String idempotencyKey;
|
||||
private String feeItemCode;
|
||||
private String feeItemName;
|
||||
private BigDecimal amount;
|
||||
|
||||
+3
@@ -3,6 +3,9 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementFee;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@Mapper public interface OverseasProcurementFeeMapper extends BaseMapper<OverseasProcurementFee> {
|
||||
/** 调用方持有订单锁时,读取当前退款版本上界。 */
|
||||
@org.apache.ibatis.annotations.Select("SELECT COALESCE(MAX(fee_version),0) FROM ll_overseas_procurement_fee WHERE order_id=#{orderId} AND purpose='REFUND'")
|
||||
int maxRefundVersion(@org.apache.ibatis.annotations.Param("orderId") long orderId);
|
||||
@org.apache.ibatis.annotations.Select("SELECT * FROM ll_overseas_procurement_fee WHERE id=#{id} AND order_id=#{orderId} AND customer_id=#{customerId} FOR UPDATE")
|
||||
OverseasProcurementFee lock(@org.apache.ibatis.annotations.Param("id") long id,
|
||||
@org.apache.ibatis.annotations.Param("orderId") long orderId,
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.baiyue.lastleg.procurement.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.lastleg.procurement.entity.OverseasProcurementFee;
|
||||
import com.baiyue.lastleg.procurement.mapper.OverseasProcurementFeeMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/** 采购退款的请求身份独立于展示单号,跨日重试仍定位原费用。 */
|
||||
@Repository
|
||||
public class ProcurementFeeNumberRepository {
|
||||
private final OverseasProcurementFeeMapper mapper;
|
||||
|
||||
/** 使用采购费用所属服务的 Mapper。 */
|
||||
public ProcurementFeeNumberRepository(OverseasProcurementFeeMapper mapper) { this.mapper = mapper; }
|
||||
|
||||
/** 上层已持有订单锁,同客户同订单的退款请求只对应一个费用。 */
|
||||
public OverseasProcurementFee findRefund(String customerId, long orderId, String key) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<OverseasProcurementFee>()
|
||||
.eq(OverseasProcurementFee::getCustomerId, customerId)
|
||||
.eq(OverseasProcurementFee::getOrderId, orderId)
|
||||
.eq(OverseasProcurementFee::getPurpose, "REFUND")
|
||||
.eq(OverseasProcurementFee::getIdempotencyKey, key));
|
||||
}
|
||||
|
||||
/** 版本沿用同订单退款序列,业务编号不承担费用版本含义。 */
|
||||
public int nextRefundVersion(long orderId) { return mapper.maxRefundVersion(orderId) + 1; }
|
||||
}
|
||||
+3
-2
@@ -43,7 +43,7 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Service
|
||||
public class OverseasProcurementService {
|
||||
@Resource private ProcurementOrderNumberGenerator orderNoGenerator;
|
||||
@Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator orderNoGenerator;
|
||||
public static final String REVIEW_PERMISSION = OverseasPermissions.PURCHASE_REVIEW;
|
||||
public static final String MANAGE_PERMISSION = OverseasPermissions.PURCHASE_EDIT;
|
||||
public static final String PENDING_REVIEW = "PENDING_REVIEW";
|
||||
@@ -61,6 +61,7 @@ public class OverseasProcurementService {
|
||||
@Resource private OverseasProcurementFeeMapper procurementFees;
|
||||
@Resource private ProcurementFileGateway fileGateway;
|
||||
|
||||
/** 创建客户采购订单,首次请求分配 CG 日流水。 */
|
||||
@Transactional
|
||||
public OverseasProcurementResponses.Detail create(String customerId, String operatorId,
|
||||
OverseasProcurementRequests.Create request) {
|
||||
@@ -74,7 +75,7 @@ public class OverseasProcurementService {
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setCustomerName(trim(request.customerName()));
|
||||
order.setOrderNo(orderNoGenerator.next(customerId, request.customerName()));
|
||||
order.setOrderNo(orderNoGenerator.next("CG"));
|
||||
order.setPurchaseCategory(request.purchaseCategory() == null ? "" : request.purchaseCategory().trim());
|
||||
order.setProductName(request.productName().trim());
|
||||
order.setProductSpecification(trim(request.productSpecification()));
|
||||
|
||||
+6
-2
@@ -16,6 +16,8 @@ import static com.baiyue.lastleg.procurement.service.ProcurementLifecycleService
|
||||
|
||||
@Service @RequiredArgsConstructor
|
||||
public class ProcurementAdjustmentService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final ProcurementLifecycleService lifecycle;
|
||||
private final ProcurementFinanceService finance;
|
||||
private final JdbcTemplate jdbc;
|
||||
@@ -23,6 +25,7 @@ public class ProcurementAdjustmentService {
|
||||
BigDecimal exchangeRate,String exchangeRateMonth,BigDecimal targetAmount,BigDecimal previousNetAmount,
|
||||
BigDecimal differenceAmount,List<String> feeNos) { }
|
||||
private record Identity(long rootFeeId,Correct request) { }
|
||||
/** 更正原采购费用并以 PG 日流水标识本次更正记录。 */
|
||||
@Transactional
|
||||
public Result correct(String customer,String orderNo,String operator,long rootFeeId,Correct request) {
|
||||
var order=lifecycle.lock(customer,orderNo);var identity=new Identity(rootFeeId,request);
|
||||
@@ -37,7 +40,7 @@ public class ProcurementAdjustmentService {
|
||||
.subtract(chain.stream().filter(f->"REFUNDED".equals(f.getWalletStatus())).map(OverseasProcurementFee::getAmount).reduce(BigDecimal.ZERO,BigDecimal::add));
|
||||
BigDecimal target=request.targetOriginalAmount().multiply(root.getExchangeRate()).setScale(2,RoundingMode.HALF_UP),delta=target.subtract(net);
|
||||
if(target.precision()-target.scale()>18)throw bad("折算人民币金额超出支持范围");
|
||||
long id=IdWorker.getId();String no="PC-"+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase();
|
||||
long id=IdWorker.getId();String no=documentNumbers.next("PG");
|
||||
// The node command carries the exact request hash; this table stores the computed immutable monetary facts.
|
||||
jdbc.update("INSERT INTO ll_procurement_fee_correction(id,order_id,customer_id,correction_no,root_fee_id,idempotency_key,request_hash,target_original_amount,original_currency,exchange_rate,exchange_rate_month,target_amount,previous_net_amount,difference_amount,voucher_file_id,reason,created_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
id,order.getId(),customer,no,rootFeeId,request.idempotencyKey(),lifecycle.requestHash(identity),request.targetOriginalAmount(),root.getOriginalCurrency(),root.getExchangeRate(),root.getExchangeRateMonth(),target,net,delta,request.voucherFileId(),request.reason(),operator);
|
||||
@@ -47,6 +50,7 @@ public class ProcurementAdjustmentService {
|
||||
lifecycle.complete(order,operator,"CORRECT_FEE_TARGET",request.idempotencyKey(),request.reason(),identity);
|
||||
return result(order.getId(),request.idempotencyKey());
|
||||
}
|
||||
/** 替换失败费用,保留原费用链并分配新的 PG 更正记录号。 */
|
||||
@Transactional
|
||||
public Result replaceFailed(String customer,String orderNo,String operator,long originalFeeId,Correct request) {
|
||||
var order=lifecycle.lock(customer,orderNo);var identity=new Identity(originalFeeId,request);
|
||||
@@ -57,7 +61,7 @@ public class ProcurementAdjustmentService {
|
||||
BigDecimal target=request.targetOriginalAmount().multiply(original.getExchangeRate()).setScale(2,RoundingMode.HALF_UP);
|
||||
if("SAMPLE_PAYMENT".equals(original.getPurpose()))lifecycle.requireReplacementBudget(order,request.targetOriginalAmount(),original.getOriginalCurrency(),original.getBusinessDate(),target);
|
||||
if(target.precision()-target.scale()>18)throw bad("折算人民币金额超出支持范围");
|
||||
long id=IdWorker.getId();String no="PC-"+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase();
|
||||
long id=IdWorker.getId();String no=documentNumbers.next("PG");
|
||||
jdbc.update("INSERT INTO ll_procurement_fee_correction(id,order_id,customer_id,correction_no,root_fee_id,idempotency_key,request_hash,target_original_amount,original_currency,exchange_rate,exchange_rate_month,target_amount,previous_net_amount,difference_amount,voucher_file_id,reason,created_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
id,order.getId(),customer,no,originalFeeId,request.idempotencyKey(),lifecycle.requestHash(identity),request.targetOriginalAmount(),original.getOriginalCurrency(),original.getExchangeRate(),original.getExchangeRateMonth(),target,BigDecimal.ZERO,target,request.voucherFileId(),request.reason(),operator);
|
||||
var fee=finance.replaceFailed(customer,orderNo,operator,originalFeeId,request.targetOriginalAmount(),request.voucherFileId(),request.reason());
|
||||
|
||||
+4
-1
@@ -13,6 +13,8 @@ import static com.baiyue.lastleg.procurement.service.ProcurementLifecycleService
|
||||
|
||||
@Service @RequiredArgsConstructor
|
||||
public class ProcurementControlService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final ProcurementLifecycleService lifecycle;
|
||||
private final ProcurementFinanceService finance;
|
||||
private final ProcurementWarehouseDirectory warehouses;
|
||||
@@ -141,5 +143,6 @@ public class ProcurementControlService {
|
||||
return result;
|
||||
}
|
||||
private String serialize(Object value){if(value==null)return null;try{return json.writeValueAsString(value);}catch(Exception e){throw new IllegalStateException(e);}}
|
||||
private static String number(String prefix){return prefix+'-'+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase();}
|
||||
/** 说明和关闭记录使用各自独立的日流水。 */
|
||||
private String number(String prefix){return documentNumbers.next(prefix);}
|
||||
}
|
||||
|
||||
+10
-8
@@ -38,6 +38,9 @@ import java.util.UUID;
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProcurementFinanceService implements WalletResultHandler {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.procurement.repository.ProcurementFeeNumberRepository feeNumbers;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final OverseasProcurementFeeMapper fees;
|
||||
private final OverseasProcurementFileMapper files;
|
||||
private final OverseasProcurementOrderMapper orders;
|
||||
@@ -118,7 +121,7 @@ public class ProcurementFinanceService implements WalletResultHandler {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
OverseasProcurementFee fee = new OverseasProcurementFee();
|
||||
fee.setId(IdWorker.getId()); fee.setOrderId(order.getId()); fee.setCustomerId(order.getCustomerId());
|
||||
fee.setFeeNo("PF-" + UUID.randomUUID().toString().replace("-", "").substring(0, 24).toUpperCase());
|
||||
fee.setFeeNo(documentNumbers.next("PF"));
|
||||
fee.setPurpose(request.purpose().name()); fee.setConfirmationOrigin(origin); fee.setFeeVersion(maxVersion == null ? 1 : maxVersion + 1);
|
||||
fee.setActive(true); fee.setDetermination(request.determination()); fee.setFeeItemCode(code);
|
||||
fee.setFeeItemName(request.purpose() == ProcurementFeePurpose.OTHER ? request.feeItemName().trim() : request.purpose().label());
|
||||
@@ -205,6 +208,7 @@ public class ProcurementFinanceService implements WalletResultHandler {
|
||||
var order = lockOrder(customerId, orderNo);
|
||||
return refundLocked(order,operatorId,originalFeeId,amount,voucherFileId,reason,idempotencyKey,true);
|
||||
}
|
||||
/** 订单锁内先恢复同幂等键退款,首次请求校验后才分配独立 PF 业务号。 */
|
||||
private OverseasProcurementFee refundLocked(OverseasProcurementOrder order,String operatorId,long originalFeeId,
|
||||
BigDecimal amount,long voucherFileId,String reason,String idempotencyKey,boolean checkChain) {
|
||||
String customerId=order.getCustomerId(),orderNo=order.getOrderNo();
|
||||
@@ -212,11 +216,8 @@ public class ProcurementFinanceService implements WalletResultHandler {
|
||||
var request = new Confirmation(ProcurementFeePurpose.REFUND, "REFUND", "退款", "POSITIVE", amount,
|
||||
"CNY", LocalDate.now(), voucherFileId, reason);
|
||||
validate(request);
|
||||
// Identity is independent of wall-clock time, so retrying after midnight returns the same refund.
|
||||
String number = "RF-" + UUID.nameUUIDFromBytes((customerId + ':' + orderNo + ':' + idempotencyKey)
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8)).toString().replace("-", "").toUpperCase();
|
||||
String requestHash = hash(List.of(originalFeeId, amount, voucherFileId, reason));
|
||||
var existing = fees.selectOne(new LambdaQueryWrapper<OverseasProcurementFee>().eq(OverseasProcurementFee::getFeeNo, number));
|
||||
var existing = feeNumbers.findRefund(customerId, order.getId(), idempotencyKey);
|
||||
if (existing != null) {
|
||||
if (!requestHash.equals(existing.getRequestHash())) throw conflict("退款幂等键对应不同原扣款或金额");
|
||||
return existing;
|
||||
@@ -237,10 +238,11 @@ public class ProcurementFinanceService implements WalletResultHandler {
|
||||
if (already.add(amount).compareTo(original.getAmount()) > 0) throw bad("退款超过原扣款可退余额");
|
||||
var voucher = voucher(order, request, true);
|
||||
var fee = new OverseasProcurementFee();
|
||||
String number = documentNumbers.next("PF");
|
||||
fee.setIdempotencyKey(idempotencyKey);
|
||||
fee.setId(IdWorker.getId()); fee.setOrderId(order.getId()); fee.setCustomerId(customerId); fee.setFeeNo(number);
|
||||
fee.setConfirmationOrigin("FINANCIAL");fee.setPurpose("REFUND"); fee.setFeeItemCode("REFUND"); fee.setFeeItemName("关联费用退款");
|
||||
Integer last = jdbc.queryForObject("SELECT COALESCE(MAX(fee_version),0) FROM ll_overseas_procurement_fee WHERE order_id=? AND purpose='REFUND'", Integer.class, order.getId());
|
||||
fee.setFeeVersion(last == null ? 1 : last + 1); fee.setActive(true); fee.setDetermination("POSITIVE");
|
||||
fee.setFeeVersion(feeNumbers.nextRefundVersion(order.getId())); fee.setActive(true); fee.setDetermination("POSITIVE");
|
||||
fee.setAmount(amount); fee.setCurrency("CNY"); fee.setOriginalAmount(amount); fee.setOriginalCurrency("CNY");
|
||||
fee.setExchangeRate(BigDecimal.ONE); fee.setExchangeRateMonth(request.businessDate().toString().substring(0, 7));
|
||||
fee.setBusinessDate(request.businessDate()); fee.setParentFeeId(originalFeeId); fee.setWalletBusinessId(fee.getId());
|
||||
@@ -272,7 +274,7 @@ public class ProcurementFinanceService implements WalletResultHandler {
|
||||
requireSourceCapacity(delta,delta);
|
||||
var proof=voucher(order,new Confirmation(ProcurementFeePurpose.ADJUSTMENT,"ADJUSTMENT","追加费用","POSITIVE",delta,"CNY",root.getBusinessDate(),voucherId,reason),true);
|
||||
var fee=new OverseasProcurementFee();fee.setId(IdWorker.getId());fee.setOrderId(order.getId());fee.setCustomerId(customer);
|
||||
fee.setFeeNo("PF-"+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase());fee.setConfirmationOrigin("FINANCIAL");fee.setPurpose("ADJUSTMENT");
|
||||
fee.setFeeNo(documentNumbers.next("PF"));fee.setConfirmationOrigin("FINANCIAL");fee.setPurpose("ADJUSTMENT");
|
||||
fee.setFeeItemCode("ADJ_"+root.getId());fee.setFeeItemName("原费用正额调整");
|
||||
fee.setFeeVersion(jdbc.queryForObject("SELECT COALESCE(MAX(fee_version),0)+1 FROM ll_overseas_procurement_fee WHERE order_id=? AND purpose='ADJUSTMENT' AND fee_item_code=?",Integer.class,order.getId(),fee.getFeeItemCode()));
|
||||
fee.setActive(true);fee.setDetermination("POSITIVE");fee.setAmount(delta);fee.setCurrency("CNY");fee.setOriginalCurrency("CNY");fee.setOriginalAmount(delta);
|
||||
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
package com.baiyue.lastleg.procurement.service;
|
||||
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
@Component
|
||||
public class ProcurementOrderNumberGenerator {
|
||||
private final JdbcTemplate jdbc;
|
||||
public ProcurementOrderNumberGenerator(JdbcTemplate jdbc) { this.jdbc = jdbc; }
|
||||
|
||||
@Transactional
|
||||
public String next(String customerId, String customerName) {
|
||||
LocalDate date = LocalDate.now();
|
||||
jdbc.update("INSERT INTO ll_procurement_daily_sequence(customer_id,business_date,sequence_value) VALUES(?,?,LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE sequence_value=LAST_INSERT_ID(sequence_value+1)",
|
||||
customerId, date);
|
||||
Integer value = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Integer.class);
|
||||
if (value == null || value < 1 || value > 9999) throw new IllegalStateException("客户采购订单当日序号已耗尽");
|
||||
return initials(customerName) + date.format(DateTimeFormatter.BASIC_ISO_DATE) + String.format("%04d", value);
|
||||
}
|
||||
|
||||
static String initials(String customerName) {
|
||||
if (!StringUtils.hasText(customerName)) return "KH";
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean latinWord = false;
|
||||
for (char value : customerName.trim().toCharArray()) {
|
||||
if (result.length() >= 8) break;
|
||||
if (Character.UnicodeScript.of(value) == Character.UnicodeScript.HAN) {
|
||||
String[] pinyin = PinyinHelper.toHanyuPinyinStringArray(value);
|
||||
if (pinyin != null && pinyin.length > 0 && !pinyin[0].isBlank()) result.append(Character.toUpperCase(pinyin[0].charAt(0)));
|
||||
latinWord = false;
|
||||
} else if (Character.isLetterOrDigit(value)) {
|
||||
if (!latinWord) result.append(Character.toUpperCase(value));
|
||||
latinWord = true;
|
||||
} else {
|
||||
latinWord = false;
|
||||
}
|
||||
}
|
||||
String normalized = result.toString().replaceAll("[^A-Z0-9]", "").toUpperCase(Locale.ROOT);
|
||||
return normalized.isBlank() ? "KH" : normalized.substring(0, Math.min(normalized.length(), 8));
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -19,6 +19,8 @@ import static com.baiyue.lastleg.procurement.service.ProcurementLifecycleService
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProcurementPhysicalService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final ProcurementLifecycleService lifecycle;
|
||||
private final ProcurementWarehouseDirectory warehouses;
|
||||
private final ProcurementFinanceService finance;
|
||||
@@ -327,5 +329,6 @@ public class ProcurementPhysicalService {
|
||||
}
|
||||
private static void measure(BigDecimal value,int scale){if(value==null || value.signum()<=0 || value.scale()>scale)throw bad("箱规须为正数且最多两位小数,毛重须为正数且最多三位小数");}
|
||||
private static long add(long a,long b){try{return Math.addExact(a,b);}catch(ArithmeticException e){throw bad("数量超过可处理范围");}}
|
||||
private static String number(String prefix){return prefix+'-'+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase();}
|
||||
/** 实收记录使用独立 PR 日流水。 */
|
||||
private String number(String prefix){return documentNumbers.next(prefix);}
|
||||
}
|
||||
|
||||
+4
-1
@@ -19,6 +19,8 @@ import static com.baiyue.lastleg.procurement.service.ProcurementLifecycleService
|
||||
/** Every shipment is a single immutable consumption of this order's entire eligible batch. */
|
||||
@Service @RequiredArgsConstructor
|
||||
public class ProcurementTransportService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
|
||||
private final ProcurementLifecycleService lifecycle;
|
||||
private final ProcurementPhysicalService physical;
|
||||
private final ProcurementFinanceService finance;
|
||||
@@ -142,6 +144,7 @@ public class ProcurementTransportService {
|
||||
return snapshot(order);
|
||||
}
|
||||
|
||||
/** 确认采购交寄并记录 PJ 日流水,保留原单关联与重放结果。 */
|
||||
@Transactional
|
||||
public Snapshot ship(String customer,String orderNo,String operator,Ship request) {
|
||||
var order=lifecycle.lock(customer,orderNo);
|
||||
@@ -155,7 +158,7 @@ public class ProcurementTransportService {
|
||||
} else {
|
||||
if(request.shippedAt().isBefore(((LocalDateTime)flow.get("domestic_received_at"))))throw bad("交寄时间不能早于国内收货完成");
|
||||
}
|
||||
long id=IdWorker.getId();String number="PS-"+UUID.randomUUID().toString().replace("-","").substring(0,24).toUpperCase();
|
||||
long id=IdWorker.getId();String number=documentNumbers.next("PJ");
|
||||
jdbc.update("INSERT INTO ll_procurement_shipment(id,order_id,customer_id,shipment_no,leg,manifest_id,shipped_at,address_snapshot,created_by) VALUES(?,?,?,?,?,?,?,CAST(? AS JSON),?)",
|
||||
id,order.getId(),customer,number,request.leg().name(),labels.manifestId(),request.shippedAt(),serialize(new Addresses(labels.sender(),labels.receiver())),operator);
|
||||
for(var carton:cargo) {
|
||||
|
||||
+6
-4
@@ -80,7 +80,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
READY_FOR_OUTBOUND, "COMPLETED", "REJECTED", "CANCELLED");
|
||||
private static final Set<String> DELIVERY_METHODS = Set.of("TRUCK", "EXPRESS", "AMAZON_PICKUP", "CUSTOMER_PICKUP", "PICKUP");
|
||||
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@Resource private RelabelApplicationMapper applications;
|
||||
@Resource private com.baiyue.lastleg.relabel.repository.RelabelStatusRepository statusRepository;
|
||||
@Resource private RelabelItemMapper items;
|
||||
@@ -361,6 +361,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return create(customerId, operatorId, request, existingTailOrderId);
|
||||
}
|
||||
|
||||
/** 创建换标申请,使用 TB 日流水并保留来源货物关联。 */
|
||||
private RelabelResponses.Detail create(String customerId, String operatorId, RelabelRequests.Create request,
|
||||
Long existingTailOrderId) {
|
||||
if (!StringUtils.hasText(request.operationWarehouseId())) throw badRequest("Operation warehouse is required");
|
||||
@@ -488,7 +489,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long applicationId = IdWorker.getId();
|
||||
String operationWarehouseId = request.operationWarehouseId().trim();
|
||||
String relabelNo = compactNumbers.next("TB", customerId);
|
||||
String relabelNo = compactNumbers.next("TB");
|
||||
BigDecimal chargeAmount = Objects.requireNonNullElse(request.chargeAmount(), BigDecimal.ZERO);
|
||||
boolean requiresWallet = chargeAmount.signum() > 0;
|
||||
int holdIndex = 0;
|
||||
@@ -1065,6 +1066,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
return detail(customerId, relabelNo);
|
||||
}
|
||||
|
||||
/** 建立换标出库与派送单,分别分配 HBCK 和 DL 业务号。 */
|
||||
private void createDirectOutbound(RelabelApplication application, String operatorId, LocalDateTime now) {
|
||||
String key = "RELABEL:" + application.getId() + ":OUTBOUND";
|
||||
OutboundOrder existing = outboundOrders.findByIdempotency(application.getCustomerId(), key);
|
||||
@@ -1103,7 +1105,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
}
|
||||
OutboundOrder outbound = new OutboundOrder();
|
||||
outbound.setId(IdWorker.getId()); outbound.setCustomerId(application.getCustomerId());
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND", application.getOverseasWarehouseId(), outbound.getId()));
|
||||
outbound.setOutboundNo(orderNoGenerator.next("RELABEL_OUTBOUND"));
|
||||
outbound.setOverseasWarehouseId(application.getOverseasWarehouseId()); outbound.setOutboundType("RELABEL_OUTBOUND");
|
||||
outbound.setStatus("WAIT_PICK"); outbound.setPackageCount(relabelItems.stream().mapToInt(item -> item.getReturnCartonId() == null ? intValue(item.getNotifiedPackageCount()) : 1).sum());
|
||||
outbound.setPalletCount((int) relabelItems.stream().map(this::outboundSourceKey).distinct().count());
|
||||
@@ -1153,7 +1155,7 @@ public class RelabelApplicationService implements WalletResultHandler {
|
||||
for (var destination : destinations.entrySet()) {
|
||||
var delivery = new com.baiyue.lastleg.delivery.entity.DeliveryOrder();
|
||||
delivery.setId(IdWorker.getId()); delivery.setCustomerId(application.getCustomerId());
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY", application.getOverseasWarehouseId(), delivery.getId()));
|
||||
delivery.setDeliveryNo(orderNoGenerator.next("DELIVERY"));
|
||||
delivery.setOutboundOrderId(outbound.getId()); delivery.setOutboundNo(outbound.getOutboundNo());
|
||||
delivery.setOverseasWarehouseId(application.getOverseasWarehouseId());
|
||||
delivery.setCustomerName(customerName); delivery.setOverseasWarehouseName(warehouseName);
|
||||
|
||||
+5
-3
@@ -189,7 +189,7 @@ public class ReturnInventoryService {
|
||||
List<ReturnInventoryRequests.PlanBox> boxes = List.of(new ReturnInventoryRequests.PlanBox(
|
||||
reserved.entrySet().stream().map(e -> new ReturnInventoryRequests.PlanItem(e.getKey(), e.getValue())).toList(), null, null, null, null));
|
||||
ReturnPackingPlan plan = new ReturnPackingPlan();
|
||||
plan.setId(IdWorker.getId()); plan.setPlanNo(numbers.next("RETURN_PACKING_PLAN", first.getOverseasWarehouseId(), plan.getId()));
|
||||
plan.setId(IdWorker.getId()); plan.setPlanNo(numbers.next("RETURN_PACKING_PLAN"));
|
||||
plan.setCustomerId(first.getCustomerId()); plan.setOverseasWarehouseId(first.getOverseasWarehouseId());
|
||||
plan.setReturnNosJson(json(returnNos)); plan.setBoxesJson(json(boxes)); plan.setSourceItemsJson(json(allocation));
|
||||
plan.setMode(mode); plan.setStatus("PENDING_REVIEW"); plan.setRemark(request.remark()); plan.setIdempotencyKey(request.idempotencyKey());
|
||||
@@ -540,7 +540,7 @@ public class ReturnInventoryService {
|
||||
long cartonId = IdWorker.getId();
|
||||
// 包裹签收已生成 PK 标签,后续库存、废弃和扫码沿用同一编码。
|
||||
if ("PARCEL".equals(receipt.getUnitType())) cartonMark = normalizedUnitCode;
|
||||
else if (cartonMark == null) cartonMark = numbers.next("RETURN_CARTON", order.getOverseasWarehouseId(), cartonId);
|
||||
else if (cartonMark == null) cartonMark = LastlegOrderNoGenerator.compactCartonMark(cartonId);
|
||||
if (cartons.findMark(cartonMark) != null) throw conflict("箱号已存在: " + cartonMark);
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long total = lines.stream().mapToLong(ReceiptLine::quantity).sum();
|
||||
@@ -984,6 +984,7 @@ public class ReturnInventoryService {
|
||||
return mergeScoped(identity, request, limits, targetMark, null);
|
||||
}
|
||||
|
||||
/** 在当前仓范围执行合箱,目标实物箱码继续使用原标签算法。 */
|
||||
private ReturnInventoryResponses.MergeResult mergeScoped(PdaWarehouseIdentity identity, ReturnInventoryRequests.Merge request,
|
||||
Map<String, Map<String, Long>> limits, String targetMark, String generatedMark) {
|
||||
String requestHash = targetMark == null ? hash(request) : hash(List.of(request, targetMark, limits.keySet()));
|
||||
@@ -1122,7 +1123,7 @@ public class ReturnInventoryService {
|
||||
// A merged carton may remain loose; pallet binding is an explicit warehouse operation.
|
||||
target.setSourceUnitType("BOX");
|
||||
target.setId(targetId); target.setCustomerId(customerId); target.setOverseasWarehouseId(sources.get(0).getOverseasWarehouseId());
|
||||
if (existingTarget == null) target.setCartonMark(generatedMark == null ? numbers.next("RETURN_CARTON", target.getOverseasWarehouseId(), targetId) : generatedMark);
|
||||
if (existingTarget == null) target.setCartonMark(generatedMark == null ? LastlegOrderNoGenerator.compactCartonMark(targetId) : generatedMark);
|
||||
if (existingTarget != null) {
|
||||
if (text(target.getSourceBatchNo()) != null) consumedBatchNos.addAll(java.util.Arrays.asList(target.getSourceBatchNo().split(",")));
|
||||
if (text(target.getSourceReturnNo()) != null) consumedReturnNos.addAll(java.util.Arrays.asList(target.getSourceReturnNo().split(",")));
|
||||
@@ -1276,6 +1277,7 @@ public class ReturnInventoryService {
|
||||
+ "</div></body></html>";
|
||||
}
|
||||
private static String dimension(BigDecimal value) { return value == null ? "—" : value.stripTrailingZeros().toPlainString(); }
|
||||
/** 按合箱分组生成实物箱唛,保留完整 ID 编码和分箱后缀。 */
|
||||
private List<String> packingMarks(List<ReturnInventoryRequests.PlanBox> boxes) {
|
||||
List<List<String>> groups = boxes.stream().map(box -> box.items() == null ? List.<String>of() : box.items().stream()
|
||||
.map(ReturnInventoryRequests.PlanItem::skuCode).distinct().sorted().toList()).toList();
|
||||
|
||||
+8
-5
@@ -62,7 +62,7 @@ public class ReturnReceiptService {
|
||||
public static final String PARTIALLY_RECEIVED = "PARTIALLY_RECEIVED";
|
||||
public static final String COMPLETED = "COMPLETED";
|
||||
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@Resource private ReturnOrderMapper orders;
|
||||
@Resource private ReturnOrderItemMapper orderItems;
|
||||
@Resource private ReturnReceiptMapper receipts;
|
||||
@@ -81,6 +81,7 @@ public class ReturnReceiptService {
|
||||
@Resource private com.baiyue.lastleg.service.FbaWarehouseValidator fbaWarehouseValidator;
|
||||
@Resource private com.baiyue.lastleg.service.ReturnOrderReferenceResolver returnOrderReferences;
|
||||
|
||||
/** 创建退货订单并分配 TH 日流水,实物标签继续沿用原规则。 */
|
||||
@Transactional
|
||||
public ReturnOrderResponses.Detail create(String customerId, String operatorId, ReturnOrderRequests.Create request) {
|
||||
String key = text(request.idempotencyKey());
|
||||
@@ -100,7 +101,7 @@ public class ReturnReceiptService {
|
||||
order.setOverseasWarehouseId(text(request.overseasWarehouseId()));
|
||||
order.setWarehouseContactName(text(request.warehouseContactName()));
|
||||
order.setWarehouseContactPhone(text(request.warehouseContactPhone()));
|
||||
order.setReturnNo(compactNumbers.next("TT", customerId));
|
||||
order.setReturnNo(compactNumbers.next("TH"));
|
||||
order.setCustomerReferenceNo(text(request.customerReferenceNo())); order.setNeedRelabel(request.needRelabel()); order.setStatus(PENDING_REVIEW);
|
||||
order.setExpectedSkuTypes(skuCodes.size()); order.setExpectedQuantity(expected); order.setReceivedQuantity(0L);
|
||||
order.setRemark(text(request.remark())); order.setIdempotencyKey(key); order.setRequestHash(requestHash);
|
||||
@@ -396,6 +397,7 @@ public class ReturnReceiptService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/** 从退货创建换标业务并分配 THHB 编号,重放沿用原记录。 */
|
||||
@Transactional
|
||||
public ReturnOrderResponses.RelabelCreated createRelabel(String customerId, String operatorId, String returnNo,
|
||||
ReturnOrderRequests.RelabelCreate request) {
|
||||
@@ -430,7 +432,7 @@ public class ReturnReceiptService {
|
||||
totalQuantity = Math.addExact(totalQuantity, requested.quantity());
|
||||
}
|
||||
long appId = IdWorker.getId(); LocalDateTime now = LocalDateTime.now();
|
||||
String relabelNo = compactNumbers.next("TT", customerId);
|
||||
String relabelNo = compactNumbers.next("THHB");
|
||||
LastlegOrder tail = order.getLastlegOrderId() == null ? null : lastlegOrders.selectById(order.getLastlegOrderId());
|
||||
if (tail == null) {
|
||||
tail = new LastlegOrder(); tail.setId(IdWorker.getId()); tail.setOrderNo(relabelNo);
|
||||
@@ -569,6 +571,7 @@ public class ReturnReceiptService {
|
||||
return detail(order.getCustomerId(), order.getReturnNo());
|
||||
}
|
||||
|
||||
/** 确认退货实收,新增实物箱码沿用原编码,不使用单据日流水。 */
|
||||
@Transactional
|
||||
public PdaReturnReceiptResponses.Confirmed confirm(PdaWarehouseIdentity identity, PdaReturnReceiptRequests.Confirm request) {
|
||||
String key = text(request.idempotencyKey());
|
||||
@@ -643,8 +646,8 @@ public class ReturnReceiptService {
|
||||
long receiptId = IdWorker.getId();
|
||||
ReturnReceipt receipt = new ReturnReceipt();
|
||||
receipt.setId(receiptId); receipt.setReturnOrderId(order.getId()); receipt.setCustomerId(order.getCustomerId());
|
||||
receipt.setReceiptNo(numbers.next("RETURN_RECEIPT", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setReceiptBatchNo(numbers.next("RETURN_BATCH", order.getOverseasWarehouseId(), receiptId));
|
||||
receipt.setReceiptNo(numbers.next("RETURN_RECEIPT"));
|
||||
receipt.setReceiptBatchNo(numbers.next("RETURN_BATCH"));
|
||||
receipt.setTrackingNo(trackingNo); receipt.setBoundBy(startedBy); receipt.setStatus("SIGNED");
|
||||
receipt.setUnitType(unitType); receipt.setUnitCode(unitCode);
|
||||
receipt.setTotalBoxCount(totalBoxCount);
|
||||
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import net.sourceforge.pinyin4j.PinyinHelper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Allocates suffix-free document numbers across all service instances. */
|
||||
@Component
|
||||
public class CompactBusinessNumberService {
|
||||
private static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyMMddHHmmss", Locale.ROOT).withZone(ZONE);
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ReturnOrderReferenceResolver customers;
|
||||
|
||||
public CompactBusinessNumberService(JdbcTemplate jdbc, ReturnOrderReferenceResolver customers) {
|
||||
this.jdbc = jdbc;
|
||||
this.customers = customers;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public String next(String prefix, String customerId) {
|
||||
return allocate(prefix, initials(customers.customerName(customerId)), Instant.now());
|
||||
}
|
||||
|
||||
String allocate(String prefix, String initials, Instant now) {
|
||||
String key = prefix + "-" + initials;
|
||||
long seconds = now.getEpochSecond();
|
||||
// The row lock serializes identical prefixes/initials even across customers and JVMs.
|
||||
jdbc.update("INSERT INTO ll_compact_number_clock(number_key,last_second) VALUES(?,LAST_INSERT_ID(?)) "
|
||||
+ "ON DUPLICATE KEY UPDATE last_second=LAST_INSERT_ID(GREATEST(last_second+1,?))", key, seconds, seconds);
|
||||
Long allocated = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
||||
if (allocated == null || allocated < seconds) throw new IllegalStateException("业务编号生成失败");
|
||||
return key + "-" + FORMAT.format(Instant.ofEpochSecond(allocated));
|
||||
}
|
||||
|
||||
static String initials(String name) {
|
||||
if (name == null || name.isBlank()) return "KH";
|
||||
StringBuilder result = new StringBuilder();
|
||||
boolean latinWord = false;
|
||||
for (char c : name.trim().toCharArray()) {
|
||||
if (result.length() >= 4) break;
|
||||
if (Character.UnicodeScript.of(c) == Character.UnicodeScript.HAN) {
|
||||
String[] pinyin = PinyinHelper.toHanyuPinyinStringArray(c);
|
||||
if (pinyin != null && pinyin.length > 0) result.append(Character.toUpperCase(pinyin[0].charAt(0)));
|
||||
latinWord = false;
|
||||
} else if (c < 128 && Character.isLetterOrDigit(c)) {
|
||||
if (!latinWord) result.append(Character.toUpperCase(c));
|
||||
latinWord = true;
|
||||
} else latinWord = false;
|
||||
}
|
||||
return result.isEmpty() ? "KH" : result.toString();
|
||||
}
|
||||
}
|
||||
+27
-91
@@ -1,122 +1,58 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderNoDailySequenceMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/** 海外单据类型映射;头程来源尾程号与实物箱码保持本次确认的范围边界。 */
|
||||
@Component
|
||||
public class LastlegOrderNoGenerator {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private static final DateTimeFormatter BUSINESS_DATE = DateTimeFormatter.ofPattern("yyMMdd");
|
||||
@Resource private LastlegOrderNoDailySequenceMapper sequenceMapper;
|
||||
private final LastlegDocumentNumberGenerator documents;
|
||||
|
||||
public String next() {
|
||||
return next(LocalDate.now(BUSINESS_ZONE));
|
||||
/** 所有海外业务入口共用所属服务的日流水。 */
|
||||
public LastlegOrderNoGenerator(LastlegDocumentNumberGenerator documents) {
|
||||
this.documents = documents;
|
||||
}
|
||||
|
||||
String next(LocalDate businessDate) {
|
||||
sequenceMapper.nextValue(businessDate);
|
||||
long sequence = sequenceMapper.currentValue();
|
||||
if (sequence < 1 || sequence > 9999) {
|
||||
throw new IllegalStateException("尾程单日流水超出 0001-9999 范围: " + businessDate);
|
||||
}
|
||||
return "LMO-" + BUSINESS_DATE.format(businessDate) + "-" + String.format("%04d", sequence);
|
||||
}
|
||||
|
||||
public String next(String businessType, String warehouse, long uniqueId) {
|
||||
String code = businessType == null ? "" : businessType.trim().toUpperCase(Locale.ROOT);
|
||||
if ("RETURN_CARTON".equals(code)) return compactCartonMark(uniqueId);
|
||||
String returnPrefix = switch (code) {
|
||||
case "RETURN" -> "RT";
|
||||
/** 明确业务类型对应的前缀,未知类型不再截断名称或使用内部 ID 回退。 */
|
||||
public String next(String businessType) {
|
||||
String prefix = switch (businessType) {
|
||||
case "ONE_CLICK_DROPSHIP" -> "DFCK";
|
||||
case "OUTBOUND" -> "CK";
|
||||
case "RELABEL_OUTBOUND" -> "HBCK";
|
||||
case "RETURN_OUTBOUND" -> "THCK";
|
||||
case "DELIVERY" -> "DL";
|
||||
case "TRUCK" -> "TR";
|
||||
case "LOOSEBOARD" -> "LB";
|
||||
case "EXCEPTION" -> "EX";
|
||||
case "DISCARD" -> "CD";
|
||||
case "INBOUND_PLAN" -> "IP";
|
||||
case "RETURN_RECEIPT" -> "RR";
|
||||
case "RETURN_BATCH" -> "RB";
|
||||
case "RETURN_CARTON" -> "RC";
|
||||
case "RETURN_RELABEL" -> "RETU";
|
||||
default -> null;
|
||||
case "RETURN_PACKING_PLAN" -> "HX";
|
||||
default -> throw new IllegalArgumentException("未登记的海外单据类型:" + businessType);
|
||||
};
|
||||
// Return numbers are independent business identifiers, not truncated database IDs.
|
||||
// Keep every UUID bit; the longest result (RETU + 25 base-36 digits) fits VARCHAR(32).
|
||||
if (returnPrefix != null) return returnPrefix + "-" + encodeReturnUuid(UUID.randomUUID());
|
||||
// Transfer numbers also identify synthetic inbound events globally. Truncating
|
||||
// a database ID to six digits can collide between unrelated fresh transfers.
|
||||
if ("TRANSFER".equals(code) || "WAREHOUSE_TRANSFER".equals(code)) {
|
||||
return "WT-" + encodeReturnUuid(UUID.randomUUID());
|
||||
}
|
||||
String type = normalizeType(businessType);
|
||||
return type + "-" + warehouseCode(warehouse) + "-"
|
||||
+ LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
|
||||
+ String.format(Locale.ROOT, "%06d", Math.floorMod(uniqueId, 1_000_000));
|
||||
return documents.next(prefix);
|
||||
}
|
||||
|
||||
static String encodeReturnUuid(UUID uuid) {
|
||||
byte[] bytes = ByteBuffer.allocate(16).putLong(uuid.getMostSignificantBits())
|
||||
.putLong(uuid.getLeastSignificantBits()).array();
|
||||
String encoded = new BigInteger(1, bytes).toString(36).toUpperCase(Locale.ROOT);
|
||||
return "0".repeat(25 - encoded.length()) + encoded;
|
||||
}
|
||||
|
||||
/** Preserve every snowflake ID bit; do not truncate or hash physical label identifiers. */
|
||||
/** 实物箱码保留完整 ID 的三十六进制编码,本次不修改扫码标签。 */
|
||||
public static String compactCartonMark(long id) {
|
||||
if (id <= 0) throw new IllegalArgumentException("Carton id must be positive");
|
||||
return "BX-" + Long.toString(id, 36).toUpperCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private String normalizeType(String value) {
|
||||
String code = value == null ? "LMO" : value.trim().toUpperCase(Locale.ROOT);
|
||||
return switch (code) {
|
||||
case "FIRST_LEG" -> "FL";
|
||||
case "ONE_CLICK_DROPSHIP" -> "OD";
|
||||
case "RELABEL_APPLICATION", "RELABEL" -> "RL";
|
||||
case "WAREHOUSE_TRANSFER", "TRANSFER" -> "WT";
|
||||
case "LOCAL_LOOSEBOARD", "LOOSEBOARD" -> "LB";
|
||||
case "DISPATCH", "PICKUP_CAR" -> "DS";
|
||||
case "OUTBOUND" -> "OB";
|
||||
case "DELIVERY" -> "DL";
|
||||
case "TRUCK" -> "TR";
|
||||
case "EXCEPTION" -> "EX";
|
||||
case "DISCARD" -> "CD";
|
||||
case "PROCUREMENT" -> "PR";
|
||||
case "INBOUND_PLAN" -> "IP";
|
||||
default -> {
|
||||
String normalized = code.replaceAll("[^A-Z0-9]", "");
|
||||
yield normalized.isEmpty() ? "LMO" : normalized.substring(0, Math.min(4, normalized.length()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public String warehouseCode(String value) {
|
||||
String code = value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||
if (code.isEmpty()) return "WH";
|
||||
if (code.matches(".*(LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD).*")) {
|
||||
var match = java.util.regex.Pattern.compile("(LAX|ONT|NYC|JFK|LGB|EWR|SFO|SEA|DFW|ORD|ATL|MIA|PHX|LAS|YYZ|YVR|AMS|LHR|FRA|CDG|HKG|NRT|SYD)").matcher(code);
|
||||
if (match.find()) return match.group(1);
|
||||
}
|
||||
if (code.matches("[A-Z0-9]{2,8}")) return code;
|
||||
return "WH";
|
||||
}
|
||||
|
||||
/** The physical container number is the tail-leg business order grouping boundary. */
|
||||
/** 独立尾程编号尚未确认,头程装柜来源继续使用原有分组编号。 */
|
||||
public String forContainerNo(String containerNo) {
|
||||
if (containerNo == null || containerNo.isBlank()) {
|
||||
throw new IllegalArgumentException("containerNo is required");
|
||||
}
|
||||
String normalized = containerNo.trim();
|
||||
if (containerNo == null || containerNo.isBlank()) throw new IllegalArgumentException("containerNo is required");
|
||||
try {
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256")
|
||||
.digest(normalized.getBytes(StandardCharsets.UTF_8));
|
||||
byte[] digest = MessageDigest.getInstance("SHA-256").digest(containerNo.trim().getBytes(StandardCharsets.UTF_8));
|
||||
long suffix = java.nio.ByteBuffer.wrap(digest, 0, 8).getLong();
|
||||
return next("FIRST_LEG", null, suffix);
|
||||
return "FL-WH-" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) + "-"
|
||||
+ String.format(Locale.ROOT, "%06d", Math.floorMod(suffix, 1_000_000));
|
||||
} catch (NoSuchAlgorithmException exception) {
|
||||
throw new IllegalStateException("SHA-256 is unavailable", exception);
|
||||
}
|
||||
|
||||
+3
-2
@@ -96,7 +96,7 @@ public class WarehouseTransferService {
|
||||
@Resource private com.baiyue.lastleg.fleet.pickup.FleetPickupService pickupService;
|
||||
/** 退货单在转运来源中的展示编码,避免把退货单号误当成普通入仓码查询。 */
|
||||
public static final String RETURN_ORDER_PREFIX = "RETURN_ORDER:";
|
||||
@Resource private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@Resource private com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge fleetPool;
|
||||
@Resource private com.baiyue.lastleg.fleet.business.FleetPoolOrderQueries fleetPoolQueries;
|
||||
@@ -239,7 +239,7 @@ public class WarehouseTransferService {
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long transferId = IdWorker.getId();
|
||||
String transferNo = compactNumbers.next("ZY", customerId);
|
||||
String transferNo = compactNumbers.next("ZY");
|
||||
String requestedInboundCode = trim(request.inboundCode());
|
||||
List<Long> selectedIds = request.sourceLastlegOrderIds() == null
|
||||
? List.of() : request.sourceLastlegOrderIds().stream().distinct().toList();
|
||||
@@ -2252,6 +2252,7 @@ public class WarehouseTransferService {
|
||||
return item.getId();
|
||||
}
|
||||
|
||||
/** 保存转运箱信息,优先使用预印箱唛,新增箱唛仍按原标签规则生成。 */
|
||||
private void insertTransferCarton(String customerId, long transferId, long transferItemId,
|
||||
String transferNo, int cartonIndex, int totalCartons,
|
||||
String customerCartonMark, LocalDateTime now, String printedMark) {
|
||||
|
||||
+1
-1
@@ -70,7 +70,7 @@ public class UsTruckOrderService {
|
||||
UsTruckOrder order = new UsTruckOrder();
|
||||
order.setId(IdWorker.getId());
|
||||
order.setCustomerId(customerId);
|
||||
order.setOrderNo(orderNoGenerator.next("TRUCK", request.origin(), order.getId()));
|
||||
order.setOrderNo(orderNoGenerator.next("TRUCK"));
|
||||
order.setVehicleType(request.vehicleType());
|
||||
order.setOrigin(trim(request.origin())); order.setOriginAddress(trim(request.originAddress()));
|
||||
order.setDestination(trim(request.destination())); order.setDestinationAddress(trim(request.destinationAddress()));
|
||||
|
||||
Reference in New Issue
Block a user