feat(numbering): unify daily business document numbers (backend)
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
### 预约、车队与导出依赖收口(2026-10-12)
|
||||
|
||||
- 预约规划只声明 `AppointmentSourceLinker` 端口;入仓域负责创建入口与关联明细实现,入仓请求声明自己的来源 DTO,再映射到规划模型。公开 JSON、事务及来源校验保持不变,规划包不反向依赖入仓实现。
|
||||
- 车队商业单使用本域的收货证据和签收快照;派送域实现车队声明的证据端口并提供完整签收事实,车队不再导入派送实体、Mapper 或完成策略。商业单编号保持原 LB/TR 规则,由本域生成;`fleet.business` 不反向依赖 `fleet.trip`。既有散板、卡车商业单的同库写入不在此次拆分范围,本次门禁通过不代表完成全域解耦。
|
||||
- 车队商业单使用本域的收货证据和签收快照;派送域实现车队声明的证据端口并提供完整签收事实,车队不再导入派送实体、Mapper 或完成策略。商业单编号沿用 LB/TR 前缀,通过共享编号组件按工作区 `docs/业务单据编号契约.md` 分配,各入口共用本库日流水;`fleet.business` 不反向依赖 `fleet.trip`。既有散板、卡车商业单的同库写入不在此次拆分范围,本次门禁通过不代表完成全域解耦。
|
||||
- 退货客户引用与转仓 FBA 目录各由本域内部客户端读取原权威服务,移除对车队客户端的借用;接口、令牌及 URL 配置不变。退货合箱共用 PDA 仓身份、转仓实收共用仓储资源网关,两条精确类边在基线中登记。
|
||||
- 平台导出作为组合入口,仅登记 `LastlegExportProviderService` 对 12 个公开查询服务与 DTO/投影的引用,不开放整个切片;车辆仍限应用层。排车规划适配器只使用车队公开规划和收货端口。采购对海外主数据的旧依赖已不存在,移除该旧许可及入向断言。
|
||||
- 反向检查继续拒绝新消费者、导出直连持久化、车辆应用层外向依赖和任何主源码包循环。分析器继续按既有显式 import、静态 import 及同包类型引用规则检查;本次不改变其识别范围。
|
||||
@@ -28,6 +28,10 @@
|
||||
|
||||
`fleet.ownedbusiness`只为原排车装车后的自有车辆组合运营资料。五条精确引用限`OwnedBusinessModels→DispatchViews`、`OwnedBusinessProjection→DispatchContracts`、`OwnedBusinessQueryService→DispatchContracts/DispatchTypes/DispatchDetailReader`,均为`dispatch.port`公开事实或只读应用端口;不增加`fleet→dispatch-delivery`通用许可,不共享原排车实体、Mapper或具体Service。原详情适配器在dispatch内部委托唯一提供者。`DispatchAggregateService→OwnedBusinessOwnershipCapture`仅在原装车事务捕获归属,不能反向调用运营查询;捕获失败随原确认回滚。反向合同拒绝具体持久化/Service引用、其他消费者借用端口许可及任何包循环。
|
||||
|
||||
### 外配业务原排车详情读取边界(2026-10-21)
|
||||
|
||||
`OutsourcedBusinessModels → DispatchViews` 仅组合既有 `dispatch.port` 公开详情。依赖登记与已有外配反向合同同步,不开放 `fleet→dispatch-delivery` 通用许可;原排车实体、Mapper、具体 Service 及其他消费者仍由门禁拒绝。
|
||||
|
||||
### 平台自营统计边界
|
||||
|
||||
`selfoperated/analytics/` 在依赖分析器中作为独立 `selfoperated-analytics` 切片优先匹配,其 `outbound`、`inventory` 和 `model` 子包不归入客户出库、客户库存或尾程交接切片。该子域通过内部 HTTP 使用 Integration,持久化十张独立 `ll_selfop_*` 表,不引用现有客户履约实体或 Mapper。唯一基础能力引用为 `SelfopEgressController → security.InternalServiceTokenGuard`,复用内部令牌校验;基线仅登记此精确类边,不增加共享技术切片通配许可,包循环仍按同一门禁拒绝。
|
||||
@@ -36,7 +40,7 @@
|
||||
|
||||
车次关联客户排车单新增 `dispatch-delivery→fleet`:fleet声明 `FleetDispatchGateway`,dispatch内适配器锁定自身排车/出库/派送并同步车和司机快照;子排车修改与发车使用fleet公开的关联门禁。fleet不导入dispatch实体、Mapper或业务服务,因此不新增双向切片依赖。车次与客户单据的归属不变,关联和同步处于同一Lastleg事务;客户单据已经开工时不允许通过改派覆盖执行事实。
|
||||
|
||||
当前分析器把 `wallet.WalletCommandOutboxService` 归入 `shared-technical`,它依赖尾程Outbox实体/Mapper;粗粒度图因而存在 `fleet→shared-technical→tailleg-handoff→dispatch-delivery→fleet` 聚合环。不能把这一聚合环等同于fleet包或Spring组件循环,也不能宣称切片图无环。门禁检查实际包无环,fleet仅允许上节自有运营的五条公开只读引用,dispatch只使用公开端口/门禁;粗粒度切片图保留为耦合报告,不因门禁通过隐去聚合环。
|
||||
当前分析器把 `wallet.WalletCommandOutboxService` 归入 `shared-technical`,它依赖尾程Outbox实体/Mapper;粗粒度图因而存在 `fleet→shared-technical→tailleg-handoff→dispatch-delivery→fleet` 聚合环。不能把这一聚合环等同于fleet包或Spring组件循环,也不能宣称切片图无环。门禁检查实际包无环,fleet仅允许上述自有运营的五条公开只读引用及外配详情的一条公开模型引用,dispatch只使用公开端口/门禁;粗粒度切片图保留为耦合报告,不因门禁通过隐去聚合环。
|
||||
|
||||
2026-10-02 新增 `fleet` 业务切片,归入当前 Lastleg 部署单元;司机档案与文件适配在 `fleet/driver`,计划与资源占用在 `fleet/trip`,纯资格、分段容量和成本分摊在 `fleet/domain`。fleet依赖技术HTTP配置,以及车辆应用层的 `VehicleRepository` / `VehicleAssignmentGuard` 端口和已有状态服务;新增 `fleet→overseas-master-data` 是同一物理车辆主档和车次占用的明确业务协作,不允许直接依赖车辆持久化适配器。车辆应用层继续只依赖自身端口,注入的占用检查实现归fleet。分析器显式分类并验证这些边界,旧统计及退役安全投影断言不代表当前架构事实。
|
||||
|
||||
|
||||
+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),
|
||||
|
||||
+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
@@ -21,13 +21,13 @@ import static com.baiyue.lastleg.fleet.business.FleetPoolOrderModels.*;
|
||||
@Service
|
||||
public class FleetPoolOrderBridge {
|
||||
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;
|
||||
@@ -50,7 +50,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(IdWorker.getId());
|
||||
row.setBusinessOrderNo(numbers.next("US_TRUCK".equals(in.businessType())?"TRUCK":"LOOSEBOARD",in.destinationLabel(),row.getBusinessOrderId()));
|
||||
row.setBusinessOrderNo(numbers.next("US_TRUCK".equals(in.businessType())?"TR":"LB"));
|
||||
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());
|
||||
|
||||
+3
-1
@@ -25,6 +25,7 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class FleetCapacityOfferService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
public static final String LIST="platform:fleet:capacity-offer:list", MANAGE="platform:fleet:capacity-offer:manage";
|
||||
private final FleetCapacityOfferMapper offers;
|
||||
private final FleetCapacityOfferCommandMapper commands;
|
||||
@@ -125,6 +126,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);
|
||||
@@ -133,7 +135,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,7 @@ import java.util.*;
|
||||
|
||||
@Service
|
||||
public class FleetTripService {
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges workChanges;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort appointmentAllocations;
|
||||
private final FleetTripRepository repository;
|
||||
@@ -107,7 +108,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());
|
||||
@@ -383,7 +384,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());
|
||||
|
||||
+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(),
|
||||
@@ -277,7 +277,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);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.baiyue.lastleg.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Options;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
/** 本服务业务编号序列,更新与读取必须处于同一事务连接。 */
|
||||
@Mapper
|
||||
public interface DocumentSequenceMapper {
|
||||
/** 唯一序列键原子递增,并在连接内保存本次分配值。 */
|
||||
@Insert("INSERT INTO ll_document_sequence(sequence_key,current_value) VALUES(#{key},LAST_INSERT_ID(1)) "
|
||||
+ "ON DUPLICATE KEY UPDATE current_value=LAST_INSERT_ID(current_value+1)")
|
||||
int allocate(@Param("key") String key);
|
||||
|
||||
/** 禁用一级和二级缓存,返回本事务刚刚分配的值。 */
|
||||
@Select("SELECT LAST_INSERT_ID()")
|
||||
@Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE)
|
||||
Long allocatedValue();
|
||||
}
|
||||
-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);
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.repository;
|
||||
|
||||
import com.baiyue.lastleg.mapper.DocumentSequenceMapper;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/** 各业务共用本库序列,独立短事务避免持有编号锁进入业务资源锁。 */
|
||||
@Repository
|
||||
public class DocumentSequenceRepository {
|
||||
private final DocumentSequenceMapper mapper;
|
||||
|
||||
/** 只访问所属服务的编号表。 */
|
||||
public DocumentSequenceRepository(DocumentSequenceMapper mapper) { this.mapper = mapper; }
|
||||
|
||||
/** 已分配号不因外层业务回滚而复用,允许失败请求产生断号。 */
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public long next(String key) {
|
||||
mapper.allocate(key);
|
||||
Long value = mapper.allocatedValue();
|
||||
if (value == null || value < 1) throw new IllegalStateException("业务编号分配失败");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+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();
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.common.core.number.DailyDocumentNumber;
|
||||
import com.baiyue.lastleg.repository.DocumentSequenceRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.Clock;
|
||||
import java.util.Set;
|
||||
|
||||
/** 按北京时间生成两至四位前缀、八位日期和四位日流水。 */
|
||||
@Component
|
||||
public class LastlegDocumentNumberGenerator {
|
||||
private static final Set<String> PREFIXES = Set.of("IP", "CK", "DFCK", "HBCK", "THCK", "DL", "EX", "CD", "TB", "ZY", "TH", "THHB", "RR", "RB", "HX", "YY", "TR", "LB", "CG", "PR", "PJ", "PF", "PN", "PX", "PG", "CT", "FB", "FO", "VR");
|
||||
private final DocumentSequenceRepository sequences;
|
||||
private final Clock clock;
|
||||
|
||||
/** 正式运行使用统一业务时区。 */
|
||||
@Autowired
|
||||
public LastlegDocumentNumberGenerator(DocumentSequenceRepository sequences) {
|
||||
this(sequences, Clock.system(DailyDocumentNumber.BUSINESS_ZONE));
|
||||
}
|
||||
|
||||
/** 固定时钟用于跨日及前缀独立性验证。 */
|
||||
LastlegDocumentNumberGenerator(DocumentSequenceRepository sequences, Clock clock) {
|
||||
this.sequences = sequences;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
/** 只为本服务已登记的业务类型分配号码。 */
|
||||
public String next(String prefix) {
|
||||
if (!PREFIXES.contains(prefix)) throw new IllegalArgumentException("未登记的业务编号前缀:" + prefix);
|
||||
return DailyDocumentNumber.next(prefix, clock, sequences::next);
|
||||
}
|
||||
}
|
||||
+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()));
|
||||
|
||||
@@ -35,12 +35,6 @@ CREATE TABLE IF NOT EXISTS ll_container_shipping_gate (
|
||||
KEY idx_ll_shipping_gate_version(container_booking_id,aggregate_version,node_sequence)
|
||||
) ENGINE=InnoDB COMMENT='海外PDA柜号船节点收货资格快照';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_order_no_daily_sequence (
|
||||
business_date DATE NOT NULL COMMENT '业务日期(Asia/Shanghai)',
|
||||
last_sequence INT NOT NULL COMMENT '当日已分配尾程单流水号',
|
||||
PRIMARY KEY(business_date)
|
||||
) ENGINE=InnoDB COMMENT='尾程单号按日原子流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_dispatch_daily_sequence (
|
||||
business_date DATE NOT NULL COMMENT '排车编号业务日期(Asia/Shanghai)',
|
||||
last_sequence INT NOT NULL COMMENT '当日已分配排车流水号,范围0001至9999',
|
||||
@@ -3209,16 +3203,6 @@ CREATE TABLE IF NOT EXISTS ll_wallet_event_inbox (
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_procurement_daily_sequence (
|
||||
customer_id VARCHAR(128) NOT NULL COMMENT '客户ID',
|
||||
business_date DATE NOT NULL COMMENT '北京时间业务日期',
|
||||
sequence_value INT NOT NULL COMMENT '客户当日已分配最大序号',
|
||||
PRIMARY KEY (customer_id,business_date),
|
||||
CONSTRAINT chk_ll_procurement_sequence CHECK (sequence_value BETWEEN 1 AND 9999)
|
||||
) ENGINE=InnoDB COMMENT='海外采购客户日序列';
|
||||
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_overseas_procurement_file (
|
||||
id BIGINT NOT NULL COMMENT '主键ID', order_id BIGINT NOT NULL COMMENT '采购订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID',
|
||||
category VARCHAR(16) NOT NULL COMMENT 'RECEIPT实收、VOUCHER费用、LABEL面单、PURCHASE采购、BUDGET预算、EXCEPTION异常凭证', file_id BIGINT NOT NULL COMMENT 'File服务文件ID',
|
||||
@@ -3246,6 +3230,7 @@ CREATE TABLE IF NOT EXISTS ll_overseas_procurement_fee (
|
||||
settled_command_event_id VARCHAR(64) NULL COMMENT '成功实扣或退款的资金命令事件',
|
||||
source_fee_id BIGINT NULL COMMENT '已实扣费用来源ID',
|
||||
parent_fee_id BIGINT NULL COMMENT '原费用或预估引用',
|
||||
idempotency_key VARCHAR(128) NULL COMMENT '退款请求幂等键,独立于业务单号',
|
||||
request_hash CHAR(64) NULL COMMENT '确认请求摘要',
|
||||
id BIGINT NOT NULL COMMENT '主键ID', order_id BIGINT NOT NULL COMMENT '采购订单ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户ID',
|
||||
fee_no VARCHAR(64) NOT NULL COMMENT '费用业务号', fee_item_code VARCHAR(32) NOT NULL COMMENT '费用项编码', fee_item_name VARCHAR(128) NOT NULL COMMENT '费用项名称',
|
||||
@@ -3256,6 +3241,7 @@ CREATE TABLE IF NOT EXISTS ll_overseas_procurement_fee (
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
CONSTRAINT chk_procurement_confirmation_origin CHECK(confirmation_origin IN('BUSINESS','FINANCIAL')),
|
||||
UNIQUE KEY uk_ll_procurement_fee_version(order_id,purpose,fee_item_code,fee_version),
|
||||
UNIQUE KEY uk_ll_procurement_refund_request(order_id,purpose,idempotency_key),
|
||||
PRIMARY KEY(id), UNIQUE KEY uk_ll_procurement_fee_no(fee_no), KEY idx_ll_procurement_fee_order(customer_id,order_id,created_at),
|
||||
CONSTRAINT chk_ll_procurement_fee_amount CHECK(amount>=0), CONSTRAINT chk_ll_procurement_fee_currency CHECK(currency='CNY')
|
||||
) ENGINE=InnoDB COMMENT='海外采购费用明细及资金生命周期';
|
||||
@@ -4498,11 +4484,6 @@ CREATE TABLE IF NOT EXISTS ll_appointment_work_history (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='预约工作事项历史与幂等命令';
|
||||
|
||||
-- 新单据短号:相同业务及客户名称缩写共享逻辑秒,避免同秒或多实例重号。
|
||||
CREATE TABLE IF NOT EXISTS ll_compact_number_clock (
|
||||
number_key VARCHAR(16) NOT NULL COMMENT '业务前缀和客户名称缩写',
|
||||
last_second BIGINT NOT NULL COMMENT '最后分配的Unix逻辑秒',
|
||||
PRIMARY KEY (number_key)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='海外业务短单号分配时钟';
|
||||
-- 库存转运无需再次预约入仓;目标库:lastleg-service 所属数据库。可重复执行。
|
||||
SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_warehouse_transfer_order' AND column_name='estimated_arrival_at' AND is_nullable='NO'), 'ALTER TABLE ll_warehouse_transfer_order MODIFY COLUMN estimated_arrival_at DATETIME(3) NULL COMMENT ''预计入仓时间,库存转运不填写''', 'SELECT 1'));
|
||||
PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;
|
||||
@@ -4638,3 +4619,11 @@ CREATE TABLE IF NOT EXISTS ll_fleet_owned_business_history (
|
||||
CONSTRAINT fk_owned_business_history_dispatch FOREIGN KEY (dispatch_id) REFERENCES ll_dispatch_order(id),
|
||||
CONSTRAINT ck_owned_business_history_version CHECK (record_version>0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原排车自有业务附加运营不可变历史';
|
||||
|
||||
-- 统一业务单号:前缀 + yyyyMMdd + 四位日流水。
|
||||
CREATE TABLE IF NOT EXISTS ll_document_sequence (
|
||||
sequence_key VARCHAR(32) NOT NULL COMMENT '业务前缀与北京时间日期组成的全局序列键',
|
||||
current_value BIGINT NOT NULL COMMENT '该前缀当日已分配流水,已分配号不回收',
|
||||
PRIMARY KEY (sequence_key),
|
||||
CONSTRAINT chk_ll_document_sequence_value CHECK (current_value > 0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='业务单据每日独立原子流水';
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class DispatchDeliveryArrangementPersistenceTest {
|
||||
var repository=new DispatchDeliveryRepository(session.getMapper(DispatchDeliveryArrangementMapper.class),session.getMapper(OutboundOrderMapper.class),
|
||||
session.getMapper(OutboundPalletItemMapper.class),session.getMapper(DeliveryOrderMapper.class),session.getMapper(DeliveryOperationMapper.class),
|
||||
session.getMapper(LastlegOrderMapper.class),session.getMapper(LastlegOrderStatusHistoryMapper.class));
|
||||
var numbers=mock(LastlegOrderNoGenerator.class);when(numbers.next(anyString(),anyString(),anyLong())).thenAnswer(a->a.getArgument(0)+"-"+a.getArgument(2));
|
||||
var numbers=mock(LastlegOrderNoGenerator.class);when(numbers.next(anyString())).thenAnswer(a->a.getArgument(0)+"-"+com.baomidou.mybatisplus.core.toolkit.IdWorker.getId());
|
||||
var target=new DispatchDeliveryService(execution,repository,grouping,fees,new NormalDeliveryCompletionPolicy(grouping),numbers,json);
|
||||
var proxy=new ProxyFactory(target);proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds),new AnnotationTransactionAttributeSource()));
|
||||
service=(DispatchDeliveryService)proxy.getProxy();
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class DispatchDeliveryServiceTest {
|
||||
@BeforeEach void prepare() {
|
||||
context=context(List.of(cargo(1,"101","ONT8",51,2),cargo(2,"101","LAX9",52,3),cargo(3,"102","ONT8",53,4)),false);
|
||||
var numbers=mock(LastlegOrderNoGenerator.class);
|
||||
when(numbers.next(anyString(),anyString(),anyLong())).thenAnswer(a->a.getArgument(0)+"-"+a.getArgument(2));
|
||||
when(numbers.next(anyString())).thenAnswer(a->a.getArgument(0)+"-"+com.baomidou.mybatisplus.core.toolkit.IdWorker.getId());
|
||||
service=new DispatchDeliveryService(execution,repository,grouping,fees,new NormalDeliveryCompletionPolicy(grouping),numbers,json);
|
||||
when(execution.lockForArrangement(any(),eq("DP-10"))).thenAnswer(a->context);
|
||||
when(execution.replay(any(),any(),any())).thenReturn(Optional.empty());
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ class DeliveryTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "planAllocations", planAllocations);
|
||||
ReflectionTestUtils.setField(service, "planLines", planLines);
|
||||
ReflectionTestUtils.setField(service, "inboundCartons", inboundCartons);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
ReflectionTestUtils.setField(service, "tailOrders", tailOrders);
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "tailOrderDetails", tailOrderDetails);
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ class ReturnOutboundClosureTest {
|
||||
ReflectionTestUtils.setField(service, "pdaPicks", picks);
|
||||
ReflectionTestUtils.setField(service, "relabelApplications", relabelApplications);
|
||||
ReflectionTestUtils.setField(service, "relabelService", relabelService);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
carton = new ReturnInventoryCarton(); carton.setId(10L); carton.setCartonMark("RC#1-1");
|
||||
carton.setCustomerId("C1"); carton.setOverseasWarehouseId("WH1"); carton.setSourceReturnNo("RT1,RT2");
|
||||
|
||||
+2
-1
@@ -82,7 +82,8 @@ class ReturnOutboundPdaPersistenceTest {
|
||||
stockProxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds), new AnnotationTransactionAttributeSource()));
|
||||
ReflectionTestUtils.setField(target, "returnStock", stockProxy.getProxy());
|
||||
var numbers = mock(LastlegOrderNoGenerator.class);
|
||||
when(numbers.next(anyString(), eq("WH-1"), anyLong())).thenAnswer(call -> "RETU-" + call.getArgument(2));
|
||||
var testNumber = new java.util.concurrent.atomic.AtomicInteger();
|
||||
when(numbers.next(anyString())).thenAnswer(call -> "THCK20261020" + String.format("%04d", testNumber.incrementAndGet()));
|
||||
ReflectionTestUtils.setField(target, "orderNoGenerator", numbers);
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds), new AnnotationTransactionAttributeSource()));
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ class CargoDiscardOutboundServiceTest {
|
||||
ReflectionTestUtils.setField(service, "cartons", cartons);
|
||||
ReflectionTestUtils.setField(service, "profiles", profiles);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
application = new CargoDiscardApplication(); application.setId(1L); application.setCustomerId("C1");
|
||||
application.setDiscardNo("CD-TEST"); application.setOverseasWarehouseId("99");
|
||||
application.setTotalPackages(5); application.setStatus("APPROVED");
|
||||
@@ -66,7 +66,7 @@ class CargoDiscardOutboundServiceTest {
|
||||
assertEquals("DISCARD_OUTBOUND", document.getOutboundType()); assertEquals("WAIT_PICK", document.getStatus());
|
||||
assertNull(document.getLxCodes());
|
||||
assertEquals(5, document.getPackageCount()); assertEquals(2, document.getPalletCount());
|
||||
assertTrue(service.outboundNo(application).startsWith("OB-"));
|
||||
assertTrue(service.outboundNo(application).matches("CK[0-9]{12}"));
|
||||
assertEquals(List.of(3, 2), snapshots.stream().map(OutboundPalletItem::getPackageCount).toList());
|
||||
verify(outbounds, times(1)).insert(any(OutboundOrder.class)); verify(pallets, times(2)).insert(any(OutboundPalletItem.class));
|
||||
}
|
||||
|
||||
+1
-1
@@ -134,7 +134,7 @@ class CargoDiscardServiceTest {
|
||||
ReflectionTestUtils.setField(service, "histories", histories);
|
||||
ReflectionTestUtils.setField(service, "inventory", inventory);
|
||||
ReflectionTestUtils.setField(service, "adjustments", adjustments);
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderHistories", orderHistories);
|
||||
ReflectionTestUtils.setField(service, "returnCartons", returnCartons);
|
||||
|
||||
+1
@@ -517,6 +517,7 @@ public class DispatchAggregatePersistenceTest {
|
||||
mock(com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper.class),mock(com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository.class),mock(DispatchAggregateService.class),json);
|
||||
var vehicles=mock(VehicleRepository.class); var drivers=mock(DriverService.class);var reservations=mock(FleetBookingReservations.class);
|
||||
var fleetService=new FleetTripService(fleetRepo,vehicles,drivers,warehouses,json,gateway,reservations);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(fleetService, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
nativeFleetService=fleetService;
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(fleetService,"workChanges",mock(com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(fleetService,"appointmentAllocations",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class));
|
||||
|
||||
+2
@@ -22,6 +22,7 @@ class DispatchDraftResourceLockTest {
|
||||
when(repository.lockTrip("TR-7")).thenReturn(first);when(repository.lockTrip("TR-2")).thenReturn(second);
|
||||
when(repository.listBindings(7L)).thenReturn(List.of(a));when(repository.listBindings(2L)).thenReturn(List.of(b));
|
||||
var service=new FleetTripService(repository,vehicles,mock(DriverService.class),mock(FleetWarehouseDirectory.class),new ObjectMapper(),mock(FleetDispatchGateway.class),mock(FleetBookingReservations.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
service.lockExistingDispatchShells(List.of("DS-A","DS-B"));
|
||||
var order=inOrder(repository,vehicles);
|
||||
order.verify(repository).lockTrip("TR-2");order.verify(repository).lockTrip("TR-7");
|
||||
@@ -37,6 +38,7 @@ class DispatchDraftResourceLockTest {
|
||||
when(vehicles.lockById(anyLong())).thenReturn(Optional.of(mock(VehicleRepository.VehicleSnapshot.class)));
|
||||
when(repository.lockDriverIdentity(anyLong())).thenAnswer(call->call.getArgument(0));
|
||||
var service=new FleetTripService(repository,vehicles,mock(DriverService.class),mock(FleetWarehouseDirectory.class),new ObjectMapper(),mock(FleetDispatchGateway.class),mock(FleetBookingReservations.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
var v9=new FleetExecutionModels.DispatchVehicle("MASTER",9L,null,null,null,null,null,List.of());
|
||||
var v3=new FleetExecutionModels.DispatchVehicle("MASTER",3L,null,null,null,null,null,List.of());
|
||||
var d8=new FleetExecutionModels.DispatchDriver("MASTER",8L,null,null,null,null,List.of(),null);
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ class DispatchRepairWorkflowTest {
|
||||
db.execute("CREATE TABLE IF NOT EXISTS ll_physical_pallet_guard(warehouse_pallet_id BIGINT PRIMARY KEY,created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3))");
|
||||
for(var column:List.of("order_id BIGINT","warehouse_pallet_id BIGINT","inventory_position_id BIGINT","received_packages BIGINT"))db.execute("ALTER TABLE ll_inbound_stock_receipt ADD COLUMN IF NOT EXISTS "+column);
|
||||
candidates=spy(session.getMapper(DispatchCandidateMapper.class));doReturn(List.of()).when(candidates).nativeReturnRelabelAvailable(any(),any(),any());dependencies.put(DispatchCandidateMapper.class,candidates);
|
||||
dependencies.put(ObjectMapper.class,json);dependencies.put(LastlegOrderNoGenerator.class,new LastlegOrderNoGenerator());
|
||||
dependencies.put(ObjectMapper.class,json);dependencies.put(LastlegOrderNoGenerator.class,com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
var sourceRepository=new com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository(candidates,session.getMapper(ReturnDispatchCandidateMapper.class),session.getMapper(DispatchSourceMapper.class));
|
||||
dependencies.put(com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository.class,sourceRepository);dependencies.put(DispatchCommandHasher.class,new DispatchCommandHasher(json));
|
||||
var relabelSources=new RelabelDispatchSourceService(new RelabelDispatchSourceRepository(mapper(RelabelDispatchSourceMapper.class)),json);
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ class ReturnCartonReturnPersistenceTest {
|
||||
var pallets=mock(PalletPhysicalGateway.class);var warehouse=mock(WarehouseResourceGateway.class);
|
||||
when(pallets.resolve("P-NEW")).thenReturn(new PalletPhysicalGateway.PalletReference(501,"P-NEW",92,301L,"OCCUPIED","ONT8",1));
|
||||
when(warehouse.resolveLocation(92,"LOC-301")).thenReturn(new WarehouseResourceGateway.WarehouseLocationReference(301,"LOC-301","RACK",BigDecimal.TEN,"ACTIVE"));
|
||||
service=new ReturnCartonReturnService(repo,pallets,warehouse,new ObjectMapper(),new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
service=new ReturnCartonReturnService(repo,pallets,warehouse,new ObjectMapper(),com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
db.update("INSERT INTO ll_exception_order(id,customer_id,exception_no,delivery_order_id,last_mile_order_id,outbound_no,status,return_direction,overseas_warehouse_id) VALUES(1,'C','EX-1',3,100,'OUT-1','PROCESSING','OVERSEAS_WAREHOUSE','92')");
|
||||
db.update("INSERT INTO ll_delivery_order(id,customer_id,outbound_order_id) VALUES(3,'C',2)");
|
||||
db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,status) VALUES(2,'C','OUT-1',10,'OUTBOUNDED')");
|
||||
@@ -183,7 +183,7 @@ class ReturnCartonReturnPersistenceTest {
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"objectMapper",new ObjectMapper().findAndRegisterModules());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"evidenceFiles",mock(com.baiyue.lastleg.exceptionorder.gateway.ExceptionEvidenceFileGateway.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"nativeReturns",service);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"orderNoGenerator",new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"orderNoGenerator",com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"inboundOrders",map(com.baiyue.lastleg.mapper.LastlegInboundOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"outboundOrders",map(OutboundOrderMapper.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(business,"outboundPalletItems",map(OutboundPalletItemMapper.class));
|
||||
|
||||
+6
-3
@@ -22,7 +22,7 @@ class FleetPoolOrderBridgeTest {
|
||||
LocalLooseboardHistoryMapper looseHistory=mock(LocalLooseboardHistoryMapper.class);
|
||||
UsTruckOrderMapper trucks=mock(UsTruckOrderMapper.class);
|
||||
UsTruckOrderStatusHistoryMapper truckHistory=mock(UsTruckOrderStatusHistoryMapper.class);
|
||||
FleetPoolOrderNumberGenerator numbers=mock(FleetPoolOrderNumberGenerator.class);
|
||||
com.baiyue.lastleg.service.LastlegDocumentNumberGenerator numbers=mock(com.baiyue.lastleg.service.LastlegDocumentNumberGenerator.class);
|
||||
FleetPoolDeliveryEvidence evidence=mock(FleetPoolDeliveryEvidence.class);
|
||||
com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker appointmentSources=mock(com.baiyue.lastleg.appointmentplanning.AppointmentSourceLinker.class);
|
||||
FleetPoolOrderBridge service;
|
||||
@@ -30,7 +30,7 @@ class FleetPoolOrderBridgeTest {
|
||||
@BeforeEach void setup(){
|
||||
service=new FleetPoolOrderBridge(new FleetPoolOrderRepository(links,loose,looseHistory,trucks,truckHistory),numbers,new ObjectMapper().findAndRegisterModules(),evidence,appointmentSources);
|
||||
|
||||
when(numbers.next(anyString(),any(),anyLong())).thenReturn("LOCAL-001");
|
||||
when(numbers.next(anyString())).thenReturn("LOCAL-001");
|
||||
when(links.lockBooking(7L)).thenAnswer(a->row);when(links.lockBookingNo("FB-7")).thenAnswer(a->row);
|
||||
when(links.lockIncoming("9","WT-7")).thenAnswer(a->row);
|
||||
when(links.lockDispatch("9","DISPATCH-7")).thenAnswer(a->row==null?List.of():List.of(row));
|
||||
@@ -171,7 +171,10 @@ class FleetPoolOrderBridgeTest {
|
||||
assertThat(method.getAnnotation(Transactional.class).propagation()).isEqualTo(Propagation.MANDATORY);
|
||||
assertThat(Arrays.stream(FleetPoolOrderBridge.class.getDeclaredFields()).map(f->f.getType().getSimpleName()))
|
||||
.noneMatch(name->name.contains("FleetTrip")||name.contains("FleetBooking")||name.contains("Fee")||name.contains("Inventory"));
|
||||
assertThat(Arrays.stream(FleetPoolOrderBridge.class.getDeclaredFields()).map(f->f.getType().getPackageName()))
|
||||
assertThat(Arrays.stream(FleetPoolOrderBridge.class.getDeclaredFields())
|
||||
// 只允许共享的编号基础能力;业务编排服务仍禁止反向依赖。
|
||||
.filter(f -> f.getType() != com.baiyue.lastleg.service.LastlegDocumentNumberGenerator.class)
|
||||
.map(f->f.getType().getPackageName()))
|
||||
.noneMatch(name->name.startsWith("com.baiyue.lastleg.delivery")||name.equals("com.baiyue.lastleg.fleet.trip")
|
||||
||name.equals("com.baiyue.lastleg.service"));
|
||||
}
|
||||
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
package com.baiyue.lastleg.fleet.business;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class FleetPoolOrderNumberGeneratorTest {
|
||||
private final FleetPoolOrderNumberGenerator generator=new FleetPoolOrderNumberGenerator();
|
||||
@Test void existingLooseboardAndTruckFormatLocationAndSuffixArePreserved(){
|
||||
String day=LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||
assertThat(generator.next("LOOSEBOARD","FBA:ONT8",1234567)).isEqualTo("LB-ONT-"+day+"-234567");
|
||||
assertThat(generator.next("TRUCK","nyc",1000001)).isEqualTo("TR-NYC-"+day+"-000001");
|
||||
}
|
||||
@Test void existingPrivateDestinationAndShortCodeRulesArePreserved(){
|
||||
String day=LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
|
||||
assertThat(generator.next("TRUCK","private address",9)).isEqualTo("TR-WH-"+day+"-000009");
|
||||
assertThat(generator.next("LOOSEBOARD","ab12",9)).isEqualTo("LB-AB12-"+day+"-000009");
|
||||
assertThat(generator.next("TRUCK",null,-1)).isEqualTo("TR-WH-"+day+"-999999");
|
||||
}
|
||||
}
|
||||
+4
@@ -30,6 +30,10 @@ class VehicleCareAuthorizationTest {
|
||||
}
|
||||
private void authenticate(String code) { SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("user", "unused", List.of(new SimpleGrantedAuthority(code)))); }
|
||||
@Configuration @EnableMethodSecurity static class Config {
|
||||
/** 为控制器权限测试中的服务替身补齐编号依赖。 */
|
||||
@Bean com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers() {
|
||||
return com.baiyue.lastleg.service.TestDocumentNumbers.documents();
|
||||
}
|
||||
@Bean VehicleCareService service() { return mock(VehicleCareService.class); }
|
||||
@Bean PlatformVehicleCareController controller(VehicleCareService service) { return new PlatformVehicleCareController(service); }
|
||||
}
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ class VehicleCarePersistenceTest {
|
||||
when(files.upload(anyLong(), anyString(), any())).thenReturn(new VehicleCareModels.File("9001", "photo.png", "image/png", 8));
|
||||
var target = new VehicleCareService(session.getMapper(VehicleCareMapper.class), session.getMapper(VehicleCareHistoryMapper.class), vehicles, files,
|
||||
new ObjectMapper().registerModule(new JavaTimeModule()), mock(VehicleCareFinanceService.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(target, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(source), new AnnotationTransactionAttributeSource()));
|
||||
service = (VehicleCareService) proxy.getProxy();
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ class OwnedBusinessScopePersistenceTest {
|
||||
baseline.setup();var reader=mock(DispatchDetailReader.class);
|
||||
when(reader.detail(any(),anyString())).thenAnswer(call->{
|
||||
var s=baseline.repository.source(call.getArgument(1));var r=baseline.codec.read(s.getResourcesJson(),DispatchContracts.Resources.class);
|
||||
return new DispatchViews.Detail(s.getDispatchNo(),DispatchTypes.Status.ACTIVE,DispatchTypes.Stage.valueOf(s.getStage()),s.getRecordVersion(),
|
||||
return new DispatchViews.Detail(s.getDispatchNo(),"DELIVERY",DispatchTypes.Status.ACTIVE,DispatchTypes.Stage.valueOf(s.getStage()),s.getRecordVersion(),
|
||||
DispatchTypes.ReviewState.APPROVED,null,null,true,Map.of(),new DispatchViews.VehicleInfo(r.vehicle(),r.driver()),null,null,List.of());
|
||||
});
|
||||
queries=tx(new OwnedBusinessQueryService(baseline.repository,baseline.projection,baseline.codec,reader));
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ class FleetCapacityOfferServiceTest {
|
||||
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,"",List.of(new SimpleGrantedAuthority(FleetCapacityOfferService.LIST))));
|
||||
service=new FleetCapacityOfferService(offers,mock(FleetCapacityOfferCommandMapper.class),trips,mock(FleetBookingMapper.class),mock(FleetBookingService.class),reservations,
|
||||
execution,mock(FleetCustomerDirectory.class),mock(FleetPoolOrderBridge.class),new ObjectMapper().registerModule(new JavaTimeModule()),Clock.fixed(now,ZoneOffset.UTC));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
}
|
||||
@AfterEach void clear(){SecurityContextHolder.clearContext();}
|
||||
@Test void ordinaryPageOnlyCalculatesRequestedDatabasePageAndPreservesAccurateTotal(){
|
||||
|
||||
+4
@@ -100,6 +100,10 @@ class FleetTripAuthorizationTest {
|
||||
}
|
||||
@Configuration @EnableMethodSecurity
|
||||
static class Config {
|
||||
/** 为控制器权限测试中的服务替身补齐编号依赖。 */
|
||||
@Bean com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers() {
|
||||
return com.baiyue.lastleg.service.TestDocumentNumbers.documents();
|
||||
}
|
||||
/** 既有取货能力依赖原仓储引用;权限测试使用只读替身,不连接数据库。 */
|
||||
@Bean com.baiyue.lastleg.fleet.pickup.FleetPickupRepository pickups() { return mock(com.baiyue.lastleg.fleet.pickup.FleetPickupRepository.class); }
|
||||
@Bean com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges workTripChanges() { return mock(com.baiyue.lastleg.fleet.trip.work.AppointmentWorkTripChanges.class); }
|
||||
|
||||
+4
-4
@@ -359,7 +359,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
new ContainerInboundPlanPreviewResponse.Group("ONE_CLICK_DROPSHIP", "31", "US-LAX-01", "Los Angeles",
|
||||
1, 1, 1, 1, 0, 0, 1, 0, List.of(overseas))), null);
|
||||
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
|
||||
when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class)))
|
||||
when(numbers.next(eq("INBOUND_PLAN")))
|
||||
.thenReturn("IP-FBA", "IP-OVERSEAS");
|
||||
when(plans.sourcePalletNos("100", 11L, "ONE_CLICK_DROPSHIP")).thenReturn(List.of("MSCU6639870-ABE3-001"));
|
||||
when(plans.sourcePalletNos("200", 12L, "ONE_CLICK_DROPSHIP")).thenReturn(List.of("MSCU6639870-US-LAX-01-001"));
|
||||
@@ -392,7 +392,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
List.of(new ContainerInboundPlanPreviewResponse.Group("FIRST_LEG", "31", "US-LAX-01", "Los Angeles",
|
||||
1, 1, 2, 0, 2, 0, 2, 0, List.of(overseas))), null);
|
||||
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
|
||||
when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class))).thenReturn("IP-OVERSEAS");
|
||||
when(numbers.next(eq("INBOUND_PLAN"))).thenReturn("IP-OVERSEAS");
|
||||
when(plans.sourcePalletNos("200", 12L, "FIRST_LEG")).thenReturn(List.of());
|
||||
|
||||
ContainerPlanConversionCommand command = ReflectionTestUtils.invokeMethod(coordinator, "prepareCommand",
|
||||
@@ -424,7 +424,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
List.of(new ContainerInboundPlanPreviewResponse.Group("FBA", null, "ACY2", "Amazon ACY2",
|
||||
1, 1, 3, 2, 1, 0, 3, 0, List.of(order))), null);
|
||||
when(plans.previewContainer(BOOKING_ID)).thenReturn(preview);
|
||||
when(numbers.next(eq("INBOUND_PLAN"), any(String.class), any(Long.class))).thenReturn("IP-ACY2");
|
||||
when(numbers.next(eq("INBOUND_PLAN"))).thenReturn("IP-ACY2");
|
||||
when(plans.sourcePalletNos("100", 11L, "FIRST_LEG")).thenReturn(List.of(
|
||||
"D0004-101-001", "D0004-101-002"));
|
||||
|
||||
@@ -475,7 +475,7 @@ class ContainerInboundPlanConversionCoordinatorTest {
|
||||
|
||||
assertEquals(21L, preparation.items().get(0).planId());
|
||||
assertEquals("IP-MANUAL", preparation.items().get(0).planNo());
|
||||
verify(numbers, never()).next(any(String.class), any(String.class), any(Long.class));
|
||||
verify(numbers, never()).next(any(String.class));
|
||||
}
|
||||
|
||||
private ContainerInboundPlanPreviewResponse.Order previewOrder(String customerId, String orderId,
|
||||
|
||||
+1
@@ -262,6 +262,7 @@ class FbaAppointmentPersistenceTest {
|
||||
@Bean com.baiyue.lastleg.mapper.LastlegOrderMapper orders(){var mock=mock(com.baiyue.lastleg.mapper.LastlegOrderMapper.class);var row=new com.baiyue.lastleg.entity.LastlegOrder();row.setId(100L);row.setCustomerId("C");row.setInboundCode("WT-REAL");when(mock.selectById(100L)).thenReturn(row);return mock;}
|
||||
@Bean AppointmentExternalSourceGateway bookings(){var mock=mock(AppointmentExternalSourceGateway.class);when(mock.resolve(anyList())).thenReturn(List.of(new AppointmentExternalSourceGateway.Source("C","B-1","TR-1","RECEIVED","100","FBA:ONT8",100L,10,2L)));return mock;}
|
||||
@Bean AppointmentSourceLinker linker(com.baiyue.lastleg.mapper.LastlegOrderMapper orders,InboundFbaAppointmentItemMapper items,AppointmentExternalSourceGateway bookings){return new InboundAppointmentSourceLinker(orders,items,bookings);}
|
||||
@Bean com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers() { return com.baiyue.lastleg.service.TestDocumentNumbers.documents(); }
|
||||
@Bean InboundFbaAppointmentService service(){return new InboundFbaAppointmentService();}
|
||||
@Bean com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper workMapper(SqlSessionTemplate session){return session.getMapper(com.baiyue.lastleg.appointmentwork.AppointmentWorkMapper.class);}
|
||||
/** 使用生产持久层,继续验证需求失效与预约写入的同事务行为。 */
|
||||
|
||||
+2
-1
@@ -33,6 +33,7 @@ class InboundFbaAppointmentServiceTest {
|
||||
headers=mock(InboundFbaAppointmentMapper.class);items=mock(InboundFbaAppointmentItemMapper.class);query=mock(InboundFbaAppointmentQueryMapper.class);
|
||||
commands=mock(FbaAppointmentCommandMapper.class);sources=mock(AppointmentSourceService.class);flags=mock(AppointmentBatchPreviewMapper.class);
|
||||
service=new InboundFbaAppointmentService();
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
ReflectionTestUtils.setField(service,"receiving",mock(com.baiyue.lastleg.inboundplan.receiving.FbaReceivingStore.class));
|
||||
ReflectionTestUtils.setField(service,"receivingWork",mock(com.baiyue.lastleg.inboundplan.receiving.FbaAppointmentWorkPort.class));
|
||||
ReflectionTestUtils.setField(service,"workChanges",mock(com.baiyue.lastleg.inboundplan.work.AppointmentWorkAppointmentChanges.class));
|
||||
@@ -95,7 +96,7 @@ class InboundFbaAppointmentServiceTest {
|
||||
}
|
||||
|
||||
@Test void savesCrossCustomerArrangementBeforeAnyInboundPlanOrExternalNumberExists() {
|
||||
var result=create("create");assertTrue(result.appointmentNo().startsWith("AP-"));assertNull(result.confirmationNo());assertNull(result.appointmentAt());
|
||||
var result=create("create");assertTrue(result.appointmentNo().matches("YY[0-9]{12}"));assertNull(result.confirmationNo());assertNull(result.appointmentAt());
|
||||
assertEquals(LocalDate.of(2026,10,15),result.plannedDate());assertEquals(2,result.selectedCount());assertEquals("PENDING",result.appointmentStatus());
|
||||
assertEquals(List.of("A","B"),result.items().stream().map(FbaAppointmentResponse.Item::customerId).toList());verify(query,never()).selectedLines(anyList());
|
||||
}
|
||||
|
||||
+1
-1
@@ -75,7 +75,7 @@ class InboundPlanServiceTest {
|
||||
ReflectionTestUtils.setField(service, "commandMapper", commandMapper);
|
||||
ReflectionTestUtils.setField(service, "sourcePalletMapper", sourcePalletMapper);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", JsonMapper.builder().findAndAddModules().build());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
org.mockito.Mockito.lenient().when(lineMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(palletMapper.selectList(any())).thenReturn(List.of());
|
||||
org.mockito.Mockito.lenient().when(allocationMapper.selectList(any())).thenReturn(List.of());
|
||||
|
||||
+2
-3
@@ -47,7 +47,7 @@ class LocalLooseboardServiceTest {
|
||||
@Mock LocalLooseboardOperationMapper operations;
|
||||
@Mock LastlegOrderMapper lastlegOrders;
|
||||
@Mock LastlegOrderStatusHistoryMapper lastlegHistories;
|
||||
@Spy LastlegOrderNoGenerator lastlegOrderNoGenerator = new LastlegOrderNoGenerator();
|
||||
@Spy LastlegOrderNoGenerator lastlegOrderNoGenerator = com.baiyue.lastleg.service.TestDocumentNumbers.orders();
|
||||
private LocalLooseboardService service;
|
||||
private final Map<String, LocalLooseboardOperation> operationRows = new HashMap<>();
|
||||
|
||||
@@ -64,8 +64,7 @@ class LocalLooseboardServiceTest {
|
||||
ReflectionTestUtils.setField(repository, "lastlegHistories", lastlegHistories);
|
||||
ReflectionTestUtils.setField(service, "lastlegOrderNoGenerator", lastlegOrderNoGenerator);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
org.mockito.Mockito.lenient().when(lastlegOrderNoGenerator.next(anyString(), any(), anyLong()))
|
||||
.thenReturn("LB-WH-20260901-000001");
|
||||
org.mockito.Mockito.lenient().doReturn("LB202610200001").when(lastlegOrderNoGenerator).next(anyString());
|
||||
org.mockito.Mockito.lenient().when(operations.findKey(anyString(), any(), anyString()))
|
||||
.thenAnswer(call -> operationRows.get(call.getArgument(2)));
|
||||
org.mockito.Mockito.lenient().when(operations.insert(any(LocalLooseboardOperation.class))).thenAnswer(call -> {
|
||||
|
||||
+4
@@ -27,6 +27,10 @@ class ProcurementLifecycleAuthorizationTest {
|
||||
private final ProcurementLifecycleService service=context.getBean(ProcurementLifecycleService.class);
|
||||
@Configuration @EnableMethodSecurity(proxyTargetClass=true)
|
||||
static class Config {
|
||||
/** 为控制器权限测试中的服务替身补齐编号依赖。 */
|
||||
@Bean com.baiyue.lastleg.service.LastlegDocumentNumberGenerator documentNumbers() {
|
||||
return com.baiyue.lastleg.service.TestDocumentNumbers.documents();
|
||||
}
|
||||
@Bean ProcurementLifecycleService service(){return mock(ProcurementLifecycleService.class);}
|
||||
@Bean ProcurementLifecycleController controller(ProcurementLifecycleService service){return new ProcurementLifecycleController(service);}
|
||||
@Bean ProcurementPhysicalService physicalService(){return mock(ProcurementPhysicalService.class);}
|
||||
|
||||
+2
-2
@@ -43,7 +43,7 @@ class OverseasProcurementServiceTest {
|
||||
@Mock OverseasProcurementOrderMapper orders;
|
||||
@Mock OverseasProcurementStatusHistoryMapper histories;
|
||||
@Mock ProcurementWarehouseDirectory warehouseDirectory;
|
||||
@Mock ProcurementOrderNumberGenerator orderNoGenerator;
|
||||
@Mock com.baiyue.lastleg.service.LastlegDocumentNumberGenerator orderNoGenerator;
|
||||
@Mock OverseasProcurementFileMapper procurementFiles;
|
||||
@Mock OverseasProcurementFeeMapper procurementFees;
|
||||
@Mock ProcurementFileGateway fileGateway;
|
||||
@@ -62,7 +62,7 @@ class OverseasProcurementServiceTest {
|
||||
ReflectionTestUtils.setField(service, "procurementFiles", procurementFiles);
|
||||
ReflectionTestUtils.setField(service, "procurementFees", procurementFees);
|
||||
ReflectionTestUtils.setField(service, "fileGateway", fileGateway);
|
||||
lenient().when(orderNoGenerator.next(anyString(), any())).thenReturn("ACME202609050001");
|
||||
lenient().when(orderNoGenerator.next(anyString())).thenReturn("ACME202609050001");
|
||||
lenient().when(procurementFiles.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(procurementFees.selectList(any())).thenReturn(List.of());
|
||||
}
|
||||
|
||||
+3
@@ -58,8 +58,11 @@ class ProcurementChargePersistenceTest {
|
||||
when(commands.publish(anyString(),anyLong(),anyString(),anyString(),any(),anyString(),anyLong())).thenAnswer(i->"cmd-"+events.incrementAndGet());
|
||||
when(commands.publishRefund(anyLong(),anyString(),anyString(),any(),anyString(),anyString(),anyLong())).thenAnswer(i->"cmd-"+events.incrementAndGet());
|
||||
var json=new ObjectMapper().findAndRegisterModules();finance=new ProcurementFinanceService(fees,files,orders,converter,sources,commands,jdbc,json);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(finance, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(finance, "feeNumbers", new com.baiyue.lastleg.procurement.repository.ProcurementFeeNumberRepository(fees));
|
||||
lifecycle=new ProcurementLifecycleService(orders,files,mock(ProcurementWarehouseDirectory.class),finance,converter,jdbc,json);
|
||||
charges=new ProcurementChargeService(lifecycle,finance,jdbc);adjustments=new ProcurementAdjustmentService(lifecycle,finance,jdbc);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(adjustments, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
tx.execute(s->finance.confirm("9","PO-1","7",new ProcurementFinanceService.Confirmation(ProcurementFeePurpose.SAMPLE_PAYMENT,"SAMPLE_PAYMENT","货款","ZERO",BigDecimal.ZERO,"CNY",LocalDate.of(2026,9,1),null,"free")));
|
||||
estimateId=tx.execute(s->finance.confirm("9","PO-1","7",new ProcurementFinanceService.Confirmation(ProcurementFeePurpose.INTERNATIONAL_ESTIMATE,"INTERNATIONAL_ESTIMATE","预估","ZERO",BigDecimal.ZERO,"CNY",LocalDate.of(2026,9,1),null,"zero estimate"))).getId();
|
||||
}
|
||||
|
||||
+1
@@ -51,6 +51,7 @@ class ProcurementControlPersistenceTest {
|
||||
when(warehouses.required(92)).thenReturn(new ReceivingWarehouse(92L,"NEW","Correct receiving warehouse","US","ACTIVE"));
|
||||
var json=new ObjectMapper().findAndRegisterModules();lifecycle=new ProcurementLifecycleService(orders,files,warehouses,finance,mock(OverseasFeeCurrencyConverter.class),jdbc,json);
|
||||
service=new ProcurementControlService(lifecycle,finance,warehouses,jdbc,json);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
}
|
||||
@Test void purchasedButUnreceivedGoodsCanCloseOnlyWithAllSupplierDispositionAndCustomerEvidence() {
|
||||
assertThat(service.read("9","PO-1").remaining()).containsExactly(new ProcurementControlService.Remaining("PURCHASE_REMAINDER",null,"PO-1",10));
|
||||
|
||||
+9
-1
@@ -68,6 +68,8 @@ class ProcurementFinancePersistenceTest {
|
||||
when(commands.publishRefund(anyLong(),anyString(),anyString(),any(),anyString(),anyString(),anyLong()))
|
||||
.thenAnswer(i -> "command-"+sequence.incrementAndGet());
|
||||
service=new ProcurementFinanceService(fees,files,orders,converter,sources,commands,jdbc,new ObjectMapper().findAndRegisterModules());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "feeNumbers", new com.baiyue.lastleg.procurement.repository.ProcurementFeeNumberRepository(fees));
|
||||
}
|
||||
|
||||
@Test void actualFeePublishesOnlyAfterSuccessfulConsumption() {
|
||||
@@ -235,7 +237,13 @@ class ProcurementFinancePersistenceTest {
|
||||
handle(result(fees.selectById(fee.getId()),"SETTLE","SUCCEEDED","SETTLED","100"));
|
||||
jdbc.update("INSERT INTO ll_event_outbox VALUES('OVERSEAS_PROCUREMENT_FEE','901','PUBLISHED')");
|
||||
var refund=tx.execute(s -> service.refund("9","PO-1","7",fee.getId(),new BigDecimal("20"),1,"correction","refund-key"));
|
||||
assertThat(tx.execute(s -> service.refund("9","PO-1","7",fee.getId(),new BigDecimal("20"),1,"correction","refund-key")).getId()).isEqualTo(refund.getId());
|
||||
assertThat(refund.getFeeNo()).matches("PF[0-9]{12}");
|
||||
var replay = tx.execute(s -> service.refund("9","PO-1","7",fee.getId(),new BigDecimal("20"),1,"correction","refund-key"));
|
||||
assertThat(replay.getId()).isEqualTo(refund.getId());
|
||||
assertThat(replay.getFeeNo()).isEqualTo(refund.getFeeNo());
|
||||
assertThatThrownBy(() -> tx.execute(s -> service.refund("9","PO-1","7",fee.getId(),new BigDecimal("21"),1,"correction","refund-key")))
|
||||
.hasMessageContaining("退款幂等键对应不同");
|
||||
verify(commands, times(1)).publishRefund(anyLong(),anyString(),anyString(),any(),anyString(),anyString(),anyLong());
|
||||
assertThatThrownBy(() -> tx.execute(s -> service.refund("9","PO-1","7",fee.getId(),new BigDecimal("20"),1,"correction","another-key")))
|
||||
.hasMessageContaining("未完成的调整或退款");
|
||||
handle(new WalletCommandCompletedV1(refund.getWalletCommandEventId(),"REFUND",9,"PROCUREMENT_FEE",refund.getFeeNo(),
|
||||
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
package com.baiyue.lastleg.procurement.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ProcurementOrderNumberGeneratorTest {
|
||||
@Test void convertsChineseCompanyNameToInitials() {
|
||||
assertThat(ProcurementOrderNumberGenerator.initials("杭州积乐贸易有限公司")).isEqualTo("HZJLMYYX");
|
||||
}
|
||||
@Test void keepsOneInitialPerLatinWordAndUsesFallback() {
|
||||
assertThat(ProcurementOrderNumberGenerator.initials("Great Ocean Trading LLC")).isEqualTo("GOTL");
|
||||
assertThat(ProcurementOrderNumberGenerator.initials(" ")).isEqualTo("KH");
|
||||
}
|
||||
@Test void limitsInitialsToEightCharacters() {
|
||||
assertThat(ProcurementOrderNumberGenerator.initials("一二三四五六七八九十")).hasSize(8);
|
||||
}
|
||||
}
|
||||
+1
@@ -55,6 +55,7 @@ class ProcurementPhysicalPersistenceTest {
|
||||
when(finance.current(11,ProcurementFeePurpose.SAMPLE_PAYMENT,"SAMPLE_PAYMENT")).thenReturn(payment);when(finance.sourceLinked(payment)).thenReturn(true);
|
||||
var lifecycle=new ProcurementLifecycleService(orders,files,warehouses,finance,mock(OverseasFeeCurrencyConverter.class),jdbc,new ObjectMapper().findAndRegisterModules());
|
||||
service=new ProcurementPhysicalService(lifecycle,warehouses,finance,jdbc);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
}
|
||||
|
||||
@Test void splitReceiptsAndCartonSourcesRemainConservedWithoutWms() {
|
||||
|
||||
+2
@@ -24,7 +24,9 @@ class ProcurementQueryServiceTest {
|
||||
var m=Pattern.compile("CREATE TABLE IF NOT EXISTS "+name+" \\(.*?\\) ENGINE=InnoDB[^;]*;",Pattern.DOTALL).matcher(schema);assertThat(m.find()).isTrue();jdbc.execute(m.group().replaceFirst("\\) ENGINE=InnoDB[^;]*;",");"));
|
||||
}
|
||||
var physical=new ProcurementPhysicalService(fixture.lifecycle,fixture.warehouses,fixture.finance,jdbc);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(physical, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
transport=new ProcurementTransportService(fixture.lifecycle,physical,fixture.finance,fixture.warehouses,jdbc,json);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(transport, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
base=mock(OverseasProcurementService.class);
|
||||
when(base.detail("9","PO-1")).thenReturn(new OverseasProcurementResponses.Detail(null,null,null,null,null,null,"internal remark","operator","reviewer","internal review",null,List.of(),List.of(new OverseasProcurementResponses.FileInfo(1L,"EXCEPTION",1001L,"private.pdf","application/pdf",10,null)),List.of()));
|
||||
query=new ProcurementQueryService(fixture.lifecycle,physical,transport,fixture.finance,mock(ProcurementAdjustmentService.class),fixture.service,base,jdbc);
|
||||
|
||||
+2
@@ -68,7 +68,9 @@ class ProcurementTransportPersistenceTest {
|
||||
var json=new ObjectMapper().findAndRegisterModules();
|
||||
var lifecycle=new ProcurementLifecycleService(orders,files,warehouses,finance,mock(OverseasFeeCurrencyConverter.class),jdbc,json);
|
||||
var physical=new ProcurementPhysicalService(lifecycle,warehouses,finance,jdbc);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(physical, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
service=new ProcurementTransportService(lifecycle,physical,finance,warehouses,jdbc,json);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service, "documentNumbers", com.baiyue.lastleg.service.TestDocumentNumbers.documents());
|
||||
}
|
||||
@Test void entireBatchShipsOnceWithFrozenAddressLabelsAndSources() {
|
||||
prepare();var request=shipRequest(Leg.OVERSEAS,"ship");
|
||||
|
||||
+4
-4
@@ -208,17 +208,17 @@ class RelabelApplicationServiceTest {
|
||||
@Mock OverseasFeeSourceService feeSources;
|
||||
@Mock ReturnOrderMapper returnOrders;
|
||||
@Mock com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper pdaTasks;
|
||||
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
|
||||
@Spy LastlegOrderNoGenerator orderNoGenerator = com.baiyue.lastleg.service.TestDocumentNumbers.orders();
|
||||
|
||||
private RelabelApplicationService service;
|
||||
|
||||
private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.CompactBusinessNumberService.class);
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.LastlegDocumentNumberGenerator.class);
|
||||
service = new RelabelApplicationService();
|
||||
ReflectionTestUtils.setField(service, "compactNumbers", compactNumbers);
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
|
||||
ReflectionTestUtils.setField(service, "bindingGuard", bindingGuard);
|
||||
ReflectionTestUtils.setField(service, "returnPackingTraces", org.mockito.Mockito.mock(com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.class));
|
||||
ReflectionTestUtils.setField(service, "skuConversions", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.ReturnSkuConversionService.class));
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ class ReturnRelabelDeliveryTest {
|
||||
when(customers.find("C1")).thenReturn(new FleetCustomerDirectory.Customer("C1", "C", "测试客户", null, null));
|
||||
when(warehouses.required(100L, "OVERSEAS")).thenReturn(new DriverModels.WarehouseOption("100", "WH", "洛杉矶测试仓", "OVERSEAS"));
|
||||
ReflectionTestUtils.setField(service, "outboundTasks", mock(OutboundPdaTaskMapper.class));
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", new LastlegOrderNoGenerator());
|
||||
ReflectionTestUtils.setField(service, "orderNoGenerator", com.baiyue.lastleg.service.TestDocumentNumbers.orders());
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper());
|
||||
var application = new RelabelApplication(); application.setId(1L); application.setCustomerId("C1");
|
||||
application.setRelabelNo("REL1"); application.setOverseasWarehouseId("100"); application.setDeliveryMethod(deliveryMethod);
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package com.baiyue.lastleg.repository;
|
||||
|
||||
import com.baiyue.lastleg.mapper.DocumentSequenceMapper;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mybatis.spring.SqlSessionTemplate;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.*;
|
||||
import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource;
|
||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** 实际 Mapper 和隔离 H2 MySQL 模式验证原子序列,不访问部署环境。 */
|
||||
class DocumentSequenceRepositoryTest {
|
||||
private static class Fixture {
|
||||
final JdbcTemplate jdbc;
|
||||
final DataSourceTransactionManager transactions;
|
||||
final DocumentSequenceMapper mapper;
|
||||
final DocumentSequenceRepository repository;
|
||||
/** 仅测试造表允许 JDBC;被测路径全部使用 Mapper。 */
|
||||
Fixture() throws Exception {
|
||||
var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=10000","sa","");
|
||||
jdbc=new JdbcTemplate(data);transactions=new DataSourceTransactionManager(data);
|
||||
jdbc.execute("CREATE TABLE ll_document_sequence(sequence_key VARCHAR(32) PRIMARY KEY,current_value BIGINT NOT NULL)");
|
||||
var configuration=new MybatisConfiguration();configuration.addMapper(DocumentSequenceMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(configuration);factory.afterPropertiesSet();
|
||||
mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DocumentSequenceMapper.class);
|
||||
var proxy=new ProxyFactory(new DocumentSequenceRepository(mapper));proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(transactions,new AnnotationTransactionAttributeSource()));
|
||||
repository=(DocumentSequenceRepository)proxy.getProxy();
|
||||
}
|
||||
}
|
||||
/** 日期和前缀各自独立,重复分配持续递增。 */
|
||||
@Test void dateAndPrefixHaveIndependentSequences() throws Exception {
|
||||
var f=new Fixture();
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(1);
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(2);
|
||||
assertThat(f.repository.next("BB:20261020")).isEqualTo(1);
|
||||
assertThat(f.repository.next("AA:20261021")).isEqualTo(1);
|
||||
}
|
||||
/** 两个线程使用真实事务连接并发争用,不能产生重号。 */
|
||||
@Test void concurrentConnectionsAllocateUniqueNumbers() throws Exception {
|
||||
var f=new Fixture();var pool=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var work=new ArrayList<Callable<Long>>();
|
||||
for(int i=0;i<64;i++)work.add(()->f.repository.next("AA:20261020"));
|
||||
var values=new TreeSet<Long>();
|
||||
for(var future:pool.invokeAll(work))values.add(future.get());
|
||||
assertThat(values).hasSize(64);assertThat(values.first()).isEqualTo(1L);assertThat(values.last()).isEqualTo(64L);
|
||||
} finally {pool.shutdownNow();}
|
||||
}
|
||||
/** 外层建单失败后不能把已经分配的单号再交给其他单据。 */
|
||||
@Test void businessRollbackDoesNotReuseAllocatedNumber() throws Exception {
|
||||
var f=new Fixture();
|
||||
assertThatThrownBy(()->new TransactionTemplate(f.transactions).execute(status->{
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(1);
|
||||
throw new IllegalStateException("业务回滚");
|
||||
})).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(f.repository.next("AA:20261020")).isEqualTo(2);
|
||||
}
|
||||
/** 同一会话中连接值变化后,读取必须绕过 MyBatis 缓存。 */
|
||||
@Test void connectionResultIsNeverCached() throws Exception {
|
||||
var f=new Fixture();
|
||||
new TransactionTemplate(f.transactions).executeWithoutResult(status->{
|
||||
f.jdbc.queryForObject("SELECT LAST_INSERT_ID(7)",Long.class);
|
||||
assertThat(f.mapper.allocatedValue()).isEqualTo(7L);
|
||||
f.jdbc.queryForObject("SELECT LAST_INSERT_ID(37)",Long.class);
|
||||
assertThat(f.mapper.allocatedValue()).isEqualTo(37L);
|
||||
});
|
||||
}
|
||||
}
|
||||
+11
-8
@@ -281,7 +281,7 @@ class ReturnInventoryServiceTest {
|
||||
receipt.setPalletCode("PAL-RETURN-1");
|
||||
receipt.setUnitType("PALLET");
|
||||
receipt.setUnitCode("PAL-RETURN-1");
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-1");
|
||||
|
||||
|
||||
service.createReceiptCarton(order, receipt, null,
|
||||
List.of(new ReturnInventoryService.ReceiptLine("SKU-A", "商品A", 2)));
|
||||
@@ -339,7 +339,7 @@ class ReturnInventoryServiceTest {
|
||||
when(cartons.listReceipt(21L)).thenReturn(List.of(source));
|
||||
when(cartons.lockMark("RC-SOURCE")).thenReturn(source);
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(skuA, skuB));
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-MERGED");
|
||||
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
|
||||
service.merge(new PdaWarehouseIdentity("operator-1", "操作员", 42L),
|
||||
@@ -382,13 +382,16 @@ class ReturnInventoryServiceTest {
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 6L)));
|
||||
when(packingTrace.ownership("10001", "RC-SOURCE", "SKU-A")).thenReturn(List.of(
|
||||
new com.baiyue.lastleg.returnreceipt.mapper.ReturnPackingTraceMapper.Ownership("RT-1", 3L)));
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
|
||||
service.merge(new PdaWarehouseIdentity("operator-2", "操作员2", 42L), mergeRequest(quantity));
|
||||
|
||||
var target = ArgumentCaptor.forClass(ReturnInventoryCarton.class);
|
||||
verify(cartons).insert(target.capture());
|
||||
assertThat(target.getValue().getCartonMark()).isEqualTo(LastlegOrderNoGenerator.compactCartonMark(target.getValue().getId()));
|
||||
verify(packingTrace).insert(anyLong(), eq("10001"), anyLong(), eq("RC-SOURCE"),
|
||||
eq("RC-TARGET"), eq("RT-1"), eq("SKU-A"), eq(quantity), any());
|
||||
eq(target.getValue().getCartonMark()), eq("RT-1"), eq("SKU-A"), eq(quantity), any());
|
||||
verify(packingTrace, org.mockito.Mockito.times(1)).insert(anyLong(), anyString(), anyLong(),
|
||||
anyString(), anyString(), anyString(), anyString(), anyLong(), any());
|
||||
assertThat(source.getTotalQuantity()).isEqualTo(6L - quantity);
|
||||
@@ -432,7 +435,7 @@ class ReturnInventoryServiceTest {
|
||||
void fullyConsumedParcelSourceIsMergedAndCreatesOnePhysicalCarton() {
|
||||
ReturnInventoryCarton locked = prepareConcurrentSource();
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
service.merge(new PdaWarehouseIdentity("operator-2", "操作员2", 42L), mergeRequest(2));
|
||||
assertThat(locked.getTotalQuantity()).isZero();
|
||||
@@ -457,14 +460,14 @@ class ReturnInventoryServiceTest {
|
||||
var source = prepareConcurrentSource();
|
||||
when(cartonItems.lockCarton(31L)).thenReturn(List.of(item(41L, "SKU-A", 2L)));
|
||||
when(cartons.completedSkuQuantity(31L, "SKU-A")).thenReturn(2L);
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("RC-TARGET");
|
||||
|
||||
when(balances.adjustCartonQuantity(anyString(), anyString(), anyString(), anyLong())).thenReturn(1);
|
||||
var relabel = org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.mapper.RelabelItemMapper.class);
|
||||
ReflectionTestUtils.setField(service, "relabelItems", relabel);
|
||||
service.merge(new PdaWarehouseIdentity("operator-2", "name", 42L), mergeRequest(2));
|
||||
var target = ArgumentCaptor.forClass(ReturnInventoryCarton.class);
|
||||
verify(cartons).insert(target.capture());
|
||||
verify(relabel).moveCompletedReturnCarton("10001", 31L, target.getValue().getId(), "RC-TARGET");
|
||||
verify(relabel).moveCompletedReturnCarton("10001", 31L, target.getValue().getId(), target.getValue().getCartonMark());
|
||||
assertThat(source.getTotalQuantity()).isZero();
|
||||
}
|
||||
|
||||
@@ -579,7 +582,7 @@ class ReturnInventoryServiceTest {
|
||||
when(cartonItems.lockSku(31L, "SKU-A")).thenReturn(line);
|
||||
when(balances.reserveAvailable("10001", "42", "SKU-A", 2)).thenReturn(1);
|
||||
when(balances.releaseAvailable("10001", "42", "SKU-A", 2)).thenReturn(1);
|
||||
when(numbers.next(anyString(), anyString(), anyLong())).thenReturn("PLAN-1");
|
||||
when(numbers.next(anyString())).thenReturn("PLAN-1");
|
||||
var request = new ReturnInventoryRequests.PackingPlanCreate(List.of("RT-1"), "SKU",
|
||||
List.of(new ReturnInventoryRequests.PlanBox(List.of(new ReturnInventoryRequests.PlanItem("SKU-A", 2)), null, null, null, null)), null, "plan-key");
|
||||
var created = service.createPackingPlan("platform", request);
|
||||
|
||||
+9
-9
@@ -131,13 +131,13 @@ class ReturnReceiptServiceTest {
|
||||
ReturnReceiptService service;
|
||||
PdaWarehouseIdentity identity;
|
||||
|
||||
private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.CompactBusinessNumberService.class);
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.LastlegDocumentNumberGenerator.class);
|
||||
service = new ReturnReceiptService();
|
||||
ReflectionTestUtils.setField(service, "compactNumbers", compactNumbers);
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "202610200001");
|
||||
ReflectionTestUtils.setField(service, "orders", orders);
|
||||
ReflectionTestUtils.setField(service, "orderItems", orderItems);
|
||||
ReflectionTestUtils.setField(service, "receipts", receipts);
|
||||
@@ -257,7 +257,7 @@ class ReturnReceiptServiceTest {
|
||||
when(orderItems.lockOrder(1L)).thenReturn(List.of(item(11L, "SKU-A", 50, 50), item(12L, "SKU-B", 40, 40)));
|
||||
when(fbaWarehouseValidator.required("200", "XLX6", null)).thenReturn(
|
||||
new FbaWarehouseValidator.VerifiedFbaWarehouse(200L, "XLX6", "FBA XLX6", "US", "CA"));
|
||||
when(compactNumbers.next(eq("TT"), eq("customer-1"))).thenReturn("RL-MULTI");
|
||||
when(compactNumbers.next(eq("THHB"))).thenReturn("RL-MULTI");
|
||||
when(lastlegOrders.selectById(99L)).thenReturn(tail);
|
||||
when(cartons.findAvailable("customer-1", "10")).thenReturn(List.of(first, second));
|
||||
when(inventory.matchesReturnSource(any(), eq("RT-MULTI"))).thenCallRealMethod();
|
||||
@@ -310,7 +310,7 @@ class ReturnReceiptServiceTest {
|
||||
when(orderItems.lockOrder(1L)).thenReturn(List.of(item(11L, "SKU-A", 100, 100)));
|
||||
when(fbaWarehouseValidator.required("200", "XLX6", null)).thenReturn(
|
||||
new FbaWarehouseValidator.VerifiedFbaWarehouse(200L, "XLX6", "FBA XLX6", "US", "CA"));
|
||||
when(compactNumbers.next(eq("TT"), eq("customer-1"))).thenReturn("RL-SECOND");
|
||||
when(compactNumbers.next(eq("THHB"))).thenReturn("RL-SECOND");
|
||||
when(lastlegOrders.selectById(99L)).thenReturn(tail);
|
||||
when(cartons.findAvailable("customer-1", "10")).thenReturn(List.of(carton));
|
||||
when(inventory.matchesReturnSource(any(), eq("RT-MULTI"))).thenCallRealMethod();
|
||||
@@ -365,7 +365,7 @@ class ReturnReceiptServiceTest {
|
||||
void createForecastPersistsCompleteTailOrderSourceAndReturnsItsBusinessNumber() {
|
||||
AtomicReference<ReturnOrder> saved = new AtomicReference<>();
|
||||
when(orders.findKey("customer-1", "create-key")).thenReturn(null);
|
||||
when(compactNumbers.next(eq("TT"), eq("customer-1"))).thenReturn("RT-1");
|
||||
when(compactNumbers.next(eq("TH"))).thenReturn("RT-1");
|
||||
when(orders.insert(any(ReturnOrder.class))).thenAnswer(invocation -> { saved.set(invocation.getArgument(0)); return 1; });
|
||||
when(orders.findNo("customer-1", "RT-1")).thenAnswer(invocation -> saved.get());
|
||||
when(orderItems.listOrder(anyLong())).thenReturn(List.of());
|
||||
@@ -389,7 +389,7 @@ class ReturnReceiptServiceTest {
|
||||
assertEquals("商品A", cargo.getValue().getProductNameZh());
|
||||
assertEquals(2, cargo.getValue().getProductQuantity());
|
||||
assertEquals(0, cargo.getValue().getPackageCount());
|
||||
verify(numbers, never()).next(eq("RETURN_RELABEL"), anyString(), anyLong());
|
||||
verify(numbers, never()).next(eq("RETURN_RELABEL"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -559,8 +559,8 @@ class ReturnReceiptServiceTest {
|
||||
when(orders.lockNo("RT-1")).thenReturn(order);
|
||||
when(orderItems.lockOrder(1L)).thenReturn(items);
|
||||
when(orderItems.listOrder(1L)).thenReturn(items);
|
||||
when(numbers.next(eq("RETURN_RECEIPT"), eq("10"), anyLong())).thenReturn("RR-1");
|
||||
when(numbers.next(eq("RETURN_BATCH"), eq("10"), anyLong())).thenReturn("RB-1");
|
||||
when(numbers.next(eq("RETURN_RECEIPT"))).thenReturn("RR-1");
|
||||
when(numbers.next(eq("RETURN_BATCH"))).thenReturn("RB-1");
|
||||
ReturnInventoryCarton carton = new ReturnInventoryCarton();
|
||||
carton.setCartonMark(cartonMark);
|
||||
when(inventory.createReceiptCarton(eq(order), any(ReturnReceipt.class), any(), any())).thenReturn(carton);
|
||||
|
||||
+55
-1
@@ -344,6 +344,60 @@ class DashboardSqlSemanticTest {
|
||||
assertTrue(rendered.endsWith(finalSelect));rendered=rendered.substring(0,rendered.length()-finalSelect.length())
|
||||
+"SELECT customer_code,sku_code,bucket_code FROM classified WHERE bucket_code IS NOT NULL";
|
||||
}
|
||||
String id="dashboard.literal."+(++sequence);config.addMappedStatement(new MappedStatement.Builder(config,id,new org.apache.ibatis.builder.StaticSqlSource(config,rendered),SqlCommandType.SELECT).resultMaps(original.getResultMaps()).build());return session.selectList(id);
|
||||
// H2 会指数展开重复引用的 CTE;沿用周转测试的逐关系物化方式,
|
||||
// 保留生产查询表达式和结果映射,不据此推定 MySQL 的执行计划。
|
||||
var relations = new ArrayList<String>();
|
||||
try {
|
||||
String finalSql = materializeRelations(rendered, relations);
|
||||
String id="dashboard.literal."+(++sequence);
|
||||
config.addMappedStatement(new MappedStatement.Builder(config,id,
|
||||
new org.apache.ibatis.builder.StaticSqlSource(config,finalSql),SqlCommandType.SELECT)
|
||||
.resultMaps(original.getResultMaps()).build());
|
||||
return session.selectList(id);
|
||||
} catch (SQLException error) {
|
||||
throw new AssertionError("本地统计 SQL 测试关系物化失败", error);
|
||||
} finally {
|
||||
Collections.reverse(relations);
|
||||
for (String name : relations) {
|
||||
try (var statement = session.getConnection().createStatement()) {
|
||||
statement.execute("DROP TABLE " + name);
|
||||
} catch (SQLException error) {
|
||||
throw new AssertionError("本地统计 SQL 测试关系释放失败:" + name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 只物化实际生产 CTE 的查询内容,临时表局限在当前隔离 H2 会话。 */
|
||||
private String materializeRelations(String sql, List<String> relations) throws SQLException {
|
||||
if (!sql.startsWith("WITH ")) return sql;
|
||||
int cursor = 5;
|
||||
while (true) {
|
||||
int as = sql.indexOf(" AS (", cursor);
|
||||
assertTrue(as >= cursor, "生产 CTE 应使用具名 AS 查询");
|
||||
String name = sql.substring(cursor, as).trim();
|
||||
assertTrue(name.matches("[A-Za-z_][A-Za-z0-9_]*"), "测试临时关系必须是固定 SQL 标识");
|
||||
int start = as + 5, end = start, depth = 1;
|
||||
boolean quoted = false;
|
||||
while (depth > 0) {
|
||||
char ch = sql.charAt(end++);
|
||||
if (ch == '\'') {
|
||||
if (quoted && end < sql.length() && sql.charAt(end) == '\'') { end++; continue; }
|
||||
quoted = !quoted;
|
||||
} else if (!quoted) {
|
||||
if (ch == '(') depth++;
|
||||
if (ch == ')') depth--;
|
||||
}
|
||||
}
|
||||
try (var statement = session.getConnection().createStatement()) {
|
||||
statement.execute("CREATE LOCAL TEMPORARY TABLE " + name + " AS " + sql.substring(start, end - 1));
|
||||
}
|
||||
relations.add(name);
|
||||
cursor = end;
|
||||
while (cursor < sql.length() && Character.isWhitespace(sql.charAt(cursor))) cursor++;
|
||||
if (sql.charAt(cursor) != ',') return sql.substring(cursor);
|
||||
cursor++;
|
||||
while (cursor < sql.length() && Character.isWhitespace(sql.charAt(cursor))) cursor++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import java.time.Instant;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class CompactBusinessNumberServiceTest {
|
||||
@Test void usesCustomerInitialsAndShanghaiSecondsWithoutSequenceSuffix() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
var customers = mock(ReturnOrderReferenceResolver.class);
|
||||
when(customers.customerName("42")).thenReturn("柏悦物流");
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class))
|
||||
.thenAnswer(call -> Instant.now().getEpochSecond());
|
||||
String number = new CompactBusinessNumberService(jdbc, customers).next("TB", "42");
|
||||
assertTrue(number.matches("TB-BYWL-[0-9]{12}"), number);
|
||||
assertEquals("AB", CompactBusinessNumberService.initials("Alpha Beta"));
|
||||
}
|
||||
|
||||
@Test void usesDatabaseLogicalSecondAcrossInstancesAndClockRollback() {
|
||||
JdbcTemplate jdbc = mock(JdbcTemplate.class);
|
||||
var first = new CompactBusinessNumberService(jdbc, null);
|
||||
var second = new CompactBusinessNumberService(jdbc, null);
|
||||
Instant now = Instant.parse("2026-10-16T02:30:00Z");
|
||||
when(jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class))
|
||||
.thenReturn(now.getEpochSecond(), now.getEpochSecond() + 1, now.getEpochSecond() + 2);
|
||||
assertEquals("ZY-ABC-261016103000", first.allocate("ZY", "ABC", now));
|
||||
assertEquals("ZY-ABC-261016103001", second.allocate("ZY", "ABC", now));
|
||||
assertEquals("ZY-ABC-261016103002", first.allocate("ZY", "ABC", now.minusSeconds(60)));
|
||||
}
|
||||
|
||||
@Test void physicalLabelKeepsEveryIdBitAndIsCompact() {
|
||||
long id = 2087379674555461633L;
|
||||
String mark = LastlegOrderNoGenerator.compactCartonMark(id);
|
||||
assertTrue(mark.length() <= 16);
|
||||
assertEquals(id, Long.parseLong(mark.substring(3), 36));
|
||||
assertNotEquals(mark, LastlegOrderNoGenerator.compactCartonMark(id + 1_000_000));
|
||||
assertEquals(mark, new LastlegOrderNoGenerator().next("RETURN_CARTON", "92", id));
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.repository.DocumentSequenceRepository;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import java.time.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class LastlegDocumentNumberGeneratorTest {
|
||||
private static final Clock CLOCK=Clock.fixed(Instant.parse("2026-10-19T16:00:00Z"),ZoneOffset.UTC);
|
||||
|
||||
/** 多构造器情况下正式依赖可以被 Spring 正确选择。 */
|
||||
@Test void springCreatesGeneratorWithRepository() {
|
||||
try(var context=new AnnotationConfigApplicationContext()) {
|
||||
context.registerBean(DocumentSequenceRepository.class,()->mock(DocumentSequenceRepository.class));
|
||||
context.register(LastlegDocumentNumberGenerator.class);context.refresh();
|
||||
assertThat(context.getBean(LastlegDocumentNumberGenerator.class)).isNotNull();
|
||||
}
|
||||
}
|
||||
/** 主机使用 UTC 时仍按北京时间跨午夜,且四位流水不截断。 */
|
||||
@Test void usesShanghaiDateAndIndependentSequenceKey() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
when(sequences.next("YY:20261020")).thenReturn(1L,2L,9999L);
|
||||
var numbers=new LastlegDocumentNumberGenerator(sequences,CLOCK);
|
||||
assertThat(numbers.next("YY")).isEqualTo("YY202610200001");
|
||||
assertThat(numbers.next("YY")).isEqualTo("YY202610200002");
|
||||
assertThat(numbers.next("YY")).isEqualTo("YY202610209999");
|
||||
verify(sequences,times(3)).next("YY:20261020");
|
||||
}
|
||||
/** 未登记类型不能消耗流水,分配异常或溢出时拒绝出号。 */
|
||||
@Test void rejectsUnknownTypeAndOverflow() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
var numbers=new LastlegDocumentNumberGenerator(sequences,CLOCK);
|
||||
assertThatThrownBy(()->numbers.next("ZZZZ")).isInstanceOf(IllegalArgumentException.class);
|
||||
verifyNoInteractions(sequences);
|
||||
when(sequences.next("YY:20261020")).thenReturn(10000L);
|
||||
assertThatThrownBy(()->numbers.next("YY")).isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
+22
-86
@@ -1,94 +1,30 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.mapper.LastlegOrderNoDailySequenceMapper;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import java.util.Map;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class LastlegOrderNoGeneratorTest {
|
||||
@Mock LastlegOrderNoDailySequenceMapper sequenceMapper;
|
||||
private LastlegOrderNoGenerator generator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
generator = new LastlegOrderNoGenerator();
|
||||
ReflectionTestUtils.setField(generator, "sequenceMapper", sequenceMapper);
|
||||
/** 各作业类型使用不同前缀,同类型第二次调用必须递增。 */
|
||||
@Test void routesEveryOperationalTypeToItsDailySequence() {
|
||||
var generator=TestDocumentNumbers.orders();
|
||||
Map.ofEntries(Map.entry("OUTBOUND","CK"),Map.entry("ONE_CLICK_DROPSHIP","DFCK"),
|
||||
Map.entry("RELABEL_OUTBOUND","HBCK"),Map.entry("RETURN_OUTBOUND","THCK"),
|
||||
Map.entry("DELIVERY","DL"),Map.entry("TRUCK","TR"),Map.entry("LOOSEBOARD","LB"),
|
||||
Map.entry("EXCEPTION","EX"),Map.entry("DISCARD","CD"),Map.entry("INBOUND_PLAN","IP"),
|
||||
Map.entry("RETURN_RECEIPT","RR"),Map.entry("RETURN_BATCH","RB"),Map.entry("RETURN_PACKING_PLAN","HX"))
|
||||
.forEach((type,prefix)->{
|
||||
assertEquals(prefix+"202610200001",generator.next(type));
|
||||
assertEquals(prefix+"202610200002",generator.next(type));
|
||||
});
|
||||
assertThrows(IllegalArgumentException.class,()->generator.next("UNKNOWN"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usesShanghaiBusinessDateAndDatabaseAllocatedSequence() {
|
||||
LocalDate date = LocalDate.of(2026, 8, 27);
|
||||
when(sequenceMapper.currentValue()).thenReturn(1L, 2L);
|
||||
|
||||
assertEquals("LMO-260827-0001", generator.next(date));
|
||||
assertEquals("LMO-260827-0002", generator.next(date));
|
||||
verify(sequenceMapper, times(2)).nextValue(date);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mapperUsesOneAtomicDatabaseOperationForConcurrentInstances() throws Exception {
|
||||
Insert insert = LastlegOrderNoDailySequenceMapper.class
|
||||
.getMethod("nextValue", LocalDate.class).getAnnotation(Insert.class);
|
||||
|
||||
assertTrue(insert.value()[0].contains("ON DUPLICATE KEY UPDATE"));
|
||||
assertTrue(insert.value()[0].contains("LAST_INSERT_ID(last_sequence+1)"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void crossDayAllocationUsesTheNewShanghaiBusinessDateSequence() {
|
||||
LocalDate firstDay = LocalDate.of(2026, 8, 27);
|
||||
LocalDate nextDay = firstDay.plusDays(1);
|
||||
when(sequenceMapper.currentValue()).thenReturn(42L, 1L);
|
||||
|
||||
assertEquals("LMO-260827-0042", generator.next(firstDay));
|
||||
assertEquals("LMO-260828-0001", generator.next(nextDay));
|
||||
verify(sequenceMapper).nextValue(firstDay);
|
||||
verify(sequenceMapper).nextValue(nextDay);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsDailySequenceBeyondFourDigits() {
|
||||
when(sequenceMapper.currentValue()).thenReturn(10_000L);
|
||||
assertThrows(IllegalStateException.class, () -> generator.next(LocalDate.of(2026, 8, 27)));
|
||||
}
|
||||
|
||||
@Test void actualContainerNumberIsStableAndShared() {
|
||||
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
|
||||
assertTrue(generator.forContainerNo("2087379674555461633").matches("FL-WH-\\d{8}-\\d{6}"));
|
||||
assertEquals(generator.forContainerNo("MSCU1"), new LastlegOrderNoGenerator().forContainerNo("MSCU1"));
|
||||
assertNotEquals(generator.forContainerNo("MSCU1"), generator.forContainerNo("CAB1"));
|
||||
assertTrue(generator.forContainerNo("MSCU1234567890").length() <= 32);
|
||||
assertThrows(IllegalArgumentException.class, () -> generator.forContainerNo(" "));
|
||||
}
|
||||
|
||||
@Test void formatsBusinessWarehouseAndDate() {
|
||||
String value = new LastlegOrderNoGenerator().next("OUTBOUND", "warehouse-LAX-01", 123L);
|
||||
assertTrue(value.matches("OB-LAX-\\d{8}-000123"), value);
|
||||
assertTrue(value.length() <= 24);
|
||||
}
|
||||
|
||||
@Test void transferNumbersDoNotCollideWhenDatabaseIdSuffixesRepeat() {
|
||||
String first = generator.next("TRANSFER", "warehouse-LAX-01", 123L);
|
||||
String second = generator.next("TRANSFER", "warehouse-LAX-01", 1_000_123L);
|
||||
String repeatedId = generator.next("WAREHOUSE_TRANSFER", "warehouse-LAX-01", 123L);
|
||||
assertTrue(first.matches("WT-[0-9A-Z]{25}"), first);
|
||||
assertTrue(first.length() <= 32);
|
||||
assertNotEquals(first, second);
|
||||
assertNotEquals(first, repeatedId);
|
||||
/** 头程来源尾程号未纳入独立改号,保持同柜分组关系。 */
|
||||
@Test void actualContainerNumberRemainsStableAndShared() {
|
||||
var generator=TestDocumentNumbers.orders();
|
||||
assertTrue(generator.forContainerNo("MSCU6639870").matches("FL-WH-\\d{8}-\\d{6}"));
|
||||
assertEquals(generator.forContainerNo("MSCU6639870"),TestDocumentNumbers.orders().forContainerNo("MSCU6639870"));
|
||||
assertNotEquals(generator.forContainerNo("MSCU6639870"),generator.forContainerNo("CSQU3054383"));
|
||||
assertThrows(IllegalArgumentException.class,()->generator.forContainerNo(" "));
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,10 +3,10 @@ import com.baiyue.common.event.*; import com.baiyue.lastleg.entity.*; import com
|
||||
@ExtendWith(org.mockito.junit.jupiter.MockitoExtension.class)
|
||||
class LoadingCompletedConsumerTest {
|
||||
private final Map<String,String> frozenContainerNos=new HashMap<>();
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper loadingEvidence; @Mock LastlegOrderNoDailySequenceMapper sequences; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"loadingEvidence",loadingEvidence); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(); ReflectionTestUtils.setField(generator,"sequenceMapper",sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().when(sequences.currentValue()).thenReturn(1L); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(2); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
|
||||
@Mock LastlegEventInboxMapper inbox; @Mock LastlegOrderMapper orders; @Mock LastlegInboundOrderMapper inboundOrders; @Mock LastlegOrderDetailMapper details; @Mock LastlegOrderStatusHistoryMapper histories; @Mock InboundPlanCartonMapper cartons; @Mock DropshipOrderService dropshipOrders; @Mock InventoryCommandService inventory; @Mock com.baiyue.lastleg.shipping.mapper.ContainerShippingGateMapper shippingGates; @Mock com.baiyue.lastleg.inboundplan.pda.mapper.ContainerLoadingEvidenceMapper loadingEvidence; @Mock LastlegDocumentNumberGenerator sequences; ObjectMapper json=new ObjectMapper().findAndRegisterModules(); LoadingCompletedConsumer service;
|
||||
@BeforeEach void setup(){ service=new LoadingCompletedConsumer(); ReflectionTestUtils.setField(service,"inboxMapper",inbox); ReflectionTestUtils.setField(service,"orderMapper",orders); ReflectionTestUtils.setField(service,"inboundOrderMapper",inboundOrders); ReflectionTestUtils.setField(service,"detailMapper",details); ReflectionTestUtils.setField(service,"historyMapper",histories); ReflectionTestUtils.setField(service,"cartonMapper",cartons); ReflectionTestUtils.setField(service,"dropshipOrders",dropshipOrders); ReflectionTestUtils.setField(service,"inventory",inventory); ReflectionTestUtils.setField(service,"shippingGates",shippingGates); ReflectionTestUtils.setField(service,"loadingEvidence",loadingEvidence); ReflectionTestUtils.setField(service,"objectMapper",json); LastlegOrderNoGenerator generator=new LastlegOrderNoGenerator(sequences); ReflectionTestUtils.setField(service,"orderNoGenerator",generator); lenient().doAnswer(i->{ LastlegOrder o=i.getArgument(0); o.setId(Math.abs(o.getSourceInboundId().hashCode())+1L); return 1; }).when(orders).insert(any(LastlegOrder.class)); lenient().when(orders.lockContainerNumbers(anyString())).thenReturn(List.of()); lenient().when(shippingGates.initialize(anyString(),anyString(),anyString(),anyLong(),anyLong())).thenAnswer(i->{ String key=i.getArgument(0); String value=i.getArgument(2); return frozenContainerNos.putIfAbsent(key,value)==null?1:0; }); lenient().when(shippingGates.lock(anyString())).thenAnswer(i->{ String key=i.getArgument(0); String value=frozenContainerNos.get(key); if(value==null)return null; var gate=new com.baiyue.lastleg.shipping.entity.ContainerShippingGate(); gate.setContainerBookingId(key); gate.setContainerNo(value); return gate; }); }
|
||||
@Test void createsPendingOrdersAndInTransitSnapshotsWithoutInboundPlans(){ EventEnvelope event=event(); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(event).outcome()); assertThrows(NoSuchFieldException.class,()->LoadingCompletedConsumer.class.getDeclaredField("inboundPlans")); verify(orders,times(2)).insert(any(LastlegOrder.class)); ArgumentCaptor<InventoryCommandRequests.InTransit> inTransit=ArgumentCaptor.forClass(InventoryCommandRequests.InTransit.class); verify(inventory,times(2)).registerInTransit(eq("100"),eq("SYSTEM"),inTransit.capture()); assertEquals(Set.of("RK1","RK2"),inTransit.getAllValues().stream().map(InventoryCommandRequests.InTransit::inboundOrderNo).collect(java.util.stream.Collectors.toSet())); verify(details,times(2)).insert(any(LastlegOrderDetail.class)); verify(histories,times(2)).insert(any(LastlegOrderStatusHistory.class)); ArgumentCaptor<LastlegOrder> c=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(c.capture()); assertEquals(Set.of("in-1","in-2"),c.getAllValues().stream().map(LastlegOrder::getSourceInboundId).collect(java.util.stream.Collectors.toSet())); assertTrue(c.getAllValues().stream().allMatch(o->"PENDING_ASSIGNMENT".equals(o.getCurrentStatus()))); assertTrue(c.getAllValues().stream().allMatch(o->o.getInboundOrderNo()==null)); assertTrue(c.getAllValues().stream().allMatch(o->o.getId()!=null)); assertTrue(c.getAllValues().stream().allMatch(o->"MSCU6639870".equals(o.getContainerNo()))); assertTrue(c.getAllValues().stream().allMatch(o->"FBA".equals(o.getDestinationType()))); assertTrue(c.getAllValues().stream().allMatch(o->"Customer 100".equals(o.getCustomerName()))); assertTrue(c.getAllValues().stream().allMatch(o->"ow-1".equals(o.getSourceWarehouseId())&&"193000000000200003".equals(o.getOverseasWarehouseId())&&"US-LAX-01".equals(o.getOverseasWarehouseCode())&&"洛杉矶仓".equals(o.getOverseasWarehouseName())&&Integer.valueOf(0).equals(o.getReceivingWarehouseVersion()))); ArgumentCaptor<LastlegInboundOrder> snapshots=ArgumentCaptor.forClass(LastlegInboundOrder.class); verify(inboundOrders,times(2)).insert(snapshots.capture()); assertTrue(snapshots.getAllValues().stream().allMatch(o->"100".equals(o.getCustomerId())&&"FBA".equals(o.getDestinationType()))); assertEquals(1,c.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(c.getAllValues().get(0).getOrderNo().matches("FL-WH-[0-9]{8}-[0-9]{6}")); }
|
||||
@Test void oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); com.fasterxml.jackson.databind.node.ArrayNode rows=json.createArrayNode(); for(int index=1;index<=15;index++){ com.fasterxml.jackson.databind.node.ObjectNode row=node.path("orders").get(0).deepCopy(); row.put("sourceInboundId","in-"+index).put("rootInboundId","root-"+index).put("sourceOrderId","order-"+index).put("sourceOrderNo","FL-"+index).put("rootOrderId","root-order-"+index).put("inboundCode","RK-"+index); rows.add(row); } node.set("orders",rows); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope fifteen=new EventEnvelope("123e4567-e89b-12d3-a456-426614174015",source.eventType(),source.eventVersion(),source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(fifteen).outcome()); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(15)).insert(captured.capture()); assertEquals(1,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(captured.getAllValues().get(0).getOrderNo().matches("^FL-WH-\\d{8}-\\d{6}$")); verify(sequences,never()).nextValue(any(java.time.LocalDate.class)); }
|
||||
@Test void oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); com.fasterxml.jackson.databind.node.ArrayNode rows=json.createArrayNode(); for(int index=1;index<=15;index++){ com.fasterxml.jackson.databind.node.ObjectNode row=node.path("orders").get(0).deepCopy(); row.put("sourceInboundId","in-"+index).put("rootInboundId","root-"+index).put("sourceOrderId","order-"+index).put("sourceOrderNo","FL-"+index).put("rootOrderId","root-order-"+index).put("inboundCode","RK-"+index); rows.add(row); } node.set("orders",rows); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope fifteen=new EventEnvelope("123e4567-e89b-12d3-a456-426614174015",source.eventType(),source.eventVersion(),source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(fifteen).outcome()); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(15)).insert(captured.capture()); assertEquals(1,captured.getAllValues().stream().map(LastlegOrder::getOrderNo).distinct().count()); assertTrue(captured.getAllValues().get(0).getOrderNo().matches("^FL-WH-\\d{8}-\\d{6}$")); verifyNoInteractions(sequences); }
|
||||
@Test void rejectsMissingOwnerSnapshotBeforeAnyLastlegWrite(){ EventEnvelope source=event(); com.fasterxml.jackson.databind.node.ObjectNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node.path("orders").get(0)).put("customerName"," "); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope missing=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(missing)); assertTrue(error.getMessage().contains("尾程事件负载无效")||error.getMessage().contains("货主快照")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
|
||||
@Test void freezesFormalPlannedWarehouseArrivalFromLoadingEvent(){ EventEnvelope source=event(); JsonNode node=source.payload().deepCopy(); ((com.fasterxml.jackson.databind.node.ObjectNode)node).put("estimatedWarehouseArrivalAt","2026-10-03T09:00"); String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope changed=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),node,hash); service.consume(changed); ArgumentCaptor<LastlegOrder> captor=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(2)).insert(captor.capture()); assertTrue(captor.getAllValues().stream().allMatch(order->java.time.LocalDateTime.of(2026,10,3,9,0).equals(order.getEstimatedWarehouseAt()))); }
|
||||
@Test void loadingCreatesAwaitingPickupReadyGateWithoutPollutingShippingVersion(){ service.consume(event()); verify(shippingGates).initialize("book-1","","MSCU6639870",0L,1L); }
|
||||
@@ -15,7 +15,7 @@ class LoadingCompletedConsumerTest {
|
||||
@Test void rejectsDifferentContainerNumberForSameSourceBookingBeforeAnyBusinessWrite(){ EventEnvelope source=event(); frozenContainerNos.put("book-1","CSQU3054383"); IllegalStateException error=assertThrows(IllegalStateException.class,()->service.consume(source)); assertTrue(error.getMessage().contains("sourceContainerBookingId")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
|
||||
@Test void rejectsExistingOrderContainerDriftForSameSourceBooking(){ when(orders.lockContainerNumbers("book-1")).thenReturn(List.of("MSCU6639870","CSQU3054383")); assertThrows(IllegalStateException.class,()->service.consume(event())); verify(inbox,never()).insert(any(LastlegEventInbox.class)); verify(orders,never()).insert(any(LastlegOrder.class)); }
|
||||
@Test void acceptsSeparateCustomerEventsForTheSameFrozenContainerIdentity(){ EventEnvelope first=event(); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(first).outcome()); com.fasterxml.jackson.databind.node.ObjectNode node=first.payload().deepCopy(); int index=0; for(JsonNode order:node.path("orders")){ ((com.fasterxml.jackson.databind.node.ObjectNode)order).put("customerId","200").put("customerName","Customer 200").put("sourceInboundId","customer-200-in-"+(++index)); } String hash=EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(node)); EventEnvelope second=new EventEnvelope("123e4567-e89b-12d3-a456-426614174200",first.eventType(),first.eventVersion(),first.occurredAt(),"200",first.aggregateType(),first.aggregateId(),first.aggregateVersion(),first.handoverVersion(),first.traceId(),first.correlationId(),first.causationId(),node,hash); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.APPLIED, service.consume(second).outcome()); verify(shippingGates,times(2)).initialize(eq("book-1"),eq(""),eq("MSCU6639870"),eq(0L),eq(1L)); ArgumentCaptor<LastlegOrder> captured=ArgumentCaptor.forClass(LastlegOrder.class); verify(orders,times(4)).insert(captured.capture()); assertEquals(Set.of("100","200"),captured.getAllValues().stream().map(LastlegOrder::getCustomerId).collect(java.util.stream.Collectors.toSet())); assertTrue(captured.getAllValues().stream().allMatch(order->"MSCU6639870".equals(order.getContainerNo()))); }
|
||||
@Test void replayPreservesExistingLmoNumberAndAddsNothing(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); row.setStatus("PROCESSED"); when(inbox.selectById(event.eventId())).thenReturn(row); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.DUPLICATE, service.consume(event).outcome()); verify(sequences,never()).nextValue(any(java.time.LocalDate.class)); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); verify(histories,never()).insert(any(LastlegOrderStatusHistory.class)); }
|
||||
@Test void replayPreservesExistingLmoNumberAndAddsNothing(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash(event.payloadHash()); row.setStatus("PROCESSED"); when(inbox.selectById(event.eventId())).thenReturn(row); assertEquals(com.baiyue.common.event.DeliveryAck.Outcome.DUPLICATE, service.consume(event).outcome()); verifyNoInteractions(sequences); verify(orders,never()).insert(any(LastlegOrder.class)); verify(details,never()).insert(any(LastlegOrderDetail.class)); verify(histories,never()).insert(any(LastlegOrderStatusHistory.class)); }
|
||||
@Test void sameEventWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox row=new LastlegEventInbox(); row.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(row); assertThrows(IllegalArgumentException.class,()->service.consume(event)); }
|
||||
@Test void tamperedPayloadHashIsRejectedBeforeInboxMutation(){ EventEnvelope source=event(); EventEnvelope tampered=new EventEnvelope(source.eventId(),source.eventType(),source.eventVersion(),source.occurredAt(),source.tenantId(),source.aggregateType(),source.aggregateId(),source.aggregateVersion(),source.handoverVersion(),source.traceId(),source.correlationId(),source.causationId(),source.payload(),"sha256:"+"0".repeat(64)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(tampered)); assertTrue(error.getMessage().contains("payloadHash")); verify(inbox,never()).insert(any(LastlegEventInbox.class)); }
|
||||
@Test void concurrentDuplicateKeyWithDifferentHashIsRejected(){ EventEnvelope event=event(); LastlegEventInbox raced=new LastlegEventInbox(); raced.setPayloadHash("sha256:"+"0".repeat(64)); when(inbox.selectById(event.eventId())).thenReturn(null,raced); doThrow(new org.springframework.dao.DuplicateKeyException("race")).when(inbox).insert(any(LastlegEventInbox.class)); IllegalArgumentException error=assertThrows(IllegalArgumentException.class,()->service.consume(event)); assertTrue(error.getMessage().contains("摘要冲突")); verify(orders,never()).countContainer(anyString(),anyString()); }
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PhysicalCartonNumberTest {
|
||||
/** 箱码不属于本次改号,完整保留内部 ID 的每一位以保证扫码唯一性。 */
|
||||
@Test void preservesEveryIdBit() {
|
||||
long id = 2087379674555461633L;
|
||||
String mark = LastlegOrderNoGenerator.compactCartonMark(id);
|
||||
assertTrue(mark.length() <= 16);
|
||||
assertEquals(id, Long.parseLong(mark.substring(3), 36));
|
||||
assertNotEquals(mark, LastlegOrderNoGenerator.compactCartonMark(id + 1_000_000));
|
||||
assertEquals(mark, LastlegOrderNoGenerator.compactCartonMark(id));
|
||||
assertThrows(IllegalArgumentException.class, () -> LastlegOrderNoGenerator.compactCartonMark(0));
|
||||
}
|
||||
}
|
||||
+16
-49
@@ -1,60 +1,27 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.IntStream;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ReturnBusinessNumberTest {
|
||||
// 实物箱唛采用稳定BX编码,由CompactBusinessNumberServiceTest校验;此处仅验证独立业务编号。
|
||||
private static final Map<String, String> TYPES = Map.of("RETURN", "RT", "RETURN_RECEIPT", "RR",
|
||||
"RETURN_BATCH", "RB", "RETURN_RELABEL", "RETU");
|
||||
|
||||
@Test
|
||||
void formerlyCollidingInternalIdsAndRepeatedIdsNoLongerDetermineBusinessNumbers() {
|
||||
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
|
||||
long first = 2099800000000781889L, second = first + 1_000_000L;
|
||||
assertEquals(Math.floorMod(first, 1_000_000), Math.floorMod(second, 1_000_000));
|
||||
TYPES.forEach((type, prefix) -> {
|
||||
String a = generator.next(type, "warehouse-LAX-01", first);
|
||||
String b = generator.next(type, "warehouse-LAX-01", second);
|
||||
String c = generator.next(type, "warehouse-LAX-01", first);
|
||||
assertEquals(3, new HashSet<>(java.util.List.of(a, b, c)).size());
|
||||
for (String value : java.util.List.of(a, b, c)) {
|
||||
assertTrue(value.matches(prefix + "-[0-9A-Z]{25}"), value);
|
||||
assertTrue(value.length() <= 32);
|
||||
assertFalse(value.contains(Long.toString(first)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void encodingPreservesAll128BitsIncludingLeadingZerosAndSignBits() {
|
||||
for (UUID uuid : java.util.List.of(new UUID(0, 0), new UUID(-1, -1), new UUID(Long.MIN_VALUE, 1),
|
||||
UUID.fromString("00000000-0000-4000-8000-000000000001"),
|
||||
UUID.fromString("ffffffff-ffff-4fff-bfff-000000000001"))) {
|
||||
String encoded = LastlegOrderNoGenerator.encodeReturnUuid(uuid);
|
||||
assertEquals(25, encoded.length());
|
||||
assertEquals(new BigInteger(uuid.toString().replace("-", ""), 16), new BigInteger(encoded, 36));
|
||||
/** 独立单据不再依赖客户简称和内部 ID,各前缀从一开始。 */
|
||||
@Test void businessPrefixesHaveIndependentFourDigitSequences() {
|
||||
var generator=TestDocumentNumbers.documents();
|
||||
for(String prefix:List.of("TH","THHB","TB","ZY","RR","RB","HX","CG","PR","PJ","PF","PN","PX","PG","CT","FB","FO","VR")) {
|
||||
var values=List.of(generator.next(prefix),generator.next(prefix),generator.next(prefix));
|
||||
assertEquals(3,new HashSet<>(values).size());
|
||||
assertEquals(prefix+"202610200001",values.get(0));
|
||||
assertEquals(prefix+"202610200003",values.get(2));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void independentInstancesGenerateDistinctNumbersDuringConcurrentBurst() {
|
||||
var values = IntStream.range(0, 20_000).parallel()
|
||||
.mapToObj(i -> new LastlegOrderNoGenerator().next("RETURN_RECEIPT", "42", 123L)).toList();
|
||||
assertEquals(values.size(), new HashSet<>(values).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nonReturnRulesRemainUnchangedAndReturnNormalizationIsLocaleIndependent() {
|
||||
LastlegOrderNoGenerator generator = new LastlegOrderNoGenerator();
|
||||
for (String type : java.util.List.of("OUTBOUND", "RELABEL", "PROCUREMENT", "DISPATCH")) {
|
||||
assertEquals(generator.next(type, "LAX", 123), generator.next(type, "LAX", 1_000_123));
|
||||
}
|
||||
assertTrue(generator.next(" return_receipt ", "42", 1).matches("RR-[0-9A-Z]{25}"));
|
||||
/** 商业池和普通作业入口必须共享卡车及散板同类型序列。 */
|
||||
@Test void commercialAndOperationalEntriesShareTheSameCounters() {
|
||||
var documents=TestDocumentNumbers.documents();var operations=new LastlegOrderNoGenerator(documents);
|
||||
assertEquals("TR202610200001",documents.next("TR"));
|
||||
assertEquals("TR202610200002",operations.next("TRUCK"));
|
||||
assertEquals("LB202610200001",operations.next("LOOSEBOARD"));
|
||||
assertEquals("LB202610200002",documents.next("LB"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.lastleg.service;
|
||||
|
||||
import com.baiyue.lastleg.repository.DocumentSequenceRepository;
|
||||
import java.time.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 业务单测使用真实格式及隔离计数器;数据库并发由 Repository 集成测试覆盖。 */
|
||||
public final class TestDocumentNumbers {
|
||||
/** 测试工具不实例化。 */
|
||||
private TestDocumentNumbers() {}
|
||||
/** 每个测试生成器独立计数,类型前缀之间互不占号。 */
|
||||
public static LastlegDocumentNumberGenerator documents() {
|
||||
var sequences=mock(DocumentSequenceRepository.class);
|
||||
var counters=new ConcurrentHashMap<String,AtomicLong>();
|
||||
lenient().when(sequences.next(anyString())).thenAnswer(call->counters.computeIfAbsent(call.getArgument(0),key->new AtomicLong()).incrementAndGet());
|
||||
return new LastlegDocumentNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-20T00:00:00Z"),ZoneOffset.UTC));
|
||||
}
|
||||
/** 海外类型映射同样使用生产生成器。 */
|
||||
public static LastlegOrderNoGenerator orders() { return new LastlegOrderNoGenerator(documents()); }
|
||||
}
|
||||
+5
-6
@@ -196,7 +196,7 @@ class WarehouseTransferServiceTest {
|
||||
@Mock LastlegOrderDetailMapper orderDetails;
|
||||
@Mock LastlegOrderStatusHistoryMapper orderHistories;
|
||||
@Mock RelabelApplicationMapper relabelApplications;
|
||||
@Spy LastlegOrderNoGenerator orderNoGenerator = new LastlegOrderNoGenerator();
|
||||
@Spy LastlegOrderNoGenerator orderNoGenerator = com.baiyue.lastleg.service.TestDocumentNumbers.orders();
|
||||
@Mock InventoryCommandService inventory;
|
||||
@Mock InventoryCommandMapper inventoryCommands;
|
||||
@Mock com.baiyue.lastleg.relabel.service.RelabelApplicationService relabelService;
|
||||
@@ -204,16 +204,16 @@ class WarehouseTransferServiceTest {
|
||||
@Mock WarehouseTransferReceiptCoordinator receiptCoordinator;
|
||||
@Mock com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory customerDirectory;
|
||||
|
||||
private com.baiyue.lastleg.service.CompactBusinessNumberService compactNumbers;
|
||||
private com.baiyue.lastleg.service.LastlegDocumentNumberGenerator compactNumbers;
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.CompactBusinessNumberService.class);
|
||||
compactNumbers = org.mockito.Mockito.mock(com.baiyue.lastleg.service.LastlegDocumentNumberGenerator.class);
|
||||
service = new WarehouseTransferService();
|
||||
var measurementRepository = new com.baiyue.lastleg.transfer.repository.TransferMeasurementRepository();
|
||||
ReflectionTestUtils.setField(measurementRepository, "cartons", transferCartons);
|
||||
ReflectionTestUtils.setField(service, "transferMeasurementRepository", measurementRepository);
|
||||
ReflectionTestUtils.setField(service, "compactNumbers", compactNumbers);
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
|
||||
org.mockito.Mockito.lenient().when(compactNumbers.next(org.mockito.ArgumentMatchers.anyString())).thenAnswer(call -> call.getArgument(0) + "-CS-261016102030");
|
||||
ReflectionTestUtils.setField(service, "customerDirectory", customerDirectory);
|
||||
lenient().when(customerDirectory.find("TENANT-A")).thenReturn(
|
||||
new com.baiyue.lastleg.fleet.customer.FleetCustomerDirectory.Customer("TENANT-A", "C-A", "Customer A", null, null));
|
||||
@@ -267,8 +267,7 @@ class WarehouseTransferServiceTest {
|
||||
ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands);
|
||||
ReflectionTestUtils.setField(service, "relabelService", relabelService);
|
||||
ReflectionTestUtils.setField(service, "objectMapper", new ObjectMapper().findAndRegisterModules());
|
||||
lenient().when(orderNoGenerator.next(anyString(), any(), anyLong()))
|
||||
.thenReturn("WT-0000000000000000000000001");
|
||||
lenient().doReturn("ZY202610200001").when(orderNoGenerator).next(anyString());
|
||||
lenient().when(inventoryAllocations.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(transferReceipts.selectList(any())).thenReturn(List.of());
|
||||
lenient().when(transferCartons.listOrder(anyString(), anyLong())).thenReturn(List.of());
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class UsTruckOrderServiceTest {
|
||||
|
||||
@Test
|
||||
void createStoresBothContactsAndMinuteTimesWithoutInventingPallets() {
|
||||
when(orderNoGenerator.next(anyString(), anyString(), anyLong())).thenReturn("TK1");
|
||||
when(orderNoGenerator.next(anyString())).thenReturn("TK1");
|
||||
when(histories.listOrder(anyString(), anyLong())).thenReturn(historyRows);
|
||||
when(orders.insert(any(UsTruckOrder.class))).thenAnswer(call -> { order = call.getArgument(0); return 1; });
|
||||
var request = create("PALLET_15", "new-truck");
|
||||
|
||||
Reference in New Issue
Block a user