优化
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
package com.baiyue.lastleg.dispatch.fleet;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchDetailReader;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchViews;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 只读委托既有排车应用服务,运营域不引用主流程实现或持久化实体。 */
|
||||
@Component
|
||||
public class DispatchDetailReadAdapter implements DispatchDetailReader {
|
||||
private final DispatchAggregateService dispatch;
|
||||
|
||||
/** 复用唯一原排车详情提供者,不建立第二套查询规则。 */
|
||||
public DispatchDetailReadAdapter(DispatchAggregateService dispatch) {
|
||||
this.dispatch = dispatch;
|
||||
}
|
||||
|
||||
/** 原单资格与可见性仍由原应用服务核对。 */
|
||||
@Override
|
||||
public DispatchViews.Detail detail(DispatchContracts.Actor actor, String dispatchNo) {
|
||||
return dispatch.detail(actor, dispatchNo);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package com.baiyue.lastleg.dispatch.port;
|
||||
|
||||
/** 原排车详情的只读应用端口,供运营视图组合真实履约事实。 */
|
||||
public interface DispatchDetailReader {
|
||||
/** 沿原排车读取及身份守卫,不创建单据、不推进状态。 */
|
||||
DispatchViews.Detail detail(DispatchContracts.Actor actor, String dispatchNo);
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.time.LocalDate;
|
||||
|
||||
/** 整车单号独立日流水;独立短事务归还序列锁后才进入车辆、司机及货物锁。 */
|
||||
@Repository
|
||||
public class DispatchNumberSequence {
|
||||
private final JdbcTemplate jdbc;
|
||||
|
||||
/** 流水与排车主单使用相同服务数据库。 */
|
||||
public DispatchNumberSequence(JdbcTemplate jdbc) { this.jdbc = jdbc; }
|
||||
|
||||
/** 唯一日期行原子递增,连接内取回本次值;失败不复用已分配的业务号。 */
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public long next(LocalDate date) {
|
||||
jdbc.update("INSERT INTO ll_dispatch_daily_sequence(business_date,last_sequence) VALUES(?,LAST_INSERT_ID(1)) "
|
||||
+ "ON DUPLICATE KEY UPDATE last_sequence=LAST_INSERT_ID(last_sequence+1)", date);
|
||||
Long value = jdbc.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
|
||||
if (value == null || value < 1) throw new IllegalStateException("排车日流水分配失败");
|
||||
if (value > 9999) throw new BizException(ResultCode.CONFLICT, "当日排车单四位流水号已用满");
|
||||
return value;
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -10,6 +10,7 @@ import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate;
|
||||
import com.baiyue.lastleg.dispatch.port.*;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository;
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture;
|
||||
import com.baiyue.lastleg.fleet.trip.FleetExecutionModels;
|
||||
import com.baiyue.lastleg.fleet.trip.FleetTripExecutionService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
@@ -31,6 +32,7 @@ import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||||
/** 真整车创建、加货与查询;客户只属于明细,不属于整车头。 */
|
||||
@Service
|
||||
public class DispatchAggregateService implements DispatchExecutionPort,DispatchAppointmentAmendmentPort {
|
||||
@jakarta.annotation.Resource private DispatchNumberGenerator numbers;
|
||||
@jakarta.annotation.Resource private DispatchAppointmentAllocations appointments;
|
||||
@jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims;
|
||||
@jakarta.annotation.Resource private DispatchItemSourceService relabelSources;
|
||||
@@ -47,7 +49,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
private final ObjectMapper json;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway evidence;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.repository.DispatchExecutionRepository executionReads;
|
||||
@jakarta.annotation.Resource private com.baiyue.lastleg.fleet.ownedbusiness.port.OwnedBusinessOwnershipCapture ownedBusinessOwnership;
|
||||
@jakarta.annotation.Resource private OwnedBusinessOwnershipCapture ownedBusinessOwnership;
|
||||
|
||||
/** 领域编排只访问Repository和提供者应用入口。 */
|
||||
public DispatchAggregateService(DispatchAggregateRepository repository, DispatchSourceRepository sources,
|
||||
@@ -268,7 +270,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA
|
||||
var replay = commands.replay(actor,null,identity);
|
||||
if (replay.isPresent()) return replay.get();
|
||||
long id=IdWorker.getId();
|
||||
String number="DS-"+UUID.randomUUID().toString().replace("-","").toUpperCase(Locale.ROOT);
|
||||
String number=numbers.next();
|
||||
var shell=fleet.createDispatchShell(actor.operatorId(),new FleetExecutionModels.DispatchShellRequest(id,number,
|
||||
Long.parseLong(request.warehouseId()),request.idempotencyKey(),identity.commandHash(),request.plannedDepartureAt(),
|
||||
fleetVehicle(request.vehicle()),fleetDriver(request.driver()),request.remark()));
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchNumberSequence;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Locale;
|
||||
|
||||
/** 平台排车单号:PC、北京时间日期与每日独立四位流水。 */
|
||||
@Component
|
||||
public class DispatchNumberGenerator {
|
||||
private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Shanghai");
|
||||
private final DispatchNumberSequence sequences;
|
||||
private final Clock clock;
|
||||
|
||||
/** 正式运行按统一业务时区取当天日期。 */
|
||||
@Autowired
|
||||
public DispatchNumberGenerator(DispatchNumberSequence sequences) { this(sequences, Clock.systemUTC()); }
|
||||
|
||||
/** 固定时钟用于核实跨午夜归零边界。 */
|
||||
DispatchNumberGenerator(DispatchNumberSequence sequences, Clock clock) { this.sequences = sequences; this.clock = clock; }
|
||||
|
||||
/** 已有命令重放由上层账本恢复原号,本方法只用于首次建单。 */
|
||||
public String next() {
|
||||
LocalDate date = LocalDate.now(clock.withZone(BUSINESS_ZONE));
|
||||
long value = sequences.next(date);
|
||||
if (value < 1 || value > 9999) throw new IllegalStateException("排车日流水不在四位范围内");
|
||||
return "PC" + date.format(DateTimeFormatter.BASIC_ISO_DATE) + String.format(Locale.ROOT, "%04d", value);
|
||||
}
|
||||
}
|
||||
+6
-1
@@ -11,7 +11,8 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Set;
|
||||
import static com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessSupport.*;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
|
||||
/** File保存照片,本域另行授权原排车及分类引用。 */
|
||||
@Component
|
||||
@@ -76,6 +77,10 @@ public class HttpOwnedBusinessFileGateway implements OwnedBusinessFileGateway {
|
||||
byte[] png={(byte)137,80,78,71,13,10,26,10};if(!"image/png".equals(type) || bytes.length<png.length)return false;
|
||||
for(int i=0;i<png.length;i++)if(bytes[i]!=png[i])return false;return true;
|
||||
}
|
||||
/** 基础设施错误不反向依赖应用Service,保持单向包依赖。 */
|
||||
private static BizException bad(String message){return new BizException(ResultCode.BAD_REQUEST,message);}
|
||||
/** 不明确的文件结果保留为业务冲突,由上传账本记录。 */
|
||||
private static BizException conflict(String message){return new BizException(ResultCode.CONFLICT,message);}
|
||||
public record Envelope(String code,Metadata data) {}
|
||||
public record Metadata(Long fileId,Long tenantId,String businessType,String businessId,String fileName,String contentType,Long sizeBytes,LocalDateTime createdAt) {}
|
||||
}
|
||||
|
||||
+6
-4
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.mapper;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.*;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import java.util.List;
|
||||
@@ -9,7 +8,10 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface OwnedBusinessQueryMapper {
|
||||
String SOURCE="""
|
||||
SELECT d.*,o.vehicle_id AS owned_vehicle_id,o.resource_mode AS owned_resource_mode,
|
||||
SELECT d.id,d.dispatch_no,d.status,d.stage,d.record_version,d.overseas_warehouse_id,
|
||||
d.overseas_warehouse_code,d.overseas_warehouse_name,d.planned_departure_at,d.loading_completed_at,
|
||||
d.departed_at,d.delivery_completed_at,d.loading_completion_hash,d.loading_confirmation_json,d.resources_json,
|
||||
o.vehicle_id AS owned_vehicle_id,o.resource_mode AS owned_resource_mode,
|
||||
o.vehicle_ownership AS owned_vehicle_ownership,o.vehicle_record_version AS owned_vehicle_record_version,
|
||||
o.loading_completion_hash AS owned_loading_completion_hash,o.loading_completed_at AS owned_loading_completed_at
|
||||
FROM ll_dispatch_order d LEFT JOIN ll_fleet_owned_business_ownership o ON o.dispatch_id=d.id
|
||||
@@ -38,8 +40,8 @@ public interface OwnedBusinessQueryMapper {
|
||||
@Select(SOURCE+" WHERE d.dispatch_no=#{number}")
|
||||
Source find(String number);
|
||||
/** 页内全部货物一次读取,各客户保持原归属。 */
|
||||
@Select("<script>SELECT * FROM ll_dispatch_item WHERE dispatch_order_id IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach> ORDER BY dispatch_order_id,id</script>")
|
||||
List<DispatchItem> cargo(@Param("ids") List<Long> ids);
|
||||
@Select("<script>SELECT id,dispatch_order_id,warehouse_pallet_id,customer_id,customer_name,source_key,order_no,inbound_code,pallet_no,destination_type,destination_warehouse_code,destination_warehouse_name,fba_warehouse_code,delivery_address,postal_code,package_count FROM ll_dispatch_item WHERE dispatch_order_id IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach> ORDER BY dispatch_order_id,id</script>")
|
||||
List<CargoRow> cargo(@Param("ids") List<Long> ids);
|
||||
/** 只聚合真实FBA分配,目的仓与数量保持原事实,不按客户重复整车。 */
|
||||
@Select("""
|
||||
<script>SELECT a.dispatch_id,a.destination_key,p.appointment_no,p.confirmation_no,
|
||||
|
||||
+12
-2
@@ -1,6 +1,5 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.model;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchAggregate;
|
||||
import lombok.Data;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -14,11 +13,22 @@ public final class OwnedBusinessRows {
|
||||
private String resourceMode,vehicleOwnership,loadingCompletionHash;
|
||||
private LocalDateTime ownershipCapturedAt,loadingCompletedAt;
|
||||
}
|
||||
@Data public static class Source extends DispatchAggregate {
|
||||
@Data public static class Source {
|
||||
private Long id,recordVersion;
|
||||
private String dispatchNo,status,stage,overseasWarehouseId,overseasWarehouseCode,overseasWarehouseName;
|
||||
private String loadingCompletionHash,loadingConfirmationJson,resourcesJson;
|
||||
private LocalDateTime plannedDepartureAt,loadingCompletedAt,departedAt,deliveryCompletedAt;
|
||||
private String ownedResourceMode,ownedVehicleOwnership,ownedLoadingCompletionHash;
|
||||
private Long ownedVehicleId,ownedVehicleRecordVersion;
|
||||
private LocalDateTime ownedLoadingCompletedAt;
|
||||
}
|
||||
/** 原表货物只读字段;不是可写排车实体,不复制来源状态机。 */
|
||||
@Data public static class CargoRow {
|
||||
private Long id,dispatchOrderId,warehousePalletId;
|
||||
private String customerId,customerName,sourceKey,orderNo,inboundCode,palletNo;
|
||||
private String destinationType,destinationWarehouseCode,destinationWarehouseName,fbaWarehouseCode,deliveryAddress,postalCode;
|
||||
private Integer packageCount;
|
||||
}
|
||||
@Data public static class Record {
|
||||
private Long id,dispatchId,recordVersion;
|
||||
private String currency,arrivalsJson,photosJson,createdBy,updatedBy;
|
||||
|
||||
+1
-2
@@ -4,7 +4,6 @@ import com.baiyue.lastleg.fleet.ownedbusiness.mapper.OwnedBusinessMapper;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.mapper.OwnedBusinessQueryMapper;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.Record;
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import java.util.List;
|
||||
|
||||
@@ -26,7 +25,7 @@ public class OwnedBusinessRepository {
|
||||
/** 读取唯一原排车身份及归属证据。 */
|
||||
public Source source(String number){return queries.find(number);}
|
||||
/** 页内货物批量查询。 */
|
||||
public List<DispatchItem> cargo(List<Long> ids){return ids.isEmpty()?List.of():queries.cargo(ids);}
|
||||
public List<CargoRow> cargo(List<Long> ids){return ids.isEmpty()?List.of():queries.cargo(ids);}
|
||||
/** 页内实际预约分配批量查询。 */
|
||||
public List<Batch> batches(List<Long> ids){return ids.isEmpty()?List.of():queries.batches(ids);}
|
||||
/** GET只读,无记录保留虚拟0版本。 */
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ public class OwnedBusinessOwnershipService implements OwnedBusinessOwnershipCapt
|
||||
var row=new Ownership();row.setDispatchId(dispatchId);row.setVehicleId(vehicleId);row.setResourceMode(resourceMode);
|
||||
if("MASTER".equals(resourceMode)) {
|
||||
var vehicle=vehicles.findById(vehicleId).orElseThrow(()->conflict("原车辆主档不存在"));
|
||||
if(vehicle.id()!=vehicleId.longValue())throw conflict("车辆归属提供者返回的身份不一致");
|
||||
if(vehicle.recordVersion()<0)throw conflict("原车辆档案版本无效");
|
||||
if(!"OWNED".equals(vehicle.ownership()) && !"DISPATCHED".equals(vehicle.ownership()))throw conflict("车辆归属未明确");
|
||||
row.setVehicleOwnership(vehicle.ownership());row.setVehicleRecordVersion(vehicle.recordVersion());
|
||||
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.*;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.ConfirmedLoad;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts.Resources;
|
||||
import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.*;
|
||||
@@ -27,7 +27,7 @@ public class OwnedBusinessProjection {
|
||||
/** 照片已绑定才出现在当前记录,无文件不生成占位图。 */
|
||||
public List<Photo> photos(Record record){return record==null?List.of():codec.list(record.getPhotosJson(),Photo.class);}
|
||||
/** 使用原货物确定目的地key,保持与排车及到达登记一致。 */
|
||||
public String destinationKey(DispatchItem item) {
|
||||
public String destinationKey(CargoRow item) {
|
||||
String key=FleetDispatchGateway.destinationKey(item.getDestinationType(),item.getDestinationWarehouseCode(),
|
||||
item.getFbaWarehouseCode(),item.getDeliveryAddress(),item.getPostalCode());
|
||||
if(key==null || key.isBlank())throw OwnedBusinessSupport.conflict("原货物目的地事实缺失");return key;
|
||||
@@ -44,7 +44,7 @@ public class OwnedBusinessProjection {
|
||||
return ids.size();
|
||||
}
|
||||
/** 一车一行;目的地与预约只行内展开,不重复整车金额或托数。 */
|
||||
public Row row(Source source,Record record,List<DispatchItem> cargo,List<Batch> batches,Actor actor) {
|
||||
public Row row(Source source,Record record,List<CargoRow> cargo,List<Batch> batches,Actor actor) {
|
||||
var resources=codec.read(source.getResourcesJson(),Resources.class);
|
||||
if(resources.vehicle()==null)throw OwnedBusinessSupport.conflict("原车辆快照缺失");
|
||||
var ops=operations(record);var photos=photos(record);int pallets=palletCount(source);
|
||||
|
||||
+3
-3
@@ -5,7 +5,7 @@ import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.Source;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.Search;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.Record;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchDetailReader;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchTypes.ActorMode;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -22,10 +22,10 @@ public class OwnedBusinessQueryService {
|
||||
private final OwnedBusinessRepository repository;
|
||||
private final OwnedBusinessProjection projection;
|
||||
private final OwnedBusinessSupport codec;
|
||||
private final DispatchAggregateService dispatch;
|
||||
private final DispatchDetailReader dispatch;
|
||||
/** 主单详情沿既有真实提供者,列表使用本域批量投影。 */
|
||||
public OwnedBusinessQueryService(OwnedBusinessRepository repository,OwnedBusinessProjection projection,
|
||||
OwnedBusinessSupport codec,DispatchAggregateService dispatch){this.repository=repository;this.projection=projection;this.codec=codec;this.dispatch=dispatch;}
|
||||
OwnedBusinessSupport codec,DispatchDetailReader dispatch){this.repository=repository;this.projection=projection;this.codec=codec;this.dispatch=dispatch;}
|
||||
/** 四项统计与分页使用同一过滤和快照,GET不写库。 */
|
||||
@Transactional(readOnly=true,isolation=Isolation.REPEATABLE_READ)
|
||||
public ListResult list(Actor actor,Query query) {
|
||||
|
||||
+13
-1
@@ -60,7 +60,19 @@ public class OwnedBusinessRecordService {
|
||||
photos.put(slot,new Photo(slot,slot.category(),verified.fileId(),verified.fileName(),verified.contentType(),
|
||||
verified.sizeBytes(),verified.uploadedAt(),Instant.now().toString()));
|
||||
row.setPhotosJson(codec.write(List.copyOf(photos.values())));
|
||||
return commit(source,row,actor,version,"BIND_PHOTO",request.idempotencyKey(),identity,"保存"+slot.name()+"照片");
|
||||
return commit(source,row,actor,version,"BIND_PHOTO",request.idempotencyKey(),identity,photoReason(slot));
|
||||
}
|
||||
/** 操作记录使用准确中文槽位,不向用户展示内部枚举。 */
|
||||
private String photoReason(PhotoSlot slot) {
|
||||
return switch(slot) {
|
||||
case LOADING_SCENE -> "保存装车现场照片";
|
||||
case LOADING_LIST -> "保存装车清单照片";
|
||||
case PRE_DEPARTURE_CHECK -> "保存出车前检查照片";
|
||||
case DRIVER_LICENSE -> "保存驾驶证附件照片";
|
||||
case RECEIPT -> "保存签收凭证照片";
|
||||
case FUEL_START -> "保存起始油耗表盘照片";
|
||||
case FUEL_END -> "保存结束油耗表盘照片";
|
||||
};
|
||||
}
|
||||
/** 照片框也参与摘要,不能换URL框位复用原键。 */
|
||||
public record PhotoBinding(PhotoSlot slot,BindPhoto request) {}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.baiyue.lastleg.fleet.trip;
|
||||
|
||||
/** 日历专用只读查询投影,真实排车号不属于车次持久化字段。 */
|
||||
public class FleetTripCalendarRow extends FleetTripEntity {
|
||||
private String dispatchNo;
|
||||
|
||||
/** 返回关联整车主单的业务号。 */
|
||||
public String getDispatchNo() { return dispatchNo; }
|
||||
|
||||
/** 仅供日历SQL结果映射,不写入车次表。 */
|
||||
public void setDispatchNo(String dispatchNo) { this.dispatchNo = dispatchNo; }
|
||||
}
|
||||
+5
-4
@@ -11,10 +11,10 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
import java.time.*;
|
||||
import java.util.*;
|
||||
|
||||
/** Read-only transport plans. A trip is never evidence of a receiving appointment confirmation. */
|
||||
/** 排车日历沿真实整车主单读取运输时间,不代表已取得目的仓收货确认。 */
|
||||
@Service
|
||||
public class FleetTripCalendarService {
|
||||
public record Entry(String sourceType, String tripNo, String status, String originWarehouseId,
|
||||
public record Entry(String sourceType, String dispatchNo, String status, String originWarehouseId,
|
||||
String originWarehouseCode, String originWarehouseName, String originCountryCode,
|
||||
String originTimezone, String originWarehouseStatus, Instant plannedDepartureAt,
|
||||
Instant expectedDriverEndAt, Instant departedAt, Instant completedAt,
|
||||
@@ -31,12 +31,13 @@ public class FleetTripCalendarService {
|
||||
this.trips=trips; this.warehouses=warehouses; this.json=json; this.token=token;
|
||||
}
|
||||
|
||||
/** 一次读取真实排车及一次仓库资料查询,按业务地点判定日历归属。 */
|
||||
@Transactional(readOnly=true)
|
||||
public Result calendar(Instant from, Instant to) {
|
||||
if (from==null || to==null || !to.isAfter(from) || Duration.between(from,to).compareTo(Duration.ofDays(44))>0)
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"查询须为不超过44天的UTC时间范围");
|
||||
var rows=trips.calendar(LocalDateTime.ofInstant(from,ZoneOffset.UTC),LocalDateTime.ofInstant(to,ZoneOffset.UTC));
|
||||
if (rows.size()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"车次过多,请缩小查询日期范围");
|
||||
if (rows.size()>2000) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"排车过多,请缩小查询日期范围");
|
||||
if (rows.isEmpty()) return new Result(List.of(),List.of());
|
||||
var ids=rows.stream().map(FleetTripEntity::getWarehouseId).filter(Objects::nonNull).filter(id->id>0).distinct().sorted().toList();
|
||||
var metadata=metadata(ids);
|
||||
@@ -60,7 +61,7 @@ public class FleetTripCalendarService {
|
||||
throw new IllegalArgumentException("incomplete route");
|
||||
stops=List.copyOf(stops);
|
||||
} catch (Exception invalid) { stops=List.of(); reasons.add("ROUTE_INCOMPLETE"); }
|
||||
var entry=new Entry("TRIP_PLAN",row.getTripNo(),row.getStatus(),row.getWarehouseId()==null?null:row.getWarehouseId().toString(),
|
||||
var entry=new Entry("DISPATCH",row.getDispatchNo(),row.getStatus(),row.getWarehouseId()==null?null:row.getWarehouseId().toString(),
|
||||
warehouse==null?row.getWarehouseCode():warehouse.warehouseCode(),warehouse==null?row.getWarehouseName():warehouse.warehouseName(),
|
||||
country,zone,warehouse==null?null:warehouse.status(),instant(row.getPlannedDepartureAt()),instant(row.getExpectedDriverEndAt()),
|
||||
instant(row.getDepartedAt()),instant(row.getCompletedAt()),stops,List.copyOf(reasons));
|
||||
|
||||
@@ -7,17 +7,18 @@ import org.apache.ibatis.annotations.Select;
|
||||
|
||||
@Mapper
|
||||
public interface FleetTripMapper extends BaseMapper<FleetTripEntity> {
|
||||
/** 预约日历只读海外仓出库派送,取货入仓车次不属于排车类型。 */
|
||||
/** 日历只读真实整车主单的出库派送,一单一条;独立车次及取消主单不进入排车日历。 */
|
||||
@Select("""
|
||||
SELECT trip_no,status,warehouse_id,warehouse_code,warehouse_name,planned_departure_at,
|
||||
expected_driver_end_at,departed_at,completed_at,stops_json
|
||||
FROM ll_fleet_trip
|
||||
WHERE trip_purpose='DELIVERY' AND (
|
||||
(departed_at IS NOT NULL AND departed_at>=#{from} AND departed_at<#{to})
|
||||
OR (departed_at IS NULL AND planned_departure_at>=#{from} AND planned_departure_at<#{to}))
|
||||
ORDER BY COALESCE(departed_at,planned_departure_at),trip_no LIMIT 2001
|
||||
SELECT d.dispatch_no,t.trip_no,t.status,t.warehouse_id,t.warehouse_code,t.warehouse_name,t.planned_departure_at,
|
||||
t.expected_driver_end_at,t.departed_at,t.completed_at,t.stops_json
|
||||
FROM ll_dispatch_order d JOIN ll_fleet_trip t ON t.id=d.trip_id
|
||||
WHERE d.status='ACTIVE' AND d.dispatch_no IS NOT NULL AND TRIM(d.dispatch_no)<>''
|
||||
AND t.trip_purpose='DELIVERY' AND (
|
||||
(t.departed_at IS NOT NULL AND t.departed_at>=#{from} AND t.departed_at<#{to})
|
||||
OR (t.departed_at IS NULL AND t.planned_departure_at>=#{from} AND t.planned_departure_at<#{to}))
|
||||
ORDER BY COALESCE(t.departed_at,t.planned_departure_at),d.dispatch_no LIMIT 2001
|
||||
""")
|
||||
java.util.List<FleetTripEntity> calendar(@Param("from") java.time.LocalDateTime from,@Param("to") java.time.LocalDateTime to);
|
||||
java.util.List<FleetTripCalendarRow> calendar(@Param("from") java.time.LocalDateTime from,@Param("to") java.time.LocalDateTime to);
|
||||
@Select("SELECT * FROM ll_fleet_trip WHERE trip_no=#{tripNo} FOR UPDATE")
|
||||
FleetTripEntity lockByNo(@Param("tripNo") String tripNo);
|
||||
@Select("SELECT * FROM ll_fleet_trip WHERE trip_no=#{tripNo}")
|
||||
|
||||
+3
-2
@@ -18,13 +18,14 @@ import org.springframework.web.server.ResponseStatusException;
|
||||
@RequestMapping("/api/platform/v1/last-leg/fleet/trips")
|
||||
public class PlatformFleetTripController {
|
||||
@jakarta.annotation.Resource private FleetTripCalendarService calendar;
|
||||
/** 预约排车日历沿用真实排车列表读取权限,不要求已退出菜单的车次管理权限。 */
|
||||
@GetMapping("/calendar")
|
||||
@PreAuthorize("hasAuthority('platform:fleet:trip:list')")
|
||||
@PreAuthorize("hasAuthority('platform:overseas-warehouse:dispatch:list')")
|
||||
public ApiResponse<FleetTripCalendarService.Result> calendar(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam java.time.Instant from,@RequestParam java.time.Instant to) {
|
||||
operator(user);
|
||||
if (!"PLATFORM".equals(user.appCode()) || !Long.valueOf(0).equals(user.tenantId()))
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,"仅平台主体可读取车次日历");
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN,"仅平台主体可读取排车日历");
|
||||
return ApiResponse.success(calendar.calendar(from,to));
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ CREATE TABLE IF NOT EXISTS ll_order_no_daily_sequence (
|
||||
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',
|
||||
PRIMARY KEY(business_date)
|
||||
) ENGINE=InnoDB COMMENT='排车单号每日独立原子流水';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ll_order (
|
||||
id BIGINT NOT NULL COMMENT '雪花 ID',
|
||||
order_no VARCHAR(32) NOT NULL COMMENT '尾程单号',
|
||||
@@ -4593,7 +4599,7 @@ CREATE TABLE IF NOT EXISTS ll_fleet_owned_business_record (
|
||||
CONSTRAINT ck_owned_business_record_version CHECK (record_version>=0),
|
||||
CONSTRAINT ck_owned_business_money CHECK (
|
||||
(currency IS NULL AND total_amount IS NULL)
|
||||
OR (currency IN ('CNY','USD') AND total_amount IS NOT NULL AND total_amount>=0)),
|
||||
OR (currency IS NOT NULL AND currency IN ('CNY','USD') AND total_amount IS NOT NULL AND total_amount>=0)),
|
||||
CONSTRAINT ck_owned_business_distance CHECK (actual_distance_miles IS NULL OR actual_distance_miles>=0),
|
||||
CONSTRAINT ck_owned_business_fuel CHECK (fuel_consumption_litres_per_100km IS NULL OR fuel_consumption_litres_per_100km>=0)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='原排车自有业务附加运营信息';
|
||||
|
||||
@@ -48,13 +48,14 @@ paths:
|
||||
available: {type: boolean}
|
||||
/api/platform/v1/last-leg/fleet/trips/calendar:
|
||||
get:
|
||||
summary: 读取美国车次用车日历来源
|
||||
summary: 读取美国真实整车排车日历来源
|
||||
description: >-
|
||||
仅PLATFORM且平台公共主体(tenantId=0),权限 platform:fleet:trip:list。from/to 是明确UTC时间点,已发车按实际发车时间、尚未发车按计划发车时间读取半开区间,最多44天;
|
||||
仅PLATFORM且平台公共主体(tenantId=0),权限 platform:overseas-warehouse:dispatch:list,与排车列表及详情一致。from/to 是明确UTC时间点,已发车按实际发车时间、尚未发车按计划发车时间读取半开区间,最多44天;
|
||||
该保护性窗口供界面最多42个当地日历日加时区边界使用,不扩大界面范围。
|
||||
仅出库派送用途(DELIVERY);上门取货入仓用途(PICKUP)不属于此日历排车来源。
|
||||
最多2000条候选,超限明确拒绝并要求缩小日期范围,不分页扫描、不静默截断。
|
||||
一车一条,保留取消及完成状态;保留原计划与实际时间,已执行事实不随计划改期移动,不表示收货预约已确认。
|
||||
仅关联ll_dispatch_order中ACTIVE真实主单,返回dispatchNo;独立旧车次和取消排车主单排除,一张排车主单一条。
|
||||
保留原计划与实际时间,已执行事实不随计划改期移动,不表示收货预约已确认;日历跳排车详情,不使用内部车次号拼排车路径。
|
||||
已知非美国仓排除;未知仓库/国家/地理IANA时区进入unresolvedRecords,不落入美国当地日槽。
|
||||
一次车次查询加一次仓库批量资料请求,包含停用仓历史资料,资料服务失败返回错误而非空日历。
|
||||
parameters:
|
||||
@@ -358,10 +359,10 @@ components:
|
||||
schemas:
|
||||
TripCalendarEntry:
|
||||
type: object
|
||||
required: [sourceType, tripNo, status, plannedDepartureAt, expectedDriverEndAt, stops, incompleteReasons]
|
||||
required: [sourceType, dispatchNo, status, plannedDepartureAt, expectedDriverEndAt, stops, incompleteReasons]
|
||||
properties:
|
||||
sourceType: {type: string, enum: [TRIP_PLAN]}
|
||||
tripNo: {type: string}
|
||||
sourceType: {type: string, enum: [DISPATCH]}
|
||||
dispatchNo: {type: string, description: 真实整车主单排车号,用于排车详情路由}
|
||||
status: {type: string, enum: [PLANNED, IN_TRANSIT, COMPLETED, CANCELLED]}
|
||||
originWarehouseId: {type: string, nullable: true}
|
||||
originWarehouseCode: {type: string, nullable: true}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.baiyue.lastleg.dispatch.fleet;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchContracts;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchTypes;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/** 运营组合读取必须保留原详情提供者的身份和拒绝语义。 */
|
||||
class DispatchDetailReadAdapterTest {
|
||||
/** 不吞原排车资格失败,不切换操作者或降级返回空详情。 */
|
||||
@Test
|
||||
void preservesOriginalIdentityAndReadRejection() {
|
||||
var original = mock(DispatchAggregateService.class);
|
||||
var actor = new DispatchContracts.Actor("owned-business-reader", DispatchTypes.ActorMode.PLATFORM, null);
|
||||
var failure = new IllegalArgumentException("原单不可读取");
|
||||
when(original.detail(actor, "DS-OWNED-READ")).thenThrow(failure);
|
||||
var adapter = new DispatchDetailReadAdapter(original);
|
||||
var actual = assertThrows(IllegalArgumentException.class, () -> adapter.detail(actor, "DS-OWNED-READ"));
|
||||
org.junit.jupiter.api.Assertions.assertSame(failure, actual);
|
||||
verify(original).detail(actor, "DS-OWNED-READ");
|
||||
verifyNoMoreInteractions(original);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package com.baiyue.lastleg.dispatch.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
/** 使用真实SQL、连接内流水与Spring事务代理核对唯一性和事务边界。 */
|
||||
class DispatchNumberSequenceTest {
|
||||
private static class Fixture {
|
||||
final JdbcTemplate jdbc;
|
||||
final DataSourceTransactionManager transactions;
|
||||
final DispatchNumberSequence sequence;
|
||||
Fixture() {
|
||||
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_dispatch_daily_sequence(business_date DATE PRIMARY KEY,last_sequence BIGINT NOT NULL)");
|
||||
var proxy=new ProxyFactory(new DispatchNumberSequence(jdbc));proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(transactions,new AnnotationTransactionAttributeSource()));
|
||||
sequence=(DispatchNumberSequence)proxy.getProxy();
|
||||
}
|
||||
}
|
||||
@Test void startsAtOneEachDayAndContinuesAcrossGeneratorInstances() {
|
||||
var f=new Fixture();var today=LocalDate.of(2026,10,20);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(1);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(2);
|
||||
assertThat(f.sequence.next(today.plusDays(1))).isEqualTo(1);
|
||||
assertThat(f.sequence.next(today)).isEqualTo(3);
|
||||
}
|
||||
@Test void concurrentConnectionsAllocateEveryNumberOnce() throws Exception {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);var pool=Executors.newFixedThreadPool(2);
|
||||
try {
|
||||
var work=new ArrayList<Callable<Long>>();for(int i=0;i<64;i++)work.add(()->f.sequence.next(date));
|
||||
var values=new TreeSet<Long>();for(var future:pool.invokeAll(work))values.add(future.get());
|
||||
assertThat(values).hasSize(64).first().isEqualTo(1L);
|
||||
assertThat(values.last()).isEqualTo(64L);
|
||||
} finally {pool.shutdownNow();}
|
||||
}
|
||||
@Test void outerBusinessRollbackDoesNotReuseAnAlreadyAllocatedNumber() {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);
|
||||
assertThatThrownBy(()->new TransactionTemplate(f.transactions).execute(status->{
|
||||
assertThat(f.sequence.next(date)).isEqualTo(1);throw new IllegalStateException("business rollback");
|
||||
})).isInstanceOf(IllegalStateException.class);
|
||||
assertThat(f.sequence.next(date)).isEqualTo(2);
|
||||
}
|
||||
@Test void fourDigitExhaustionDoesNotWrapOrIncrementTheStoredCounter() {
|
||||
var f=new Fixture();var date=LocalDate.of(2026,10,20);
|
||||
f.jdbc.update("INSERT INTO ll_dispatch_daily_sequence VALUES(?,9999)",date);
|
||||
assertThatThrownBy(()->f.sequence.next(date)).hasMessageContaining("四位流水号已用满");
|
||||
assertThat(f.jdbc.queryForObject("SELECT last_sequence FROM ll_dispatch_daily_sequence WHERE business_date=?",Long.class,date)).isEqualTo(9999L);
|
||||
}
|
||||
}
|
||||
+3
@@ -536,6 +536,9 @@ public class DispatchAggregatePersistenceTest {
|
||||
service=new DispatchAggregateService(repository,
|
||||
new DispatchSourceRepository(session.getMapper(DispatchCandidateMapper.class),session.getMapper(ReturnDispatchCandidateMapper.class),session.getMapper(DispatchSourceMapper.class)),rules,fleet,
|
||||
new DispatchCommandLedger(new DispatchCommandRepository(session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(json)),new DispatchCommandHasher(json),json);
|
||||
var numbers=mock(DispatchNumberGenerator.class);var dailySequence=new java.util.concurrent.atomic.AtomicInteger();
|
||||
when(numbers.next()).thenAnswer(invocation->"PC20261020"+String.format(java.util.Locale.ROOT,"%04d",dailySequence.incrementAndGet()));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"numbers",numbers);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"evidence",mock(com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.class));
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(service,"ownedBusinessOwnership",ownedBusinessOwnership);
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(gateway,"workflow",service);
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.baiyue.lastleg.dispatch.service;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.repository.DispatchNumberSequence;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.time.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class DispatchNumberGeneratorTest {
|
||||
@Test void usesBusinessDateAndFourDigitsWithoutWarehouseOrCustomerSuffix() {
|
||||
var sequences=mock(DispatchNumberSequence.class);
|
||||
when(sequences.next(LocalDate.of(2026,10,20))).thenReturn(1L,42L,9999L);
|
||||
var numbers=new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-20T08:00:00Z"),ZoneOffset.UTC));
|
||||
assertThat(numbers.next()).isEqualTo("PC202610200001");
|
||||
assertThat(numbers.next()).isEqualTo("PC202610200042");
|
||||
assertThat(numbers.next()).isEqualTo("PC202610209999");
|
||||
}
|
||||
@Test void switchesToTheNewDailySequenceAtShanghaiMidnight() {
|
||||
var sequences=mock(DispatchNumberSequence.class);
|
||||
when(sequences.next(LocalDate.of(2026,10,19))).thenReturn(238L);
|
||||
when(sequences.next(LocalDate.of(2026,10,20))).thenReturn(1L);
|
||||
assertThat(new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-19T15:59:59Z"),ZoneOffset.UTC)).next()).isEqualTo("PC202610190238");
|
||||
assertThat(new DispatchNumberGenerator(sequences,Clock.fixed(Instant.parse("2026-10-19T16:00:00Z"),ZoneOffset.UTC)).next()).isEqualTo("PC202610200001");
|
||||
}
|
||||
@Test void rejectsExhaustedOrInvalidSequenceInsteadOfWrappingOrReusingNumbers() {
|
||||
for(long value:new long[]{0,10000}) {
|
||||
var sequences=mock(DispatchNumberSequence.class);when(sequences.next(any())).thenReturn(value);
|
||||
assertThatThrownBy(()->new DispatchNumberGenerator(sequences).next()).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
package com.baiyue.lastleg.fleet.ownedbusiness;
|
||||
|
||||
import com.baiyue.lastleg.dispatch.entity.DispatchItem;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.CargoRow;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.Source;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.Actor;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessSupport;
|
||||
@@ -19,7 +19,7 @@ final class OwnedBusinessFixtures {
|
||||
static Actor actor(){return new Actor("operator-1","TEST操作员",Set.of(LIST,EDIT,UPLOAD));}
|
||||
/** 装车完成但未发车,证明不依赖DEPART历史。 */
|
||||
static Source source() {
|
||||
var row=new Source();row.setId(1L);row.setDispatchNo("DS-OWN-ONE");row.setTripId(2L);row.setStatus("ACTIVE");row.setStage("AFTER_LOADING");
|
||||
var row=new Source();row.setId(1L);row.setDispatchNo("DS-OWN-ONE");row.setStatus("ACTIVE");row.setStage("AFTER_LOADING");
|
||||
row.setRecordVersion(8L);row.setOverseasWarehouseId("9");row.setOverseasWarehouseCode("WH-9");row.setOverseasWarehouseName("测试仓");
|
||||
row.setPlannedDepartureAt(LocalDateTime.parse("2026-10-20T08:00:00"));row.setLoadingCompletedAt(LocalDateTime.parse("2026-10-20T07:30:00"));
|
||||
row.setLoadingCompletionHash("actual-load");row.setOwnedLoadingCompletionHash("actual-load");row.setOwnedLoadingCompletedAt(row.getLoadingCompletedAt());
|
||||
@@ -35,12 +35,12 @@ final class OwnedBusinessFixtures {
|
||||
""");return row;
|
||||
}
|
||||
/** 两个客户逻辑来源共享一个真实物理托盘,不合并客户归属。 */
|
||||
static List<DispatchItem> cargo() {
|
||||
static List<CargoRow> cargo() {
|
||||
var first=item(11L,"101","IN-A");var second=item(12L,"102","IN-B");return List.of(first,second);
|
||||
}
|
||||
/** 来源事实的目的地键与正式排车提供者一致。 */
|
||||
private static DispatchItem item(long id,String customer,String inbound) {
|
||||
var i=new DispatchItem();i.setId(id);i.setDispatchOrderId(1L);i.setCustomerId(customer);i.setCustomerName("测试客户"+customer);
|
||||
private static CargoRow item(long id,String customer,String inbound) {
|
||||
var i=new CargoRow();i.setId(id);i.setDispatchOrderId(1L);i.setCustomerId(customer);i.setCustomerName("测试客户"+customer);
|
||||
i.setSourceKey("SOURCE:"+inbound);i.setOrderNo("ORDER-"+inbound);i.setInboundCode(inbound);i.setWarehousePalletId(31L);i.setPalletNo("PAL-31");
|
||||
i.setDestinationType("FBA");i.setDestinationWarehouseCode("ONT8");i.setDestinationWarehouseName("ONT8仓");i.setPackageCount(1);return i;
|
||||
}
|
||||
|
||||
+9
@@ -33,6 +33,15 @@ class OwnedBusinessOwnershipTest {
|
||||
assertEquals("EXTERNAL",saved.getValue().getResourceMode());assertEquals("DISPATCHED",saved.getValue().getVehicleOwnership());
|
||||
assertNull(saved.getValue().getVehicleRecordVersion());verifyNoInteractions(vehicles);
|
||||
}
|
||||
/** MASTER只是档案选择,不代表车辆自有,更不代表货源自有。 */
|
||||
@Test void masterVehicleCanBeDispatchedAndMustKeepThatOwnership() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var vehicles=mock(VehicleRepository.class);
|
||||
when(vehicles.findById(7)).thenReturn(Optional.of(vehicle("DISPATCHED",4)));
|
||||
new OwnedBusinessOwnershipService(repo,vehicles).capture(1,7L,"MASTER","load",Instant.parse("2026-10-20T07:30:00Z"));
|
||||
var saved=ArgumentCaptor.forClass(Ownership.class);verify(repo).capture(saved.capture());
|
||||
assertEquals("DISPATCHED",saved.getValue().getVehicleOwnership());assertEquals("MASTER",saved.getValue().getResourceMode());
|
||||
assertEquals(4L,saved.getValue().getVehicleRecordVersion());
|
||||
}
|
||||
/** 重放沿原证据,不读取后来可能变化的车辆归属。 */
|
||||
@Test void matchingCaptureReplaysWithoutReadingCurrentVehicle() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var vehicles=mock(VehicleRepository.class);var owned=new Ownership();
|
||||
|
||||
+40
-4
@@ -5,7 +5,7 @@ import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.service.*;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchDetailReader;
|
||||
import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||
@@ -41,7 +41,7 @@ class OwnedBusinessPersistenceTest {
|
||||
jdbc=new JdbcTemplate(ds);manager=new DataSourceTransactionManager(ds);
|
||||
jdbc.execute("""
|
||||
CREATE TABLE ll_dispatch_order(id BIGINT PRIMARY KEY,dispatch_no VARCHAR(128),trip_id BIGINT,status VARCHAR(32),
|
||||
stage VARCHAR(32),record_version BIGINT,planned_departure_at DATETIME(3),loading_completed_at DATETIME(3),
|
||||
stage VARCHAR(32),record_version BIGINT,planned_departure_at DATETIME(3),loading_completed_at DATETIME(3),departed_at DATETIME(3),delivery_completed_at DATETIME(3),
|
||||
loading_completion_hash VARCHAR(128),loading_confirmation_json LONGTEXT,resources_json LONGTEXT,
|
||||
vehicle_plate VARCHAR(64),overseas_warehouse_id VARCHAR(64),overseas_warehouse_code VARCHAR(64),overseas_warehouse_name VARCHAR(128))
|
||||
""");
|
||||
@@ -108,6 +108,19 @@ class OwnedBusinessPersistenceTest {
|
||||
assertEquals(8L,jdbc.queryForObject("SELECT record_version FROM ll_dispatch_order WHERE id=1",Long.class));
|
||||
assertEquals(2,jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item",Integer.class));
|
||||
}
|
||||
/** 真实基线CHECK必须拒绝SQL三值逻辑的UNKNOWN漏洞,同时保留未登记和明确零。 */
|
||||
@Test void moneyCheckRejectsNullCurrencyWithAmountAndAcceptsNullPairAndZero() {
|
||||
var actor=OwnedBusinessFixtures.actor();
|
||||
service.save(actor,"DS-OWN-ONE",request("money-null-pair",0,null,null,null,List.of()));
|
||||
assertNull(repository.record(1).getCurrency());assertNull(repository.record(1).getTotalAmount());
|
||||
assertThrows(org.springframework.dao.DataIntegrityViolationException.class,
|
||||
()->jdbc.update("UPDATE ll_fleet_owned_business_record SET currency=NULL,total_amount=1 WHERE dispatch_id=1"));
|
||||
assertNull(repository.record(1).getCurrency());assertNull(repository.record(1).getTotalAmount());
|
||||
service.save(actor,"DS-OWN-ONE",request("money-zero-value",1,new Money("CNY","0"),null,null,List.of()));
|
||||
assertEquals("CNY",repository.record(1).getCurrency());assertEquals(0,repository.record(1).getTotalAmount().signum());
|
||||
service.save(actor,"DS-OWN-ONE",request("money-clear-again",2,null,null,null,List.of()));
|
||||
assertNull(repository.record(1).getCurrency());assertNull(repository.record(1).getTotalAmount());
|
||||
}
|
||||
/** 原成功载荷重放返回原版本,不能用新版本或不同操作者另起命令。 */
|
||||
@Test void replayAndStaleVersionDoNotCreateExtraRecordsOrHistory() {
|
||||
var first=request("first-operation",0,new Money("USD","0"),null,null,List.of());
|
||||
@@ -125,10 +138,31 @@ class OwnedBusinessPersistenceTest {
|
||||
assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM ll_fleet_owned_business_record",Integer.class));
|
||||
assertEquals(0,jdbc.queryForObject("SELECT COUNT(*) FROM ll_fleet_owned_business_command",Integer.class));
|
||||
}
|
||||
/** 真绑定CAS、原键重放及替换后的历史引用均沿真实4表,不改变装车事实。 */
|
||||
@Test void photoBindingKeepsCurrentAndHistoricalAuthorizationSeparate() {
|
||||
var first=new Photo(PhotoSlot.LOADING_SCENE,"LOADING","9","TEST-one.png","image/png","9","2026-10-20T07:30:00",null);
|
||||
var original=new BindPhoto("0","bind-original","9");
|
||||
var bound=service.bind(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",PhotoSlot.LOADING_SCENE,original,first);
|
||||
assertEquals("1",bound.row().recordVersion());assertNotNull(bound.photos().get(0).savedAt());
|
||||
assertEquals("保存装车现场照片",repository.revision(1,1).getReason());
|
||||
assertEquals("1",service.bind(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",PhotoSlot.LOADING_SCENE,original,first).row().recordVersion());
|
||||
var replacement=new Photo(PhotoSlot.LOADING_SCENE,"LOADING","10","TEST-two.png","image/png","9","2026-10-20T07:31:00",null);
|
||||
service.bind(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",PhotoSlot.LOADING_SCENE,new BindPhoto("1","bind-replacement","10"),replacement);
|
||||
assertEquals("10",projection.photos(repository.record(1)).get(0).fileId());
|
||||
assertEquals("9",codec.read(repository.revision(1,1).getSnapshotJson(),Snapshot.class).photos().get(0).fileId());
|
||||
assertEquals("保存装车现场照片",repository.revision(1,2).getReason());
|
||||
var file=mock(com.baiyue.lastleg.fleet.ownedbusiness.gateway.OwnedBusinessFileGateway.class);
|
||||
when(file.content(1,PhotoSlot.LOADING_SCENE,9)).thenReturn(new byte[]{1,2});
|
||||
var photoService=new OwnedBusinessPhotoService(queries,service,repository,commands,projection,codec,file);
|
||||
assertThrows(BizException.class,()->photoService.content(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",PhotoSlot.LOADING_SCENE,9,null));
|
||||
assertArrayEquals(new byte[]{1,2},photoService.content(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",PhotoSlot.LOADING_SCENE,9,1L).content());
|
||||
assertEquals(8L,jdbc.queryForObject("SELECT record_version FROM ll_dispatch_order WHERE id=1",Long.class));
|
||||
assertEquals(2,jdbc.queryForObject("SELECT COUNT(*) FROM ll_fleet_owned_business_history",Integer.class));
|
||||
}
|
||||
/** 原流水与附加流水用一次真实SQL分页,版本域不混用。 */
|
||||
@Test void historyMergesOriginalDispatchWithoutDuplicatingIt() {
|
||||
service.save(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",request("history-register",0,null,null,"0",List.of()));
|
||||
var realQueries=new OwnedBusinessQueryService(repository,projection,codec,mock(DispatchAggregateService.class));
|
||||
var realQueries=new OwnedBusinessQueryService(repository,projection,codec,mock(DispatchDetailReader.class));
|
||||
var page=realQueries.history(OwnedBusinessFixtures.actor(),"DS-OWN-ONE",1,20);
|
||||
assertEquals(2,page.total());assertEquals(2,page.records().size());
|
||||
var original=page.records().stream().filter(r->r.eventSource().equals("DISPATCH")).findFirst().orElseThrow();
|
||||
@@ -139,7 +173,7 @@ class OwnedBusinessPersistenceTest {
|
||||
}
|
||||
/** 当前页不是统计范围,装车后无DEPART也能入选;GET不建附加记录。 */
|
||||
@Test void listUsesFilteredSourceAndLeavesVirtualZeroUnwritten() {
|
||||
var realQueries=new OwnedBusinessQueryService(repository,projection,codec,mock(DispatchAggregateService.class));
|
||||
var realQueries=new OwnedBusinessQueryService(repository,projection,codec,mock(DispatchDetailReader.class));
|
||||
var list=realQueries.list(OwnedBusinessFixtures.actor(),new Query(1,20,"2026-10-20T00:00:00Z","2026-10-21T00:00:00Z","ONT8",null,"9"));
|
||||
assertEquals(1,list.total());assertEquals(1,list.summary().afterLoadingCount());assertEquals(1,list.summary().dispatchCount());
|
||||
assertEquals("0",list.records().get(0).recordVersion());assertEquals(1,list.records().get(0).actualPalletCount());
|
||||
@@ -187,6 +221,8 @@ class OwnedBusinessPersistenceTest {
|
||||
var proxy=new ProxyFactory(value);proxy.setProxyTargetClass(true);
|
||||
proxy.addAdvice(new TransactionInterceptor(manager,new AnnotationTransactionAttributeSource()));return (T)proxy.getProxy();
|
||||
}
|
||||
/** 本机内存数据库用后关闭,避免全量回归积累DB_CLOSE_DELAY实例。 */
|
||||
@AfterEach void closeDatabase(){if(jdbc!=null)jdbc.execute("SHUTDOWN");}
|
||||
/** 每个请求保留原附加版本和明确量纲。 */
|
||||
private SaveOperations request(String key,long version,Money money,String distance,String fuel,List<Arrival> arrivals) {
|
||||
return new SaveOperations(String.valueOf(version),key,new Operations(money,distance,fuel,arrivals),"本机TEST合成记录");
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@ package com.baiyue.lastleg.fleet.ownedbusiness;
|
||||
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.lastleg.dispatch.service.DispatchAggregateService;
|
||||
import com.baiyue.lastleg.dispatch.port.DispatchDetailReader;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessRows.*;
|
||||
import com.baiyue.lastleg.fleet.ownedbusiness.repository.OwnedBusinessRepository;
|
||||
@@ -21,7 +21,7 @@ class OwnedBusinessQueryTest {
|
||||
/** 当前页只有一车时统计仍来自全量,日期边界转换为UTC且共用过滤。 */
|
||||
@Test void summaryDoesNotUseCurrentPageLengthAndDateUsesPlannedDeparture() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var codec=OwnedBusinessFixtures.codec();
|
||||
var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,mock(DispatchAggregateService.class));
|
||||
var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,mock(DispatchDetailReader.class));
|
||||
var after=new StageCount();after.setStage("AFTER_LOADING");after.setQuantity(2L);
|
||||
var completed=new StageCount();completed.setStage("DELIVERED");completed.setQuantity(1L);
|
||||
when(repo.summary(any())).thenReturn(List.of(after,completed));when(repo.page(any())).thenReturn(List.of(OwnedBusinessFixtures.source()));
|
||||
@@ -37,14 +37,14 @@ class OwnedBusinessQueryTest {
|
||||
@Test void oldLoadedDispatchWithoutOwnershipIsNotReclassified() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var codec=OwnedBusinessFixtures.codec();var source=OwnedBusinessFixtures.source();
|
||||
source.setOwnedVehicleOwnership(null);when(repo.source("DS-OWN-ONE")).thenReturn(source);
|
||||
var dispatch=mock(DispatchAggregateService.class);var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,dispatch);
|
||||
var dispatch=mock(DispatchDetailReader.class);var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,dispatch);
|
||||
assertThrows(BizException.class,()->service.detail(OwnedBusinessFixtures.actor(),"DS-OWN-ONE"));
|
||||
verifyNoInteractions(dispatch);verify(repo,never()).capture(any());assertEquals(8L,source.getRecordVersion());
|
||||
}
|
||||
/** 装车前、非自有归属、失配的原装车hash均不能作为本页可写来源。 */
|
||||
@Test void stageAndFrozenOwnershipFactsAreBothRequired() {
|
||||
var repo=mock(OwnedBusinessRepository.class);var codec=OwnedBusinessFixtures.codec();
|
||||
var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,mock(DispatchAggregateService.class));
|
||||
var service=new OwnedBusinessQueryService(repo,new OwnedBusinessProjection(codec),codec,mock(DispatchDetailReader.class));
|
||||
var source=OwnedBusinessFixtures.source();when(repo.source("DS-OWN-ONE")).thenReturn(source);
|
||||
source.setStage("LOADING");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
source.setStage("AFTER_LOADING");source.setOwnedVehicleOwnership("DISPATCHED");assertThrows(BizException.class,()->service.requireOwned("DS-OWN-ONE"));
|
||||
|
||||
+15
-2
@@ -11,11 +11,16 @@ import java.util.*;
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
class FleetTripCalendarMapperTest {
|
||||
/** 同时验证计划和实发的半开范围,取货车次不能混入送仓日历。 */
|
||||
/** 测试中为每个运输资源建立唯一整车主单,独立旧车次始终不绑定主单。 */
|
||||
private void bindDispatches(JdbcTemplate jdbc) {
|
||||
jdbc.execute("INSERT INTO ll_dispatch_order(dispatch_no,trip_id,status) SELECT CONCAT('DS-',t.trip_no),t.id,'ACTIVE' FROM ll_fleet_trip t WHERE t.trip_no<>'TR-UNBOUND' AND NOT EXISTS(SELECT 1 FROM ll_dispatch_order d WHERE d.trip_id=t.id)");
|
||||
}
|
||||
/** 验证真实主单关系、计划/实发半开区间及取货排除,不把资源车次号当排车号。 */
|
||||
@Test void queriesAHalfOpenUtcWindowWithoutStatusFilteringAndCapsCandidateRows() throws Exception {
|
||||
var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");
|
||||
var jdbc=new JdbcTemplate(ds);
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip(trip_no VARCHAR(64) PRIMARY KEY,status VARCHAR(32),trip_purpose VARCHAR(32) DEFAULT 'DELIVERY',warehouse_id BIGINT,warehouse_code VARCHAR(64),warehouse_name VARCHAR(100),planned_departure_at TIMESTAMP,expected_driver_end_at TIMESTAMP,departed_at TIMESTAMP,completed_at TIMESTAMP,stops_json VARCHAR(2000))");
|
||||
jdbc.execute("CREATE TABLE ll_fleet_trip(id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,trip_no VARCHAR(64) UNIQUE,status VARCHAR(32),trip_purpose VARCHAR(32) DEFAULT 'DELIVERY',warehouse_id BIGINT,warehouse_code VARCHAR(64),warehouse_name VARCHAR(100),planned_departure_at TIMESTAMP,expected_driver_end_at TIMESTAMP,departed_at TIMESTAMP,completed_at TIMESTAMP,stops_json VARCHAR(2000))");
|
||||
jdbc.execute("CREATE TABLE ll_dispatch_order(dispatch_no VARCHAR(80) PRIMARY KEY,trip_id BIGINT UNIQUE,status VARCHAR(32))");
|
||||
var from=LocalDateTime.of(2026,11,1,0,0);var to=from.plusDays(1);
|
||||
String insert="INSERT INTO ll_fleet_trip(trip_no,status,warehouse_id,planned_departure_at,expected_driver_end_at,stops_json) VALUES(?,?,1,?,?, '[]')";
|
||||
jdbc.update(insert,"TR-BEFORE","PLANNED",from.minusSeconds(1),to);
|
||||
@@ -26,11 +31,17 @@ class FleetTripCalendarMapperTest {
|
||||
jdbc.update(insert,"TR-PICKUP-ACTUAL","IN_TRANSIT",from.minusDays(2),to);
|
||||
jdbc.update("UPDATE ll_fleet_trip SET trip_purpose='PICKUP' WHERE trip_no LIKE 'TR-PICKUP-%'");
|
||||
jdbc.update("UPDATE ll_fleet_trip SET departed_at=? WHERE trip_no='TR-PICKUP-ACTUAL'",from.plusHours(1));
|
||||
bindDispatches(jdbc);
|
||||
jdbc.update(insert,"TR-UNBOUND","PLANNED",from,to);
|
||||
jdbc.update(insert,"TR-CANCELLED-DISPATCH","PLANNED",from,to);
|
||||
bindDispatches(jdbc);
|
||||
jdbc.update("UPDATE ll_dispatch_order SET status='CANCELLED' WHERE dispatch_no='DS-TR-CANCELLED-DISPATCH'");
|
||||
var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(FleetTripMapper.class);
|
||||
var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);
|
||||
var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(FleetTripMapper.class);
|
||||
var rows=mapper.calendar(from,to);
|
||||
assertThat(rows).extracting(FleetTripEntity::getTripNo).containsExactly("TR-FROM","TR-LAST");
|
||||
assertThat(rows).extracting(FleetTripCalendarRow::getDispatchNo).containsExactly("DS-TR-FROM","DS-TR-LAST");
|
||||
assertThat(rows).extracting(FleetTripEntity::getStatus).containsExactly("CANCELLED","COMPLETED");
|
||||
assertThat(rows.get(0).getPlannedDepartureAt()).isEqualTo(from);
|
||||
// The actual month is authoritative even when the original plan is outside this window.
|
||||
@@ -40,6 +51,7 @@ class FleetTripCalendarMapperTest {
|
||||
jdbc.update("UPDATE ll_fleet_trip SET departed_at=? WHERE trip_no=?",to,"TR-ACTUAL-OUT");
|
||||
jdbc.update(insert,"TR-ACTUAL-FROM","IN_TRANSIT",from.minusDays(2),to);
|
||||
jdbc.update("UPDATE ll_fleet_trip SET departed_at=? WHERE trip_no=?",from,"TR-ACTUAL-FROM");
|
||||
bindDispatches(jdbc);
|
||||
assertThat(mapper.calendar(from,to)).extracting(FleetTripEntity::getTripNo)
|
||||
.containsExactly("TR-ACTUAL-FROM","TR-FROM","TR-ACTUAL-IN","TR-LAST");
|
||||
jdbc.update("UPDATE ll_fleet_trip SET planned_departure_at=? WHERE trip_no=?",to.plusDays(20),"TR-ACTUAL-IN");
|
||||
@@ -47,6 +59,7 @@ class FleetTripCalendarMapperTest {
|
||||
.containsExactly("TR-ACTUAL-FROM","TR-FROM","TR-ACTUAL-IN","TR-LAST");
|
||||
var many=new ArrayList<Object[]>();for(int i=0;i<2002;i++)many.add(new Object[]{"TR-MANY-"+i,"PLANNED",from.plusHours(1),to});
|
||||
jdbc.batchUpdate(insert,many);
|
||||
bindDispatches(jdbc);
|
||||
assertThat(mapper.calendar(from,to)).hasSize(2001);
|
||||
}
|
||||
}
|
||||
|
||||
+10
-10
@@ -15,8 +15,8 @@ class FleetTripCalendarServiceTest {
|
||||
final FleetWarehouseClient warehouses=mock(FleetWarehouseClient.class);
|
||||
final FleetTripCalendarService service=new FleetTripCalendarService(trips,warehouses,new ObjectMapper(),"internal-test");
|
||||
final Instant from=Instant.parse("2026-10-01T00:00:00Z"),to=from.plus(Duration.ofDays(44));
|
||||
FleetTripEntity trip(String no,Long warehouse) {
|
||||
var row=new FleetTripEntity();row.setTripNo(no);row.setWarehouseId(warehouse);row.setStatus("PLANNED");
|
||||
FleetTripCalendarRow trip(String no,Long warehouse) {
|
||||
var row=new FleetTripCalendarRow();row.setTripNo("INTERNAL-"+no);row.setDispatchNo(no);row.setWarehouseId(warehouse);row.setStatus("PLANNED");
|
||||
row.setPlannedDepartureAt(LocalDateTime.ofInstant(from,ZoneOffset.UTC));
|
||||
row.setExpectedDriverEndAt(row.getPlannedDepartureAt().plusDays(2));
|
||||
row.setStopsJson("[{\"name\":\"Origin\"},{\"name\":\"ACY2\",\"destinationKey\":\"FBA:ACY2\"}]");return row;
|
||||
@@ -44,13 +44,13 @@ class FleetTripCalendarServiceTest {
|
||||
metadata(List.of(warehouse(1,"US","America/Los_Angeles"),warehouse(2,"CA","America/Toronto"),warehouse(3,"US","UTC"),warehouse(4,null,"America/New_York")),List.of(5L));
|
||||
var result=service.calendar(from,to);
|
||||
assertThat(result.records()).singleElement().satisfies(row->{
|
||||
assertThat(row.sourceType()).isEqualTo("TRIP_PLAN");assertThat(row.tripNo()).isEqualTo("TR-US");
|
||||
assertThat(row.sourceType()).isEqualTo("DISPATCH");assertThat(row.dispatchNo()).isEqualTo("TR-US");
|
||||
assertThat(row.status()).isEqualTo("CANCELLED");assertThat(row.originWarehouseStatus()).isEqualTo("INACTIVE");
|
||||
assertThat(row.plannedDepartureAt()).isEqualTo(from);assertThat(row.departedAt()).isEqualTo(from.plusSeconds(3600));
|
||||
assertThat(row.expectedDriverEndAt()).isEqualTo(from.plus(Duration.ofDays(2)));assertThat(row.completedAt()).isNull();
|
||||
assertThat(row.stops()).hasSize(2);assertThat(row.incompleteReasons()).isEmpty();
|
||||
});
|
||||
assertThat(result.unresolvedRecords()).extracting(FleetTripCalendarService.Entry::tripNo).containsExactly("TR-UTC","TR-COUNTRY","TR-MISSING","TR-NO-ORIGIN");
|
||||
assertThat(result.unresolvedRecords()).extracting(FleetTripCalendarService.Entry::dispatchNo).containsExactly("TR-UTC","TR-COUNTRY","TR-MISSING","TR-NO-ORIGIN");
|
||||
assertThat(result.unresolvedRecords().get(0).originTimezone()).isNull();
|
||||
assertThat(result.unresolvedRecords().get(0).incompleteReasons()).containsExactly("ORIGIN_TIMEZONE_UNKNOWN");
|
||||
assertThat(result.unresolvedRecords().get(2).incompleteReasons()).contains("ORIGIN_WAREHOUSE_UNKNOWN");
|
||||
@@ -78,7 +78,7 @@ class FleetTripCalendarServiceTest {
|
||||
}
|
||||
@Test void endpointUsesExistingReadPermission() throws Exception {
|
||||
var method=PlatformFleetTripController.class.getMethod("calendar",com.baiyue.common.security.AuthenticatedUser.class,Instant.class,Instant.class);
|
||||
assertThat(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value()).isEqualTo("hasAuthority('platform:fleet:trip:list')");
|
||||
assertThat(method.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value()).isEqualTo("hasAuthority('platform:overseas-warehouse:dispatch:list')");
|
||||
}
|
||||
@Test void systemVAndFixedOffsetsAreNotGeographicOriginTimezones() {
|
||||
when(trips.calendar(any(),any())).thenReturn(List.of(trip("TR-1",1L)));
|
||||
@@ -98,13 +98,13 @@ class FleetTripCalendarServiceTest {
|
||||
org.springframework.test.util.ReflectionTestUtils.setField(controller,"calendar",calendar);
|
||||
assertThatThrownBy(()->controller.calendar(null,from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
for (var user:List.of(
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"client","MERCHANT","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,9L,9L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,null,0L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(null,0L,0L,1L,"client","PLATFORM","WEB",Set.of("platform:fleet:trip:list"))))
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"client","MERCHANT","WEB",Set.of("platform:overseas-warehouse:dispatch:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,9L,9L,1L,"client","PLATFORM","WEB",Set.of("platform:overseas-warehouse:dispatch:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(1L,null,0L,1L,"client","PLATFORM","WEB",Set.of("platform:overseas-warehouse:dispatch:list")),
|
||||
new com.baiyue.common.security.AuthenticatedUser(null,0L,0L,1L,"client","PLATFORM","WEB",Set.of("platform:overseas-warehouse:dispatch:list"))))
|
||||
assertThatThrownBy(()->controller.calendar(user,from,to)).isInstanceOf(ResponseStatusException.class);
|
||||
verifyNoInteractions(calendar);
|
||||
var platform=new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"platform","PLATFORM","WEB",Set.of("platform:fleet:trip:list"));
|
||||
var platform=new com.baiyue.common.security.AuthenticatedUser(1L,0L,0L,1L,"platform","PLATFORM","WEB",Set.of("platform:overseas-warehouse:dispatch:list"));
|
||||
controller.calendar(platform,from,to);verify(calendar).calendar(from,to);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,31 @@
|
||||
"com.baiyue.lastleg.security.service.SecurityProjectionMonotonicity"
|
||||
],
|
||||
"reviewedClassDependencies": [
|
||||
{
|
||||
"source": "com.baiyue.lastleg.fleet.ownedbusiness.model.OwnedBusinessModels",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.DispatchViews",
|
||||
"reason": "自有业务组合原排车公开只读详情,不复制履约主模型或引用实体。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessProjection",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.DispatchContracts",
|
||||
"reason": "从原装车确认与资源公开快照计算真实物理托数,仅消费不可变事实,不写原排车。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessQueryService",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.DispatchContracts",
|
||||
"reason": "按可信平台身份读取原排车公开快照,核对原装车归属;不访问主流程持久化实体。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessQueryService",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.DispatchTypes",
|
||||
"reason": "原排车只读详情端口需要可信平台ActorMode,不接受浏览器提供的执行身份。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.fleet.ownedbusiness.service.OwnedBusinessQueryService",
|
||||
"target": "com.baiyue.lastleg.dispatch.port.DispatchDetailReader",
|
||||
"reason": "运营视图通过只读应用端口组合原详情;不直接调用原Service或Mapper,不开放其他消费者。"
|
||||
},
|
||||
{
|
||||
"source": "com.baiyue.lastleg.transfer.repository.TransferForecastRepository",
|
||||
"target": "com.baiyue.lastleg.inboundplan.entity.InboundPlanLine",
|
||||
|
||||
Reference in New Issue
Block a user