fix(appointments): open canonical dispatch details from calendar

This commit is contained in:
2026-10-20 16:36:22 +08:00
parent da6b4685d7
commit 6f31fa2131
7 changed files with 62 additions and 33 deletions
@@ -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; }
}
@@ -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}")
@@ -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));
}