Compare commits

...
2 Commits
3 changed files with 77 additions and 2 deletions
@@ -17,12 +17,13 @@ public class TripOperationsRepository {
ORDER BY h.record_version LIMIT 1) AS snapshot_json, ORDER BY h.record_version LIMIT 1) AS snapshot_json,
(SELECT r.details_json FROM ll_fleet_trip_operations_record r WHERE r.trip_no=t.trip_no (SELECT r.details_json FROM ll_fleet_trip_operations_record r WHERE r.trip_no=t.trip_no
ORDER BY r.record_version DESC LIMIT 1) AS measured_json ORDER BY r.record_version DESC LIMIT 1) AS measured_json
FROM ll_fleet_trip t WHERE t.departed_at >= ? AND t.departed_at < ? FROM ll_fleet_trip t WHERE t.trip_purpose='DELIVERY' AND t.departed_at >= ? AND t.departed_at < ?
"""); """);
var args = new ArrayList<Object>(); var args = new ArrayList<Object>();
args.add(LocalDateTime.ofInstant(query.from(), ZoneOffset.UTC)); args.add(LocalDateTime.ofInstant(query.to(), ZoneOffset.UTC)); args.add(LocalDateTime.ofInstant(query.from(), ZoneOffset.UTC)); args.add(LocalDateTime.ofInstant(query.to(), ZoneOffset.UTC));
if (query.warehouseId() != null) { sql.append(" AND t.warehouse_id=?"); args.add(query.warehouseId()); } if (query.warehouseId() != null) { sql.append(" AND t.warehouse_id=?"); args.add(query.warehouseId()); }
if (query.status() != null && !query.status().isBlank()) { sql.append(" AND t.status=?"); args.add(query.status()); } if (query.status() != null && !query.status().isBlank()) { sql.append(" AND t.status=?"); args.add(query.status()); }
// 派送统计只消费 DEPART 实载快照;取货与本土卡车使用各自的执行历史,不能混入此投影。
// Search by stable business number; vehicle/driver search is applied to the immutable departure snapshot. // Search by stable business number; vehicle/driver search is applied to the immutable departure snapshot.
sql.append(" ORDER BY t.departed_at DESC,t.id DESC LIMIT 5001"); sql.append(" ORDER BY t.departed_at DESC,t.id DESC LIMIT 5001");
return jdbc.query(sql.toString(), (r, i) -> new Departure(r.getString("trip_no"), r.getString("status"), return jdbc.query(sql.toString(), (r, i) -> new Departure(r.getString("trip_no"), r.getString("status"),
@@ -3,7 +3,7 @@
"info": { "info": {
"title": "真实出车与自有外接运营统计", "title": "真实出车与自有外接运营统计",
"version": "1.0.0", "version": "1.0.0",
"description": "仅实际departed_at非空车次;使用DEPART不可变快照,一车次计一次,跨客户或多目的地不重复。物理托盘去重;自有与外接箱数以BOUND外接实收归属核算,支持同排车/同托混货。全筛选汇总与分页同快照;sourceType筛选含该类货的整车,并非裁切另一类货物。from含、to不含,UTC,最多366天/5000车次。发车车牌/司机使用历史,货物结案状态使用当前事实。约定金额按币种分别累计,不是财务应收/实收;列表财务按需读取,未读取明确NOT_QUERIED;单车次财务仅按DEPART客户/排车核对唯一派送来源,Finance返回已审核有效应收与明确归属整车次账单的确认收款分配。混合来源、无收款分配或多车次归属不估算,显示待分配。应收额仍按Finance实际币种展示,不把外接约定金额当应收;成本只通过FleetTripCostService.summaries(Collection<String>)按最多100车次一批读取,保持现有trip:list成本读权限。无权限为FORBIDDEN,既有车次无登记为NOT_RECORDED;可读汇总仅包含已登记直接成本,返回未登记及未能分摊车次数,未知分摊值为null。查询不跨服务逐行HTTP,不写执行状态。 图表与分页共用全筛选快照:按UTC日展示实运托盘及外接约定金额,目的地按稳定键统计涉及车次。按托均价仅包含PALLET计价,用该币种约定总金额除确认计价标准托位,ORDER报价排除。里程mile、油耗L是授权补充事实,与车辆档案累计里程和直接成本独立。" "description": "仅DELIVERY派送且实际departed_at非空车次;PICKUP取货和LOCAL_TRUCK本土卡车使用不同执行事实,不纳入本派送统计;使用DEPART不可变快照,一车次计一次,跨客户或多目的地不重复。物理托盘去重;自有与外接箱数以BOUND外接实收归属核算,支持同排车/同托混货。全筛选汇总与分页同快照;sourceType筛选含该类货的整车,并非裁切另一类货物。from含、to不含,UTC,最多366天/5000车次。发车车牌/司机使用历史,货物结案状态使用当前事实。约定金额按币种分别累计,不是财务应收/实收;列表财务按需读取,未读取明确NOT_QUERIED;单车次财务仅按DEPART客户/排车核对唯一派送来源,Finance返回已审核有效应收与明确归属整车次账单的确认收款分配。混合来源、无收款分配或多车次归属不估算,显示待分配。应收额仍按Finance实际币种展示,不把外接约定金额当应收;成本只通过FleetTripCostService.summaries(Collection<String>)按最多100车次一批读取,保持现有trip:list成本读权限。无权限为FORBIDDEN,既有车次无登记为NOT_RECORDED;可读汇总仅包含已登记直接成本,返回未登记及未能分摊车次数,未知分摊值为null。查询不跨服务逐行HTTP,不写执行状态。 图表与分页共用全筛选快照:按UTC日展示实运托盘及外接约定金额,目的地按稳定键统计涉及车次。按托均价仅包含PALLET计价,用该币种约定总金额除确认计价标准托位,ORDER报价排除。里程mile、油耗L是授权补充事实,与车辆档案累计里程和直接成本独立。"
}, },
"security": [ "security": [
{ {
@@ -0,0 +1,74 @@
package com.baiyue.lastleg.fleet.reporting;
import com.baiyue.lastleg.fleet.trip.FleetTripCostService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import java.time.Instant;
import java.util.UUID;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.mock;
class TripOperationsRepositoryTest {
private JdbcTemplate jdbc;
private TripOperationsRepository repository;
@BeforeEach void setUp() {
jdbc = new JdbcTemplate(new DriverManagerDataSource(
"jdbc:h2:mem:trip-report-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""));
jdbc.execute("CREATE TABLE ll_fleet_trip(id BIGINT PRIMARY KEY,trip_no VARCHAR(64),trip_purpose VARCHAR(16),status VARCHAR(32),warehouse_id BIGINT,departed_at TIMESTAMP,completed_at TIMESTAMP)");
jdbc.execute("CREATE TABLE ll_fleet_trip_history(trip_id BIGINT,record_version BIGINT,action VARCHAR(32),snapshot_json CLOB)");
jdbc.execute("CREATE TABLE ll_fleet_trip_operations_record(trip_no VARCHAR(64),record_version BIGINT,details_json CLOB)");
repository = new TripOperationsRepository(jdbc);
}
@Test void pickupAndLocalTruckFactsDoNotBreakDeliveryReport() {
trip(1, "TR-A", "DELIVERY", "IN_TRANSIT", 1, "2026-10-01 00:00:00");
jdbc.update("INSERT INTO ll_fleet_trip_history VALUES(1,3,'DEPART',?)", TripOperationsServiceTest.DEPARTURE);
trip(2, "PICKUP-A", "PICKUP", "COMPLETED", 1, "2026-10-02 00:00:00");
jdbc.update("INSERT INTO ll_fleet_trip_history VALUES(2,3,'PICKUP_PICKED','{}'),(2,4,'PICKUP_ARRIVED','{}')");
trip(3, "TRUCK-A", "LOCAL_TRUCK", "IN_TRANSIT", 1, "2026-10-02 00:00:00");
var result = service().report(query(null, null), false);
assertThat(result.page().records()).extracting(TripOperationsModels.Row::tripNo).containsExactly("TR-A");
assertThat(result.summary().trips()).isEqualTo(1);
assertThat(result.summary().physicalPallets()).isEqualTo(1);
assertThat(result.summary().ownPackages()).isEqualTo(11);
assertThat(result.summary().externalPackages()).isEqualTo(4);
}
@Test void deliveryFiltersKeepInclusiveStartExclusiveEndAndWarehouseStatus() {
trip(1, "START", "DELIVERY", "COMPLETED", 1, "2026-10-01 00:00:00");
trip(2, "END", "DELIVERY", "COMPLETED", 1, "2026-10-03 00:00:00");
trip(3, "BEFORE", "DELIVERY", "COMPLETED", 1, "2026-09-30 23:59:59");
trip(4, "OTHER-WAREHOUSE", "DELIVERY", "COMPLETED", 2, "2026-10-02 00:00:00");
trip(5, "IN-TRANSIT", "DELIVERY", "IN_TRANSIT", 1, "2026-10-02 00:00:00");
assertThat(repository.departures(query(1L, "COMPLETED")))
.extracting(TripOperationsRepository.Departure::tripNo).containsExactly("START");
}
@Test void missingDeliveryHistoryStillFailsInsteadOfSilentlyDroppingTheTrip() {
trip(1, "MISSING", "DELIVERY", "IN_TRANSIT", 1, "2026-10-01 00:00:00");
assertThatThrownBy(() -> service().report(query(null, null), false))
.isInstanceOf(IllegalStateException.class).hasMessageContaining("不可变历史").hasMessageContaining("MISSING");
}
private void trip(long id, String number, String purpose, String status, long warehouse, String departed) {
jdbc.update("INSERT INTO ll_fleet_trip VALUES(?,?,?,?,?,?,NULL)", id, number, purpose, status, warehouse, departed);
}
private TripOperationsModels.Query query(Long warehouse, String status) {
return new TripOperationsModels.Query(1, 20, Instant.parse("2026-10-01T00:00:00Z"),
Instant.parse("2026-10-03T00:00:00Z"), warehouse, null, null, status);
}
private TripOperationsService service() {
return new TripOperationsService(repository, new ObjectMapper().registerModule(new JavaTimeModule()), mock(FleetTripCostService.class));
}
}